authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-05 00:13:16-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 00:13:16-07:00
logcf3e714b0f58e29c8fa8660db50d2187e3263b65
tree25b080b898a0b4d84624744098c1e4903d1d40d8
parent250e052f44dd313b658abebc707242a7b25be340
signature Signed by PGP key B5690EEEBB952194

fix(server): Stop ends a Codex command after its thread was settled (#15546)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

2 files changed, 18 insertions(+), 3 deletions(-)

apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+11-3
...@@ -3917,7 +3917,9 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -3917,7 +3917,9 @@ describe("CodexAdapterV2 post-settle continuation", () => {
3917 ),3917 ),
3918 );3918 );
39193919
3920 const backgroundStopCases = [true, false, "still_running"] as const;3920 // "thread_unloaded": the thread was settled, so T3 unsubscribed and Codex
3921 // unloaded it (killing its terminals) before Stop arrived.
3922 const backgroundStopCases = [true, false, "still_running", "thread_unloaded"] as const;
3921 const makeBackgroundStopTranscript = (terminated: (typeof backgroundStopCases)[number]) => {3923 const makeBackgroundStopTranscript = (terminated: (typeof backgroundStopCases)[number]) => {
3922 const stillRunning = terminated === "still_running";3924 const stillRunning = terminated === "still_running";
3923 return makeCodexReplayTranscript({3925 return makeCodexReplayTranscript({
...@@ -3936,9 +3938,15 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -3936,9 +3938,15 @@ describe("CodexAdapterV2 post-settle continuation", () => {
3936 {3938 {
3937 type: "emit_inbound",3939 type: "emit_inbound",
3938 label: "terminate-background-command",3940 label: "terminate-background-command",
3939 frame: { id: 4, result: { terminated: terminated === true } },3941 frame:
3942 terminated === "thread_unloaded"
3943 ? {
3944 id: 4,
3945 error: { code: -32600, message: `thread not found: ${BG_NATIVE_THREAD}` },
3946 }
3947 : { id: 4, result: { terminated: terminated === true } },
3940 },3948 },
3941 ...(terminated !== true3949 ...(terminated === false || stillRunning
3942 ? [3950 ? [
3943 {3951 {
3944 type: "expect_outbound" as const,3952 type: "expect_outbound" as const,
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+7
...@@ -2054,6 +2054,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2054,6 +2054,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2054 Effect.catchTags({2054 Effect.catchTags({
2055 CodexAppServerProcessExitedError: () => Effect.succeed({ terminated: true }),2055 CodexAppServerProcessExitedError: () => Effect.succeed({ terminated: true }),
2056 CodexAppServerInputStreamEndedError: () => Effect.succeed({ terminated: true }),2056 CodexAppServerInputStreamEndedError: () => Effect.succeed({ terminated: true }),
2057 // The thread is unloaded, as Codex does a minute after a
2058 // settle or archive unsubscribes it. Unloading kills the
2059 // thread's terminals, so nothing is left to stop.
2060 CodexAppServerRequestError: (error) =>
2061 error.code === -32600 && error.errorMessage.startsWith("thread not found:")
2062 ? Effect.succeed({ terminated: true })
2063 : Effect.fail(error),
2057 }),2064 }),
2058 );2065 );
2059 const result = yield* decodeCodexBackgroundTerminalTerminateResponse(response);2066 const result = yield* decodeCodexBackgroundTerminalTerminateResponse(response);