fix(server): handle codex spawn errors to prevent daemon crash

Spawning a missing/broken codex binary emits an async "error" event on
the child process. Without a listener, Node.js crashes the daemon with
no log entry. Add child.on("error") in CodexAppServerClient and global
uncaughtException/unhandledRejection handlers that log via pino before
exiting.
This commit is contained in:
Mohamed Boudra
2026-04-01 13:37:51 +07:00
parent c635eabff3
commit 9c4dee5364
3 changed files with 77 additions and 0 deletions
@@ -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);
}
});
});
@@ -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
+10
View File
@@ -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) => {