diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.spawn-error.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.spawn-error.test.ts new file mode 100644 index 000000000..22f727f54 --- /dev/null +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.spawn-error.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from "vitest"; + +import { CodexAppServerAgentClient } from "./codex-app-server-agent.js"; +import { createTestLogger } from "../../../test-utils/test-logger.js"; + +describe("CodexAppServerAgentClient spawn error handling", () => { + const logger = createTestLogger(); + + test("listModels rejects gracefully when the codex binary does not exist", async () => { + const client = new CodexAppServerAgentClient(logger, { + command: { + mode: "replace", + argv: ["/nonexistent/codex-binary-that-does-not-exist"], + }, + }); + + const uncaughtErrors: unknown[] = []; + const onUncaught = (err: unknown) => { + uncaughtErrors.push(err); + }; + process.on("uncaughtException", onUncaught); + + try { + await expect(client.listModels()).rejects.toThrow(); + // Drain microtask queue to ensure no deferred uncaught errors + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(uncaughtErrors).toHaveLength(0); + } finally { + process.off("uncaughtException", onUncaught); + } + }); + + test("listPersistedAgents rejects gracefully when the codex binary does not exist", async () => { + const client = new CodexAppServerAgentClient(logger, { + command: { + mode: "replace", + argv: ["/nonexistent/codex-binary-that-does-not-exist"], + }, + }); + + const uncaughtErrors: unknown[] = []; + const onUncaught = (err: unknown) => { + uncaughtErrors.push(err); + }; + process.on("uncaughtException", onUncaught); + + try { + await expect(client.listPersistedAgents()).rejects.toThrow(); + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(uncaughtErrors).toHaveLength(0); + } finally { + process.off("uncaughtException", onUncaught); + } + }); +}); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 4b7ed2f3b..3c733c79d 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -511,6 +511,18 @@ class CodexAppServerClient { } }); + child.on("error", (err) => { + this.logger.error({ err }, "Codex app-server child process error"); + for (const pending of this.pending.values()) { + clearTimeout(pending.timer); + pending.reject(err); + } + this.pending.clear(); + this.disposed = true; + this.resolveExitPromise?.(); + this.resolveExitPromise = null; + }); + child.on("exit", (code, signal) => { const message = code === 0 && !signal diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index 70b2401d9..a71628887 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -158,6 +158,16 @@ async function main() { process.on("SIGTERM", () => beginShutdown("SIGTERM")); process.on("SIGINT", () => beginShutdown("SIGINT")); + + process.on("uncaughtException", (err) => { + logger.fatal({ err }, "Uncaught exception — daemon crashing"); + process.exit(1); + }); + + process.on("unhandledRejection", (reason) => { + logger.fatal({ err: reason }, "Unhandled promise rejection — daemon crashing"); + process.exit(1); + }); } main().catch((err) => {