Skip to content

Commit 5cf646d

Browse files
committed
fix(eve): stabilize dev host artifact paths
Signed-off-by: Casey Gowrie <ctgowrie@gmail.com>
1 parent 5035812 commit 5cf646d

8 files changed

Lines changed: 212 additions & 26 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eve": patch
3+
---
4+
5+
`eve dev` now keeps Nitro build inputs outside prunable runtime snapshots. Long-running development servers no longer fail structural rebuilds with stale import errors after snapshot cleanup.

packages/eve/src/internal/application/compiled-artifacts.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export interface GeneratedCompiledArtifactsFiles {
3737
instrumentationSourcePath?: string;
3838
}
3939

40+
export const GENERATED_COMPILED_ARTIFACT_FILE_NAMES = [
41+
"compiled-artifacts-bootstrap.mjs",
42+
"compiled-artifacts-instrumentation.mjs",
43+
"compiled-artifacts-workflow-world.mjs",
44+
] as const;
45+
4046
/**
4147
* Writes the generated compiled-artifacts bootstrap module.
4248
*
@@ -97,10 +103,6 @@ export async function writeCompiledArtifactsFiles(input: {
97103

98104
const INSTRUMENTATION_EXTENSIONS = [".ts", ".mts", ".js", ".mjs"];
99105

100-
/**
101-
* Resolves the optional `agent/instrumentation` module from the agent root
102-
* directory. Returns the absolute path if found, `undefined` otherwise.
103-
*/
104106
function resolveInstrumentationModule(agentRoot: string): string | undefined {
105107
for (const ext of INSTRUMENTATION_EXTENSIONS) {
106108
const candidate = join(agentRoot, `instrumentation${ext}`);

packages/eve/src/internal/application/paths.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export function resolveNitroBuildDirectory(
5454
return join(rootDirectory, surface);
5555
}
5656

57+
export function resolveApplicationHostArtifactsDirectory(appRoot: string): string {
58+
return join(appRoot, ".eve", "host");
59+
}
60+
5761
/**
5862
* Resolves the staged Nitro output directory for one isolated build surface.
5963
*/

packages/eve/src/internal/nitro/dev-runtime-artifacts.integration.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,46 @@ describe("development runtime artifact snapshots", () => {
155155
});
156156
});
157157

158+
it("excludes generated host entrypoints from runtime snapshots", async () => {
159+
const appRoot = await createScratchDirectory("eve-dev-runtime-host-entrypoints-");
160+
const agentRoot = join(appRoot, "agent");
161+
const compileDirectoryPath = join(appRoot, ".eve", "compile");
162+
163+
await mkdir(agentRoot, { recursive: true });
164+
await mkdir(compileDirectoryPath, { recursive: true });
165+
await writeFile(join(appRoot, "package.json"), '{"type":"module"}\n');
166+
await writeFile(
167+
join(compileDirectoryPath, "compiled-agent-manifest.json"),
168+
`${JSON.stringify({ agentRoot, appRoot }, null, 2)}\n`,
169+
);
170+
await Promise.all(
171+
[
172+
"compiled-artifacts-bootstrap.mjs",
173+
"compiled-artifacts-instrumentation.mjs",
174+
"compiled-artifacts-workflow-world.mjs",
175+
].map((fileName) => writeFile(join(compileDirectoryPath, fileName), "stale\n")),
176+
);
177+
178+
const snapshot = await stageDevelopmentRuntimeArtifactsSnapshot({
179+
paths: { compileDirectoryPath },
180+
project: { appRoot },
181+
} as CompileAgentResult);
182+
const snapshotCompileDirectory = join(snapshot.runtimeAppRoot, ".eve", "compile");
183+
184+
await expect(
185+
readFile(join(snapshotCompileDirectory, "compiled-agent-manifest.json"), "utf8").then(
186+
(source) => JSON.parse(source) as { appRoot: string },
187+
),
188+
).resolves.toMatchObject({ appRoot: snapshot.runtimeAppRoot });
189+
for (const fileName of [
190+
"compiled-artifacts-bootstrap.mjs",
191+
"compiled-artifacts-instrumentation.mjs",
192+
"compiled-artifacts-workflow-world.mjs",
193+
]) {
194+
expect(existsSync(join(snapshotCompileDirectory, fileName))).toBe(false);
195+
}
196+
});
197+
158198
it("stages package-less flat markdown agents with generated runtime package metadata", async () => {
159199
const appRoot = await createScratchDirectory("eve-dev-runtime-flat-package-less-");
160200
const compileDirectoryPath = join(appRoot, ".eve", "compile");

packages/eve/src/internal/nitro/dev-runtime-artifacts.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { existsSync, readFileSync, type Dirent } from "node:fs";
44
import { dirname, join, relative, resolve, sep } from "node:path";
55

66
import type { CompileAgentResult } from "#compiler/compile-agent.js";
7+
import { GENERATED_COMPILED_ARTIFACT_FILE_NAMES } from "#internal/application/compiled-artifacts.js";
78
import { copyDevelopmentSourceSnapshot } from "#internal/nitro/dev-runtime-source-snapshot-copy.js";
89
import { createDevelopmentSourceSnapshotPlan } from "#internal/nitro/dev-runtime-source-snapshot.js";
910

@@ -78,13 +79,19 @@ export async function stageDevelopmentRuntimeArtifactsSnapshot(
7879
appRoot: compileResult.project.appRoot,
7980
snapshotRoot,
8081
});
82+
const excludedCompileArtifactPaths = new Set(
83+
GENERATED_COMPILED_ARTIFACT_FILE_NAMES.map((fileName) =>
84+
join(compileResult.paths.compileDirectoryPath, fileName),
85+
),
86+
);
8187

8288
try {
8389
await copyDevelopmentSourceSnapshot(sourceSnapshotPlan);
8490
await cp(
8591
compileResult.paths.compileDirectoryPath,
8692
join(sourceSnapshotPlan.runtimeAppRoot, ".eve", "compile"),
8793
{
94+
filter: (source) => !excludedCompileArtifactPaths.has(source),
8895
recursive: true,
8996
},
9097
);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { existsSync } from "node:fs";
2+
import { readFile, writeFile } from "node:fs/promises";
3+
import { join } from "node:path";
4+
5+
import { describe, expect, it } from "vitest";
6+
7+
import { normalizeEsmImportSpecifier } from "#internal/application/import-specifier.js";
8+
import {
9+
pruneDevelopmentRuntimeArtifactsSnapshots,
10+
resolveDevelopmentRuntimeArtifactsPointerPath,
11+
} from "#internal/nitro/dev-runtime-artifacts.js";
12+
import { useTemporaryAppRoots } from "#internal/testing/use-temporary-app-roots.js";
13+
import { prepareApplicationHost } from "#internal/nitro/host/prepare-application-host.js";
14+
15+
const createAppRoot = useTemporaryAppRoots();
16+
17+
interface DevelopmentRuntimePointer {
18+
readonly runtimeAppRoot: string;
19+
readonly snapshotRoot: string;
20+
}
21+
22+
async function readDevelopmentRuntimePointer(appRoot: string): Promise<DevelopmentRuntimePointer> {
23+
return JSON.parse(
24+
await readFile(resolveDevelopmentRuntimeArtifactsPointerPath(appRoot), "utf8"),
25+
) as DevelopmentRuntimePointer;
26+
}
27+
28+
describe("prepareApplicationHost", () => {
29+
it("keeps Nitro host inputs stable when their runtime snapshot is pruned", async () => {
30+
const { agentRoot, appRoot } = await createAppRoot("eve-stable-dev-host-artifacts-", {
31+
files: {
32+
"agent/instructions.md": "Use the configured model.",
33+
},
34+
packageName: "stable-dev-host-artifacts",
35+
});
36+
const agentModulePath = join(agentRoot, "agent.mjs");
37+
await writeFile(agentModulePath, 'export default { model: "openai/gpt-5.4" };\n');
38+
39+
const firstHost = await prepareApplicationHost(appRoot, { dev: true });
40+
const firstPointer = await readDevelopmentRuntimePointer(appRoot);
41+
const stableHostDirectory = join(appRoot, ".eve", "host");
42+
const stableBootstrapPath = join(stableHostDirectory, "compiled-artifacts-bootstrap.mjs");
43+
const snapshotBootstrapPath = join(
44+
firstPointer.runtimeAppRoot,
45+
".eve",
46+
"compile",
47+
"compiled-artifacts-bootstrap.mjs",
48+
);
49+
50+
expect(firstHost.compiledArtifacts.bootstrapPath).toBe(stableBootstrapPath);
51+
expect(firstHost.compiledArtifacts.workflowWorldPluginPath).toBe(
52+
join(stableHostDirectory, "compiled-artifacts-workflow-world.mjs"),
53+
);
54+
expect(firstHost.compiledArtifacts.bootstrapPath).not.toContain("/.eve/dev-runtime/snapshots/");
55+
expect(await readFile(stableBootstrapPath, "utf8")).toContain(
56+
normalizeEsmImportSpecifier(agentModulePath),
57+
);
58+
expect(existsSync(snapshotBootstrapPath)).toBe(false);
59+
60+
await writeFile(
61+
agentModulePath,
62+
'export default { model: "openai/gpt-5.4" };\n// revision two\n',
63+
);
64+
const nextHost = await prepareApplicationHost(appRoot, { dev: true });
65+
const nextPointer = await readDevelopmentRuntimePointer(appRoot);
66+
67+
expect(nextHost.compiledArtifacts.bootstrapPath).toBe(stableBootstrapPath);
68+
expect(nextPointer.snapshotRoot).not.toBe(firstPointer.snapshotRoot);
69+
70+
await pruneDevelopmentRuntimeArtifactsSnapshots({
71+
appRoot,
72+
now: Date.now() + 1_000,
73+
recentWindowMs: 0,
74+
retainCount: 0,
75+
});
76+
77+
expect(existsSync(firstPointer.snapshotRoot)).toBe(false);
78+
expect(existsSync(stableBootstrapPath)).toBe(true);
79+
});
80+
});

packages/eve/src/internal/nitro/host/prepare-application-host.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { readFile } from "node:fs/promises";
2-
31
import { compileAgent } from "#compiler/compile-agent.js";
4-
import type { CompiledAgentManifest } from "#compiler/manifest.js";
52
import { createScheduleRegistrations } from "#runtime/schedules/register.js";
63
import { loadResolvedCompiledSchedules } from "#runtime/schedules/resolve-schedule.js";
74
import { writeCompiledArtifactsFiles } from "#internal/application/compiled-artifacts.js";
8-
import { resolveWorkflowBuildDirectory } from "#internal/application/paths.js";
5+
import {
6+
resolveApplicationHostArtifactsDirectory,
7+
resolveWorkflowBuildDirectory,
8+
} from "#internal/application/paths.js";
99
import { createAuthoredSourceRuntimeCompiledArtifactsSource } from "#internal/application/runtime-compiled-artifacts-source.js";
1010
import {
1111
activateDevelopmentRuntimeArtifactsSnapshot,
1212
stageDevelopmentRuntimeArtifactsSnapshot,
1313
} from "#internal/nitro/dev-runtime-artifacts.js";
14-
import { resolveRuntimeCompilerArtifactPaths } from "#runtime/loaders/artifact-paths.js";
1514
import type { PreparedApplicationHost } from "#internal/nitro/host/types.js";
1615

1716
/**
@@ -38,23 +37,9 @@ export async function prepareApplicationHost(
3837
options.dev === true
3938
? await stageDevelopmentRuntimeArtifactsSnapshot(compileResult)
4039
: undefined;
41-
const runtimeArtifactsRoot =
42-
runtimeArtifactsSnapshot === undefined
43-
? compileResult.project.appRoot
44-
: runtimeArtifactsSnapshot.runtimeAppRoot;
45-
const runtimeArtifactPaths = resolveRuntimeCompilerArtifactPaths(runtimeArtifactsRoot);
46-
const runtimeCompileResult =
47-
options.dev === true
48-
? {
49-
...compileResult,
50-
manifest: JSON.parse(
51-
await readFile(runtimeArtifactPaths.compiledManifestPath, "utf8"),
52-
) as CompiledAgentManifest,
53-
}
54-
: compileResult;
5540
const compiledArtifacts = await writeCompiledArtifactsFiles({
56-
compileResult: runtimeCompileResult,
57-
outDir: runtimeArtifactPaths.compileDirectoryPath,
41+
compileResult,
42+
outDir: resolveApplicationHostArtifactsDirectory(compileResult.project.appRoot),
5843
});
5944
if (runtimeArtifactsSnapshot !== undefined) {
6045
await activateDevelopmentRuntimeArtifactsSnapshot({

packages/eve/test/scenarios/dev-server.scenario.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { spawn, type ChildProcessByStdio } from "node:child_process";
2+
import { existsSync } from "node:fs";
3+
import { writeFile } from "node:fs/promises";
24
import { join } from "node:path";
35
import type { Readable } from "node:stream";
46

57
import { describe, expect, it } from "vitest";
68

79
import { EVE_HEALTH_ROUTE_PATH } from "../../src/protocol/routes.js";
10+
import {
11+
pruneDevelopmentRuntimeArtifactsSnapshots,
12+
readDevelopmentRuntimeArtifactsSnapshotRoot,
13+
resolveDevelopmentRuntimeArtifactsPointerPath,
14+
} from "../../src/internal/nitro/dev-runtime-artifacts.js";
15+
import { STRUCTURAL_RELOAD_LOG_LINE } from "../../src/internal/nitro/host/dev-watcher-log.js";
816
import { WEATHER_AGENT_DESCRIPTOR } from "../../src/internal/testing/scenario-apps/weather-agent.js";
917
import {
1018
type ScenarioAppDescriptor,
@@ -59,6 +67,7 @@ function hasUnsupportedWindowsEsmImport(text: string): boolean {
5967
function hasKnownDevBundlingFailure(text: string): boolean {
6068
return (
6169
hasUnsupportedWindowsEsmImport(text) ||
70+
text.includes("UNRESOLVED_IMPORT") ||
6271
(text.includes("ERR_MODULE_NOT_FOUND") && text.includes("authored-module-map-loader"))
6372
);
6473
}
@@ -75,6 +84,21 @@ async function wait(ms: number): Promise<void> {
7584
});
7685
}
7786

87+
async function waitForCondition(
88+
condition: () => boolean,
89+
failureMessage: string,
90+
timeoutMs: number = 60_000,
91+
): Promise<void> {
92+
const deadline = Date.now() + timeoutMs;
93+
94+
while (!condition()) {
95+
if (Date.now() >= deadline) {
96+
throw new Error(failureMessage);
97+
}
98+
await wait(100);
99+
}
100+
}
101+
78102
async function waitForServerUrl(input: {
79103
readonly child: ChildProcessByStdio<null, Readable, Readable>;
80104
readonly getOutput: () => {
@@ -234,7 +258,7 @@ async function startEveDev(appRoot: string): Promise<RunningEveDev> {
234258

235259
describe("eve dev server", () => {
236260
it(
237-
"boots the packaged development server and completes a streamed turn",
261+
"rebuilds after pruning its startup runtime snapshot and completes a streamed turn",
238262
async () => {
239263
const app = await scenarioApp(DEV_SERVER_AGENT_DESCRIPTOR);
240264
const server = await startEveDev(app.appRoot);
@@ -257,6 +281,45 @@ describe("eve dev server", () => {
257281
status: "ready",
258282
});
259283

284+
const pointerPath = resolveDevelopmentRuntimeArtifactsPointerPath(app.appRoot);
285+
const startupRuntimeRoot = readDevelopmentRuntimeArtifactsSnapshotRoot(pointerPath);
286+
if (startupRuntimeRoot === undefined) {
287+
throw new Error("Expected eve dev to publish an initial runtime snapshot.");
288+
}
289+
290+
await writeFile(
291+
join(app.appRoot, "agent", "instructions.md"),
292+
"Use the weather tool and answer with the current conditions.\n",
293+
);
294+
await waitForCondition(() => {
295+
const currentRuntimeRoot = readDevelopmentRuntimeArtifactsSnapshotRoot(pointerPath);
296+
return currentRuntimeRoot !== undefined && currentRuntimeRoot !== startupRuntimeRoot;
297+
}, `Timed out waiting for authored HMR.\n\nstdout:\n${server.stdout()}\n\nstderr:\n${server.stderr()}`);
298+
299+
const authoredRuntimeRoot = readDevelopmentRuntimeArtifactsSnapshotRoot(pointerPath);
300+
if (authoredRuntimeRoot === undefined) {
301+
throw new Error("Expected authored HMR to publish a runtime snapshot.");
302+
}
303+
304+
await pruneDevelopmentRuntimeArtifactsSnapshots({
305+
appRoot: app.appRoot,
306+
now: Date.now() + 1_000,
307+
recentWindowMs: 0,
308+
retainCount: 0,
309+
});
310+
expect(existsSync(startupRuntimeRoot)).toBe(false);
311+
312+
await writeFile(join(app.appRoot, ".env.local"), "EVE_SCENARIO_RELOAD=1\n");
313+
await waitForCondition(
314+
() => server.stdout().includes(STRUCTURAL_RELOAD_LOG_LINE),
315+
`Timed out waiting for a structural Nitro reload.\n\nstdout:\n${server.stdout()}\n\nstderr:\n${server.stderr()}`,
316+
);
317+
await waitForCondition(() => {
318+
const currentRuntimeRoot = readDevelopmentRuntimeArtifactsSnapshotRoot(pointerPath);
319+
return currentRuntimeRoot !== undefined && currentRuntimeRoot !== authoredRuntimeRoot;
320+
}, `Timed out waiting for the structural reload snapshot.\n\nstdout:\n${server.stdout()}\n\nstderr:\n${server.stderr()}`);
321+
await wait(2_000);
322+
260323
let messageResult: Awaited<ReturnType<typeof sendDevelopmentMessage>>;
261324
try {
262325
messageResult = await sendDevelopmentMessage({

0 commit comments

Comments
 (0)