Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dev-schedule-dispatch-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Fix the dev-only schedule dispatch route to load compiled artifacts from the active development generation instead of the authored app root, which returned a 500 for every dispatch.
5 changes: 5 additions & 0 deletions .changeset/drained-dev-worker-replacement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Rework `eve dev` structural reloads to never interrupt admitted work: an isolated candidate must compile, bundle, and start before it is promoted atomically, the retired worker keeps serving the responses and sockets it already admitted until they settle, and a failed candidate or crashed worker leaves the server available with shutdown bounded even while streams are open.
5 changes: 5 additions & 0 deletions .changeset/tidy-locks-retry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Retry transient Windows filesystem contention while atomically releasing build publication locks.
1 change: 1 addition & 0 deletions packages/eve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@
"chokidar": "5.0.0",
"commander": "14.0.3",
"emulate": "0.6.0",
"env-runner": "0.1.12",
"eventsource-parser": "3.0.8",
"experimental-ai-sdk-code-mode": "1.0.16",
"gray-matter": "4.0.3",
Expand Down
38 changes: 38 additions & 0 deletions packages/eve/scripts/vendor-compiled/declarations/env-runner.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { IncomingMessage } from "node:http";
import type { Socket } from "node:net";

export interface WorkerHooks {
onClose?(runner: BaseEnvRunner, cause?: unknown): void;
onReady?(runner: BaseEnvRunner): void;
}

export interface BaseEnvRunnerOptions {
readonly data?: Record<string, unknown>;
readonly hooks?: WorkerHooks;
readonly name: string;
readonly workerEntry: string;
}

export class BaseEnvRunner {
readonly closed: boolean;
readonly ready: boolean;
protected readonly _data: Record<string, unknown> | undefined;
protected readonly _name: string;
protected readonly _workerEntry: string;

constructor(options: BaseEnvRunnerOptions);

close(cause?: unknown): Promise<void>;
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
sendMessage(message: unknown): void;
upgrade(context: {
node: { head: Uint8Array; req: IncomingMessage; socket: Socket };
}): Promise<void>;
waitForReady(timeout?: number): Promise<void>;

protected _closeRuntime(): Promise<void>;
protected _handleMessage(message: unknown): void;
protected _hasRuntime(): boolean;
protected _initWithVirtualData(initialize: () => void): void;
protected _runtimeType(): string;
}
18 changes: 18 additions & 0 deletions packages/eve/scripts/vendor-compiled/env-runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { loadDeclaration } from "./_shared.mjs";

export default {
packageName: "env-runner",
compiledPath: "env-runner",
entries: [
{
entry: "dist/_chunks/common-base-runner.mjs",
outputPath: "index",
declaration: await loadDeclaration("env-runner.d.ts"),
},
{
entry: "dist/runners/node-worker/worker.mjs",
outputPath: "node-worker",
declaration: "export {};\n",
},
],
};
2 changes: 2 additions & 0 deletions packages/eve/scripts/vendor-compiled/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import chokidar from "./chokidar.mjs";
import commander from "./commander.mjs";
import experimentalAiSdkCodeMode from "./experimental-ai-sdk-code-mode.mjs";
import eventsourceParserStream from "./eventsource-parser-stream.mjs";
import envRunner from "./env-runner.mjs";
import grayMatter from "./gray-matter.mjs";
import jose from "./jose.mjs";
import jsoncParser from "./jsonc-parser.mjs";
Expand All @@ -52,6 +53,7 @@ export const MODULES = [
commander,
experimentalAiSdkCodeMode,
eventsourceParserStream,
envRunner,
google,
grayMatter,
jose,
Expand Down
107 changes: 88 additions & 19 deletions packages/eve/src/cli/dev/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ function isMissingEnvironmentFileError(error: unknown): error is NodeJS.ErrnoExc

interface DevelopmentEnvironmentLoader {
reload(): void;
stageReload(): DevelopmentEnvironmentReload;
}

export interface DevelopmentEnvironmentReload {
commit(): void;
rollback(): void;
}

const developmentEnvironmentLoaders = new Map<string, DevelopmentEnvironmentLoader>();
Expand All @@ -48,6 +54,23 @@ export function loadDevelopmentEnvironmentFiles(appRoot: string): void {
getDevelopmentEnvironmentLoader(appRoot).reload();
}

export function stageDevelopmentEnvironmentFiles(appRoot: string): DevelopmentEnvironmentReload {
return getDevelopmentEnvironmentLoader(appRoot).stageReload();
}

export function readDevelopmentEnvironmentHostValues(
appRoot: string,
): Readonly<Record<string, string | null>> {
const values: Record<string, string | null> = {};
const fileValues = readDevelopmentEnvironmentValues(resolve(appRoot));

for (const key of [...fileValues.keys()].sort((left, right) => left.localeCompare(right))) {
values[key] = process.env[key] ?? null;
}

return values;
}

function getDevelopmentEnvironmentLoader(appRoot: string): DevelopmentEnvironmentLoader {
const resolvedAppRoot = resolve(appRoot);
const existingLoader = developmentEnvironmentLoaders.get(resolvedAppRoot);
Expand All @@ -65,32 +88,78 @@ function createDevelopmentEnvironmentLoader(appRoot: string): DevelopmentEnviron
const protectedKeys = new Set(Object.keys(process.env));
const managedValues = new Map<string, string>();

const stageReload = (): DevelopmentEnvironmentReload => {
const previousManagedValues = new Map(managedValues);
const nextValues = readDevelopmentEnvironmentValues(appRoot);
const affectedKeys = new Set([...managedValues.keys(), ...nextValues.keys()]);
const previousEnvironment = new Map(
[...affectedKeys].map((key) => [key, process.env[key]] as const),
);
let settled = false;

applyDevelopmentEnvironmentValues({
managedValues,
nextValues,
protectedKeys,
});

return {
commit() {
settled = true;
},
rollback() {
if (settled) {
return;
}
settled = true;
managedValues.clear();
for (const [key, value] of previousManagedValues) {
managedValues.set(key, value);
}
for (const [key, value] of previousEnvironment) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
},
};
};

return {
reload() {
const nextValues = readDevelopmentEnvironmentValues(appRoot);
stageReload().commit();
},
stageReload,
};
}

for (const [key, previousValue] of managedValues) {
if (nextValues.has(key) || protectedKeys.has(key)) {
continue;
}
function applyDevelopmentEnvironmentValues(input: {
readonly managedValues: Map<string, string>;
readonly nextValues: ReadonlyMap<string, string>;
readonly protectedKeys: ReadonlySet<string>;
}): void {
for (const [key, previousValue] of input.managedValues) {
if (input.nextValues.has(key) || input.protectedKeys.has(key)) {
continue;
}

if (process.env[key] === previousValue) {
delete process.env[key];
}
if (process.env[key] === previousValue) {
delete process.env[key];
}

managedValues.delete(key);
}
input.managedValues.delete(key);
}

for (const [key, value] of nextValues) {
if (protectedKeys.has(key)) {
continue;
}
for (const [key, value] of input.nextValues) {
if (input.protectedKeys.has(key)) {
continue;
}

process.env[key] = value;
managedValues.set(key, value);
}
},
};
process.env[key] = value;
input.managedValues.set(key, value);
}
}

function readDevelopmentEnvironmentValues(appRoot: string): Map<string, string> {
Expand Down
10 changes: 4 additions & 6 deletions packages/eve/src/compiler/compile-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,11 @@ export async function compileAgent(input: CompileAgentInput = {}): Promise<Compi
}

/**
* Compiles an agent for a production build. Artifacts are written to the
* invocation-owned `writeRoot` (a throwaway build workspace), while the
* metadata and module map record paths under the stable `publishedRoot`
* where publication later installs them — so the recorded paths stay
* relocatable and identical across builds of the same source.
* Compiles an agent into a caller-owned workspace. Artifacts are written to
* `writeRoot`, while metadata and module maps record paths under the stable
* `publishedRoot` where the caller will expose them.
*/
export async function compileAgentInBuildWorkspace(input: {
export async function compileAgentInWorkspace(input: {
readonly artifactLocations: CompilerArtifactLocations;
readonly startPath: string;
}): Promise<CompileAgentResult> {
Expand Down
4 changes: 2 additions & 2 deletions packages/eve/src/execution/sandbox/prewarm.scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { join } from "node:path";

import { afterEach, describe, expect, it, vi } from "vitest";

import { compileAgent, compileAgentInBuildWorkspace } from "#compiler/compile-agent.js";
import { compileAgent, compileAgentInWorkspace } from "#compiler/compile-agent.js";
import { resolvePackageSourceFilePath } from "#internal/application/package.js";
import { createDevelopmentNitroArtifactsConfig } from "#internal/nitro/host/artifacts-config.js";
import { publishDevelopmentGeneration } from "#internal/nitro/development-generation.js";
Expand All @@ -30,7 +30,7 @@ describe("prewarmAppSandboxes", () => {

const appRoot = await createScenarioAppRoot();
const compilerAppRoot = join(appRoot, ".eve", "builds", "isolated", "compiler");
await compileAgentInBuildWorkspace({
await compileAgentInWorkspace({
artifactLocations: {
publishedRoot: join(compilerAppRoot, ".eve"),
writeRoot: join(compilerAppRoot, ".eve"),
Expand Down
13 changes: 7 additions & 6 deletions packages/eve/src/internal/application/output-publication-lock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { watch } from "node:fs";
import { mkdir, readdir, rename, rm, stat, utimes } from "node:fs/promises";
import { mkdir, readdir, rm, stat, utimes } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";

import {
Expand All @@ -17,6 +17,7 @@ import {
} from "#internal/application/output-publication-journal.js";
import { isErrnoCode } from "#shared/guards.js";
import { pathExists } from "#shared/path-exists.js";
import { renameWithTransientBusyRetry } from "#shared/rename-with-retry.js";

const PUBLICATION_LOCK_TIMEOUT_MS = 60_000;
const INCOMPLETE_LOCK_STALE_MS = 5_000;
Expand Down Expand Up @@ -89,7 +90,7 @@ export async function acquireOutputPublicationLock(
}
const releasedPath = `${lockPath}.released-${journal.token}`;
try {
await rename(lockPath, releasedPath);
await renameWithTransientBusyRetry(lockPath, releasedPath);
} catch (error) {
if (isErrnoCode(error, "ENOENT")) {
return;
Expand Down Expand Up @@ -141,7 +142,7 @@ async function recoverStalePublication(
if (await pathExists(lockPath)) {
const recoveringJournalPath = join(recoveryPath, `owner-${recoveryJournal.token}`);
try {
await rename(lockPath, recoveringJournalPath);
await renameWithTransientBusyRetry(lockPath, recoveringJournalPath);
preserveRecovery = true;
} catch (error) {
if (!isErrnoCode(error, "ENOENT")) {
Expand Down Expand Up @@ -197,7 +198,7 @@ async function acquireRecoveryLease(
}
const releasedPath = `${recoveryPath}.released-${token}`;
try {
await rename(recoveryPath, releasedPath);
await renameWithTransientBusyRetry(recoveryPath, releasedPath);
} catch (error) {
if (isErrnoCode(error, "ENOENT")) {
return;
Expand All @@ -213,7 +214,7 @@ async function acquireRecoveryLease(
}
const releasedPath = `${leasePath}.released-${token}`;
try {
await rename(leasePath, releasedPath);
await renameWithTransientBusyRetry(leasePath, releasedPath);
} catch (error) {
if (isErrnoCode(error, "ENOENT")) {
return;
Expand Down Expand Up @@ -248,7 +249,7 @@ async function acquireRecoveryLease(

const staleLeasePath = `${leasePath}.stale-${token}`;
try {
await rename(leasePath, staleLeasePath);
await renameWithTransientBusyRetry(leasePath, staleLeasePath);
} catch (error) {
if (isErrnoCode(error, "ENOENT")) {
continue;
Expand Down
58 changes: 58 additions & 0 deletions packages/eve/src/internal/compiled-manifest-fingerprint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { join } from "node:path";
import { describe, expect, it } from "vitest";

import { createCompiledAgentManifest } from "#compiler/manifest.js";
import { serializeCompiledManifestForFingerprint } from "#internal/compiled-manifest-fingerprint.js";

function manifestWithRoot(runtimeAppRoot: string, agentRoot = join(runtimeAppRoot, "agent")) {
return createCompiledAgentManifest({
agentRoot,
appRoot: runtimeAppRoot,
config: {
model: { id: "openai/gpt-5-mini", routing: { kind: "gateway", target: "openai" } },
name: "fingerprint-test",
},
});
}

describe("serializeCompiledManifestForFingerprint", () => {
it("serializes identical content under different snapshot roots identically", () => {
const firstRoot = "/tmp/snapshots/generation-a/source/app";
const secondRoot = "/tmp/snapshots/generation-b/source/app";
const first = serializeCompiledManifestForFingerprint({
manifest: manifestWithRoot(firstRoot),
runtimeAppRoot: firstRoot,
});
const second = serializeCompiledManifestForFingerprint({
manifest: manifestWithRoot(secondRoot),
runtimeAppRoot: secondRoot,
});

expect(first).toBe(second);
expect(first).toContain("$runtime/agent");
expect(first).not.toContain("generation-a");
});

it("keeps absolute paths outside the runtime root verbatim", () => {
const runtimeAppRoot = "/tmp/snapshots/generation-a/source/app";
const serialized = serializeCompiledManifestForFingerprint({
manifest: manifestWithRoot(runtimeAppRoot, "/somewhere/else/agent"),
runtimeAppRoot,
});

expect(serialized).toContain("/somewhere/else/agent");
});

it("canonicalizes object key order", () => {
const left = serializeCompiledManifestForFingerprint({
manifest: { agentRoot: "/app/agent", config: { model: "m", name: "n" } } as never,
runtimeAppRoot: "/app",
});
const right = serializeCompiledManifestForFingerprint({
manifest: { config: { name: "n", model: "m" }, agentRoot: "/app/agent" } as never,
runtimeAppRoot: "/app",
});

expect(left).toBe(right);
});
});
Loading
Loading