From 4cbf616d3049e35cfb6274f287c60b503361b5f6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 17 May 2026 00:31:37 +0100 Subject: [PATCH] fix(codex): premark terminal app-server turns --- .../codex/src/app-server/run-attempt.test.ts | 40 +++++++++++++++++++ .../codex/src/app-server/run-attempt.ts | 34 +++++++++++----- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 32621734d7c..59b71e4b492 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -4849,6 +4849,46 @@ describe("runCodexAppServerAttempt", () => { expect(result.timedOut).toBe(false); }); + it("does not fail when a buffered terminal notification is followed by client close", async () => { + let harness: ReturnType; + let resolveBufferedTerminal!: () => void; + const bufferedTerminal = new Promise((resolve) => { + resolveBufferedTerminal = resolve; + }); + harness = createAppServerHarness(async (method) => { + if (method === "thread/start") { + return threadStartResult(); + } + if (method === "turn/start") { + await harness.notify({ + method: "item/started", + params: { + threadId: "thread-1", + turnId: "turn-1", + item: { id: "tool-1", type: "commandExecution" }, + }, + }); + await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); + resolveBufferedTerminal(); + return turnStartResult("turn-1", "inProgress"); + } + return {}; + }); + + const run = runCodexAppServerAttempt( + createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")), + { turnTerminalIdleTimeoutMs: 60_000 }, + ); + await bufferedTerminal; + await new Promise((resolve) => setImmediate(resolve)); + harness.close(); + + const result = await run; + expect(result.promptError ?? undefined).toBeUndefined(); + expect(result.aborted).toBe(false); + expect(result.timedOut).toBe(false); + }); + it("does not time out when turn progress arrives before turn/start returns", async () => { let harness: ReturnType; harness = createAppServerHarness(async (method) => { diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index b53fc10b413..d2110cb16e3 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -1464,6 +1464,19 @@ export async function runCodexAppServerAttempt( }); }; + const isTerminalTurnNotificationForTurn = ( + notification: CodexServerNotification, + notificationTurnId: string, + ): boolean => { + if (!isTurnNotification(notification.params, thread.threadId, notificationTurnId)) { + return false; + } + return ( + notification.method === "turn/completed" || + isCodexTurnAbortMarkerNotification(notification, { currentPromptText: promptBuild.prompt }) + ); + }; + const handleNotification = async (notification: CodexServerNotification) => { userInputBridge?.handleNotification(notification); if (!projector || !turnId) { @@ -1562,7 +1575,7 @@ export async function runCodexAppServerAttempt( const isTurnAbortMarker = isCurrentTurnNotification && isCodexTurnAbortMarkerNotification(notification, { currentPromptText: promptBuild.prompt }); - const isTurnTerminal = isTurnCompletion || isTurnAbortMarker; + const isTurnTerminal = isTerminalTurnNotificationForTurn(notification, turnId); if (isTurnTerminal) { terminalTurnNotificationQueued = true; } @@ -1596,16 +1609,7 @@ export async function runCodexAppServerAttempt( pendingNotifications.push(notification); return Promise.resolve(); } - const isCurrentTurnNotification = isTurnNotification( - notification.params, - thread.threadId, - turnId, - ); - if ( - isCurrentTurnNotification && - (notification.method === "turn/completed" || - isCodexTurnAbortMarkerNotification(notification, { currentPromptText: promptBuild.prompt })) - ) { + if (isTerminalTurnNotificationForTurn(notification, turnId)) { terminalTurnNotificationQueued = true; } notificationQueue = notificationQueue.then( @@ -2034,6 +2038,14 @@ export async function runCodexAppServerAttempt( nativePostToolUseRelayEnabled: nativeHookRelay?.allowedEvents.includes("post_tool_use") === true, }); + if ( + isTerminalTurnStatus(turn.turn.status) || + pendingNotifications.some((notification) => + isTerminalTurnNotificationForTurn(notification, activeTurnId), + ) + ) { + terminalTurnNotificationQueued = true; + } closeCleanup = ( client as { addCloseHandler?: (handler: (client: CodexAppServerClient) => void) => () => void;