From f140ec790d88f3564af8ae7549a76b1f6940d2cc Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 4 Oct 2026 00:30:05 -0700 Subject: [PATCH] fix(server): Stop ends a Codex command after the thread was settled Settling a Codex thread unsubscribes it, and Codex unloads it a minute later, killing its terminals. Stop then asked Codex to terminate the terminal, got "thread not found", and failed before the settle step, so the banner kept showing the command as running. Treat "thread not found" from terminate as already terminated, like an exited app-server. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Adapters/CodexAdapterV2.test.ts | 14 +++++++++++--- .../orchestration-v2/Adapters/CodexAdapterV2.ts | 7 +++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 85748fa056e7..78015dac1d89 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -3869,7 +3869,9 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); - const backgroundStopCases = [true, false, "still_running"] as const; + // "thread_unloaded": the thread was settled, so T3 unsubscribed and Codex + // unloaded it (killing its terminals) before Stop arrived. + const backgroundStopCases = [true, false, "still_running", "thread_unloaded"] as const; const makeBackgroundStopTranscript = (terminated: (typeof backgroundStopCases)[number]) => { const stillRunning = terminated === "still_running"; return makeCodexReplayTranscript({ @@ -3888,9 +3890,15 @@ describe("CodexAdapterV2 post-settle continuation", () => { { type: "emit_inbound", label: "terminate-background-command", - frame: { id: 4, result: { terminated: terminated === true } }, + frame: + terminated === "thread_unloaded" + ? { + id: 4, + error: { code: -32600, message: `thread not found: ${BG_NATIVE_THREAD}` }, + } + : { id: 4, result: { terminated: terminated === true } }, }, - ...(terminated !== true + ...(terminated === false || stillRunning ? [ { type: "expect_outbound" as const, diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 8da6ffe4357d..f00dba6d4a62 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -2053,6 +2053,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi Effect.catchTags({ CodexAppServerProcessExitedError: () => Effect.succeed({ terminated: true }), CodexAppServerInputStreamEndedError: () => Effect.succeed({ terminated: true }), + // The thread is unloaded, as Codex does a minute after a + // settle or archive unsubscribes it. Unloading kills the + // thread's terminals, so nothing is left to stop. + CodexAppServerRequestError: (error) => + error.code === -32600 && error.errorMessage.startsWith("thread not found:") + ? Effect.succeed({ terminated: true }) + : Effect.fail(error), }), ); const result = yield* decodeCodexBackgroundTerminalTerminateResponse(response);