diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 19caed290d0..298c18c6ba9 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -119,6 +119,56 @@ describe("runCodexAppServerAttempt", () => { ); }); + it("does not leak unhandled rejections when shutdown closes before interrupt", async () => { + const unhandledRejections: unknown[] = []; + const onUnhandledRejection = (reason: unknown) => { + unhandledRejections.push(reason); + }; + process.on("unhandledRejection", onUnhandledRejection); + try { + const requests: Array<{ method: string; params: unknown }> = []; + const request = vi.fn(async (method: string, params?: unknown) => { + requests.push({ method, params }); + if (method === "thread/start") { + return { thread: { id: "thread-1" }, model: "gpt-5.4-codex", modelProvider: "openai" }; + } + if (method === "turn/start") { + return { turn: { id: "turn-1", status: "inProgress" } }; + } + if (method === "turn/interrupt") { + throw new Error("codex app-server client is closed"); + } + return {}; + }); + __testing.setCodexAppServerClientFactoryForTests( + async () => + ({ + request, + addNotificationHandler: () => () => undefined, + addRequestHandler: () => () => undefined, + }) as never, + ); + const abortController = new AbortController(); + const params = createParams( + path.join(tempDir, "session.jsonl"), + path.join(tempDir, "workspace"), + ); + params.abortSignal = abortController.signal; + + const run = runCodexAppServerAttempt(params); + await vi.waitFor(() => + expect(requests.some((entry) => entry.method === "turn/start")).toBe(true), + ); + abortController.abort("shutdown"); + + await expect(run).resolves.toMatchObject({ aborted: true }); + await new Promise((resolve) => setImmediate(resolve)); + expect(unhandledRejections).toEqual([]); + } finally { + process.off("unhandledRejection", onUnhandledRejection); + } + }); + it("forwards image attachments to the app-server turn input", async () => { const requests: Array<{ method: string; params: unknown }> = []; let notify: (notification: CodexServerNotification) => Promise = async () => undefined; diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 10f7c08f3da..4a86686f4ad 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -227,10 +227,14 @@ export async function runCodexAppServerAttempt( ); const abortListener = () => { - void client.request("turn/interrupt", { - threadId: thread.threadId, - turnId: activeTurnId, - }); + void client + .request("turn/interrupt", { + threadId: thread.threadId, + turnId: activeTurnId, + }) + .catch((error: unknown) => { + embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error }); + }); resolveCompletion?.(); }; runAbortController.signal.addEventListener("abort", abortListener, { once: true });