From 284098d2d8718f2da7732e454ce449d3a0d96fa1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 27 May 2026 07:23:20 +0100 Subject: [PATCH] fix(codex): preserve raw reasoning source-reply guard --- .../codex/src/app-server/run-attempt.test.ts | 37 +++++++++++++++++++ .../codex/src/app-server/run-attempt.ts | 18 ++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index d8293645bc0..60a81f535bd 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -5653,6 +5653,43 @@ describe("runCodexAppServerAttempt", () => { expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false); }); + it("keeps waiting after raw reasoning completes before a visible message call", async () => { + const harness = createStartedThreadHarness(); + const params = createParams( + path.join(tempDir, "session.jsonl"), + path.join(tempDir, "workspace"), + ); + params.timeoutMs = 60_000; + params.sourceReplyDeliveryMode = "message_tool_only"; + + let settled = false; + const run = runCodexAppServerAttempt(params, { + turnCompletionIdleTimeoutMs: 15, + turnTerminalIdleTimeoutMs: 500, + }).finally(() => { + settled = true; + }); + await harness.waitForMethod("turn/start"); + await harness.notify({ + method: "rawResponseItem/completed", + params: { + threadId: "thread-1", + turnId: "turn-1", + item: { id: "raw-reasoning-1", type: "reasoning" }, + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 25)); + expect(settled).toBe(false); + + await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); + const result = await run; + expect(result.aborted).toBe(false); + expect(result.timedOut).toBe(false); + expect(result.promptError).toBeNull(); + expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false); + }); + it("keeps the normal completion idle guard after non-source reasoning completes", async () => { const harness = createStartedThreadHarness(); const params = createParams( diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 74495fed136..3f7de10959c 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -2460,6 +2460,10 @@ export async function runCodexAppServerAttempt( isReasoningItemCompletionNotification(notification) && activeTurnItemIds.size === 0 && params.sourceReplyDeliveryMode === "message_tool_only"; + const shouldArmPostRawReasoningSourceReplyWatch = + rawResponseItemCompletedWithNoActiveItems && + isRawReasoningCompletionNotification(notification) && + params.sourceReplyDeliveryMode === "message_tool_only"; const shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem = isCurrentTurnNotification && notification.method === "item/completed" && @@ -2480,7 +2484,10 @@ export async function runCodexAppServerAttempt( armTurnAssistantCompletionIdleWatch(describeNotificationActivity(notification)); } else if (postToolRawAssistantCompletionNeedsTerminalGuard) { armTurnCompletionIdleWatch({ timeoutMs: postToolRawAssistantCompletionIdleTimeoutMs }); - } else if (shouldArmPostReasoningSourceReplyWatch) { + } else if ( + shouldArmPostReasoningSourceReplyWatch || + shouldArmPostRawReasoningSourceReplyWatch + ) { armTurnCompletionIdleWatch({ timeoutMs: CODEX_POST_REASONING_SOURCE_REPLY_IDLE_TIMEOUT_MS }); } else if (unblockedAssistantCompletionRelease) { armTurnAssistantCompletionIdleWatch(describeNotificationActivity(notification)); @@ -2511,6 +2518,7 @@ export async function runCodexAppServerAttempt( !postToolRawAssistantCompletionNeedsTerminalGuard && !rawResponseItemCompletedWithNoActiveItems && !shouldArmPostReasoningSourceReplyWatch && + !shouldArmPostRawReasoningSourceReplyWatch && !shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem ) { // The short completion-idle watchdog guards blind gaps after Codex @@ -5274,6 +5282,14 @@ function isReasoningItemCompletionNotification(notification: CodexServerNotifica return item ? readString(item, "type") === "reasoning" : false; } +function isRawReasoningCompletionNotification(notification: CodexServerNotification): boolean { + if (!isJsonObject(notification.params) || notification.method !== "rawResponseItem/completed") { + return false; + } + const item = isJsonObject(notification.params.item) ? notification.params.item : undefined; + return item ? readString(item, "type") === "reasoning" : false; +} + function isAssistantCompletionReleaseNotification( notification: CodexServerNotification, turnCrossedToolHandoff: boolean,