diff --git a/docs/plugins/codex-harness-reference.md b/docs/plugins/codex-harness-reference.md index 7b24a64ba8cb..a6a39396e0eb 100644 --- a/docs/plugins/codex-harness-reference.md +++ b/docs/plugins/codex-harness-reference.md @@ -85,25 +85,25 @@ For an already-running app-server, use WebSocket transport: Supported `appServer` fields: -| Field | Default | Meaning | -| --------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. | -| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary. | -| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. | -| `url` | unset | WebSocket app-server URL. | -| `authToken` | unset | Bearer token for WebSocket transport. | -| `headers` | `{}` | Extra WebSocket headers. | -| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. | -| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. | -| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. | -| `postToolRawAssistantCompletionIdleTimeoutMs` | `300000` | Completion-idle and progress guard used after a tool handoff, native tool completion, or post-tool raw assistant progress while OpenClaw waits for `turn/completed`. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. | -| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. | -| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start, resume, and turn. | -| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start and resume. Active OpenClaw sandboxes narrow `danger-full-access` turns to Codex `workspace-write`; the turn network flag follows OpenClaw sandbox egress. | -| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed. | -| `defaultWorkspaceDir` | current process directory | Workspace used by `/codex bind` when `--cwd` is omitted. | -| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, and `null` clears the override. Legacy `"fast"` is accepted as `"priority"`. | -| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. | +| Field | Default | Meaning | +| --------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. | +| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary. | +| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. | +| `url` | unset | WebSocket app-server URL. | +| `authToken` | unset | Bearer token for WebSocket transport. | +| `headers` | `{}` | Extra WebSocket headers. | +| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. | +| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. | +| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. | +| `postToolRawAssistantCompletionIdleTimeoutMs` | `300000` | Completion-idle and progress guard used after a tool handoff, native tool completion, post-tool raw assistant progress, raw reasoning completion, or reasoning progress while OpenClaw waits for `turn/completed`. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. | +| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. | +| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start, resume, and turn. | +| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start and resume. Active OpenClaw sandboxes narrow `danger-full-access` turns to Codex `workspace-write`; the turn network flag follows OpenClaw sandbox egress. | +| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed. | +| `defaultWorkspaceDir` | current process directory | Workspace used by `/codex bind` when `--cwd` is omitted. | +| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, and `null` clears the override. Legacy `"fast"` is accepted as `"priority"`. | +| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. | The plugin blocks older or unversioned app-server handshakes. Codex app-server must report stable version `0.125.0` or newer. @@ -333,10 +333,11 @@ because Codex has proven the turn is still alive. Tool handoffs use a longer post-tool idle budget: after OpenClaw returns an `item/tool/call` response, after native tool items such as `commandExecution` complete, after raw `custom_tool_call_output` completions, and after post-tool raw assistant -progress. The guard uses `appServer.postToolRawAssistantCompletionIdleTimeoutMs` -when configured and defaults to five minutes otherwise. That same post-tool -budget also extends the progress watchdog for the silent synthesis window before -Codex emits the next current-turn event. Reasoning completions, commentary +progress, raw reasoning completions, or reasoning progress. The guard uses +`appServer.postToolRawAssistantCompletionIdleTimeoutMs` when configured and +defaults to five minutes otherwise. That same post-tool budget also extends the +progress watchdog for the silent synthesis window before Codex emits the next +current-turn event. Reasoning completions, commentary `agentMessage` completions, and pre-tool raw reasoning or assistant progress can be followed by an automatic final reply, so they use the post-progress reply guard instead of releasing the session lane immediately. Only diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index 30f2fe6541fc..28615d5804a7 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -530,25 +530,25 @@ Supported top-level Codex plugin fields: Supported `appServer` fields: -| Field | Default | Meaning | -| --------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. | -| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. | -| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. | -| `url` | unset | WebSocket app-server URL. | -| `authToken` | unset | Bearer token for WebSocket transport. | -| `headers` | `{}` | Extra WebSocket headers. | -| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. | -| `codeModeOnly` | `false` | Opt into Codex's code-mode-only tool surface. OpenClaw dynamic tools remain registered with Codex so nested `tools.*` calls return through the app-server `item/tool/call` bridge. | -| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. | -| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. | -| `postToolRawAssistantCompletionIdleTimeoutMs` | `300000` | Completion-idle and progress guard used after a tool handoff, native tool completion, or post-tool raw assistant progress while OpenClaw waits for `turn/completed`. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. | -| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. | -| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. | -| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. When an OpenClaw sandbox is active, `danger-full-access` turns use Codex `workspace-write` with network access derived from the OpenClaw sandbox egress setting. | -| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. | -| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. | -| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. | +| Field | Default | Meaning | +| --------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. | +| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. | +| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. | +| `url` | unset | WebSocket app-server URL. | +| `authToken` | unset | Bearer token for WebSocket transport. | +| `headers` | `{}` | Extra WebSocket headers. | +| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. | +| `codeModeOnly` | `false` | Opt into Codex's code-mode-only tool surface. OpenClaw dynamic tools remain registered with Codex so nested `tools.*` calls return through the app-server `item/tool/call` bridge. | +| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. | +| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. | +| `postToolRawAssistantCompletionIdleTimeoutMs` | `300000` | Completion-idle and progress guard used after a tool handoff, native tool completion, post-tool raw assistant progress, raw reasoning completion, or reasoning progress while OpenClaw waits for `turn/completed`. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. | +| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. | +| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. | +| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. When an OpenClaw sandbox is active, `danger-full-access` turns use Codex `workspace-write` with network access derived from the OpenClaw sandbox egress setting. | +| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. | +| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. | +| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. | OpenClaw-owned dynamic tool calls are bounded independently from `appServer.requestTimeoutMs`: Codex `item/tool/call` requests use a 90 second @@ -573,15 +573,16 @@ watchdog because Codex has proven the turn is still alive. Tool handoffs use a longer post-tool idle budget: after OpenClaw returns an `item/tool/call` response, after native tool items such as `commandExecution` complete, after raw `custom_tool_call_output` completions, and after post-tool raw assistant -progress. The guard uses `appServer.postToolRawAssistantCompletionIdleTimeoutMs` -when configured and defaults to five minutes otherwise. That same post-tool -budget also extends the progress watchdog for the silent synthesis window before -Codex emits the next current-turn event. Global app-server notifications, such -as rate-limit updates, do not reset turn-idle progress. Reasoning completions, -commentary `agentMessage` completions, and pre-tool raw reasoning or assistant -progress can be followed by an automatic final reply, so they use the -post-progress reply guard instead of releasing the session lane immediately. -Only final/non-commentary completed `agentMessage` items and pre-tool raw +progress, raw reasoning completions, or reasoning progress. The guard uses +`appServer.postToolRawAssistantCompletionIdleTimeoutMs` when configured and +defaults to five minutes otherwise. That same post-tool budget also extends the +progress watchdog for the silent synthesis window before Codex emits the next +current-turn event. Global app-server notifications, such as rate-limit updates, +do not reset turn-idle progress. Reasoning completions, commentary +`agentMessage` completions, and pre-tool raw reasoning or assistant progress can +be followed by an automatic final reply, so they use the post-progress reply +guard instead of releasing the session lane immediately. Only +final/non-commentary completed `agentMessage` items and pre-tool raw assistant completions arm the assistant-output release: if Codex then goes quiet without `turn/completed`, OpenClaw best-effort interrupts the native turn and releases the session lane. Replay-safe stdio app-server failures, including diff --git a/extensions/codex/src/app-server/attempt-notification-state.ts b/extensions/codex/src/app-server/attempt-notification-state.ts index 2bc9f1648d8c..cb6361d22f5b 100644 --- a/extensions/codex/src/app-server/attempt-notification-state.ts +++ b/extensions/codex/src/app-server/attempt-notification-state.ts @@ -14,6 +14,7 @@ import { isRawAssistantProgressNotification, isRawReasoningCompletionNotification, isRawToolOutputCompletionNotification, + isReasoningProgressNotification, isReasoningItemCompletionNotification, isRetryableErrorNotification, isTurnNotification, @@ -145,11 +146,13 @@ export function applyCodexTurnNotificationState(params: { notification, turnCrossedToolHandoff, ); - const postToolRawAssistantCompletionNeedsTerminalGuard = + const postToolProgressNeedsTerminalGuard = isCurrentTurnNotification && turnCrossedToolHandoff && - isRawAssistantProgressNotification(notification) && - params.activeTurnItemIds.size === 0; + (((isRawAssistantProgressNotification(notification) || + isRawReasoningCompletionNotification(notification)) && + params.activeTurnItemIds.size === 0) || + isReasoningProgressNotification(notification)); const postToolPatchUpdateNeedsTerminalGuard = isCurrentTurnNotification && turnCrossedToolHandoff && @@ -160,7 +163,7 @@ export function applyCodexTurnNotificationState(params: { params.activeTurnItemIds.size === 0 && params.activeAppServerTurnRequests === 0 && !assistantCompletionCanRelease && - !postToolRawAssistantCompletionNeedsTerminalGuard && + !postToolProgressNeedsTerminalGuard && !rawToolOutputCompletion; const shouldArmNoToolPostProgressReplyWatch = isCurrentTurnNotification && @@ -182,7 +185,7 @@ export function applyCodexTurnNotificationState(params: { !shouldArmNoToolPostProgressReplyWatch; const shouldUsePostToolContinuationWatch = turnCrossedToolHandoff && - (postToolRawAssistantCompletionNeedsTerminalGuard || + (postToolProgressNeedsTerminalGuard || postToolPatchUpdateNeedsTerminalGuard || rawToolOutputCompletion || trackedDynamicToolCompletion || @@ -211,13 +214,11 @@ export function applyCodexTurnNotificationState(params: { turnWatches.disarmAssistantCompletionIdleWatch(); } else if (isCurrentTurnNotification && assistantCompletionCanRelease) { turnWatches.armAssistantCompletionIdleWatch(describeNotificationActivity(notification)); - } else if ( - postToolRawAssistantCompletionNeedsTerminalGuard || - postToolPatchUpdateNeedsTerminalGuard - ) { - // Post-tool assistant status and patch snapshots can be followed by more - // native edit streaming. Keep the short guard alive until Codex reports a - // terminal turn state instead of falling back to the long terminal watch. + } else if (postToolProgressNeedsTerminalGuard || postToolPatchUpdateNeedsTerminalGuard) { + // Post-tool assistant/reasoning status and patch snapshots can be followed + // by more native edit streaming. Keep the short guard alive until Codex + // reports a terminal turn state instead of falling back to the long + // terminal watch. armPostToolContinuationWatch(); } else if (shouldArmNoToolPostProgressReplyWatch || shouldArmNoToolPostRawProgressReplyWatch) { armPostProgressReplyWatch(); @@ -252,7 +253,7 @@ export function applyCodexTurnNotificationState(params: { !isNativeResponseStreamDelta && !trackedDynamicToolCompletion && !rawToolOutputCompletion && - !postToolRawAssistantCompletionNeedsTerminalGuard && + !postToolProgressNeedsTerminalGuard && !postToolPatchUpdateNeedsTerminalGuard && !rawResponseItemCompletedWithNoActiveItems && !shouldArmNoToolPostProgressReplyWatch && diff --git a/extensions/codex/src/app-server/attempt-notifications.ts b/extensions/codex/src/app-server/attempt-notifications.ts index 558662216e7c..e46098a76c72 100644 --- a/extensions/codex/src/app-server/attempt-notifications.ts +++ b/extensions/codex/src/app-server/attempt-notifications.ts @@ -115,6 +115,15 @@ export function isRawReasoningCompletionNotification( return item ? readString(item, "type") === "reasoning" : false; } +/** Returns true for streamed app-server reasoning progress. */ +export function isReasoningProgressNotification(notification: CodexServerNotification): boolean { + return ( + notification.method === "item/reasoning/textDelta" || + notification.method === "item/reasoning/summaryTextDelta" || + notification.method === "item/reasoning/summaryPartAdded" + ); +} + /** Returns true when assistant completion can release the short idle watch. */ export function isAssistantCompletionReleaseNotification( notification: CodexServerNotification, diff --git a/extensions/codex/src/app-server/attempt-timeouts.ts b/extensions/codex/src/app-server/attempt-timeouts.ts index a5c0228eb4f1..b2185286846f 100644 --- a/extensions/codex/src/app-server/attempt-timeouts.ts +++ b/extensions/codex/src/app-server/attempt-timeouts.ts @@ -10,9 +10,10 @@ export const CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS = 100; export const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 60_000; /** Short guard after apparent assistant completion. */ export const CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 10_000; -// Native Codex can stream a large custom tool input after a raw assistant -// progress item. Forwarded deltas count as activity, but older native paths may -// not surface them, so keep this terminal guard conservative. +// Native Codex can spend a long quiet window synthesizing after tool results, +// raw assistant/reasoning completions, or reasoning progress. Forwarded deltas +// count as activity, but older native paths may not surface them, so keep this +// terminal guard conservative. export const CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 5 * 60_000; /** Guard after reasoning/commentary progress when no tool handoff occurred. */ export const CODEX_POST_REASONING_REPLY_IDLE_TIMEOUT_MS = 5 * 60_000; diff --git a/extensions/codex/src/app-server/run-attempt.turn-watches.test.ts b/extensions/codex/src/app-server/run-attempt.turn-watches.test.ts index 24f896ffd927..3560064ec74c 100644 --- a/extensions/codex/src/app-server/run-attempt.turn-watches.test.ts +++ b/extensions/codex/src/app-server/run-attempt.turn-watches.test.ts @@ -16,7 +16,7 @@ import * as authBridge from "./auth-bridge.js"; import { createCodexDynamicToolBridge } from "./dynamic-tools.js"; import * as elicitationBridge from "./elicitation-bridge.js"; import { CodexAppServerEventProjector } from "./event-projector.js"; -import type { CodexServerNotification } from "./protocol.js"; +import type { CodexServerNotification, JsonObject } from "./protocol.js"; import { readRecentCodexRateLimits } from "./rate-limit-cache.js"; import { createParams, @@ -2485,7 +2485,7 @@ describe("runCodexAppServerAttempt turn watches", () => { ).toBe(false); }); - it("arms completion idle watch after non-assistant rawResponseItem/completed with no active items", async () => { + it("uses the post-tool timeout after raw reasoning completes", async () => { let notify: (notification: CodexServerNotification) => Promise = async () => undefined; let handleRequest: | ((request: { id: string; method: string; params?: unknown }) => Promise) @@ -2529,6 +2529,7 @@ describe("runCodexAppServerAttempt turn watches", () => { const run = runCodexAppServerAttempt(params, { turnCompletionIdleTimeoutMs: 5, turnAssistantCompletionIdleTimeoutMs: 500, + postToolRawAssistantCompletionIdleTimeoutMs: 80, turnTerminalIdleTimeoutMs: 500, }); await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function"), fastWait); @@ -2546,13 +2547,9 @@ describe("runCodexAppServerAttempt turn watches", () => { }, })) as { success?: boolean }; expect(toolResult.success).toBe(false); - // Send a rawResponseItem/completed with type "reasoning" — this does NOT - // qualify as postToolRawAssistantCompletionNeedsTerminalGuard (which - // requires type=message + role=assistant + text preview). Before the fix, - // this would disarm the completion idle watch via the catch-all disarm - // block, leaving only the 30-minute terminal timeout. After the fix, - // rawResponseItemCompletedWithNoActiveItems keeps the 60s (here 5ms) - // completion idle watch armed. + // Post-tool reasoning can precede the final reply; keep the longer + // post-tool guard armed instead of falling back to the generic completion + // idle timeout. await notify({ method: "rawResponseItem/completed", params: { @@ -2560,7 +2557,8 @@ describe("runCodexAppServerAttempt turn watches", () => { turnId: "turn-1", item: { type: "reasoning", - id: "raw-reasoning-1", + summary: [], + encrypted_content: null, }, }, }); @@ -2578,10 +2576,10 @@ describe("runCodexAppServerAttempt turn watches", () => { const completionWarnData = completionWarnCall?.[1] as | { lastActivityReason?: string; timeoutMs?: number } | undefined; - expect(completionWarnData?.timeoutMs).toBe(5); + expect(completionWarnData?.timeoutMs).toBe(80); expect(completionWarnData?.lastActivityReason).toBe("notification:rawResponseItem/completed"); - // The terminal idle watch (500ms) should NOT have fired — the shorter - // completion idle watch (5ms) should catch the stall first. + // The terminal idle watch (500ms) should NOT have fired; the post-tool + // completion idle watch should catch the stall first. expect( warn.mock.calls.some( ([message]) => @@ -2590,6 +2588,98 @@ describe("runCodexAppServerAttempt turn watches", () => { ).toBe(false); }); + const reasoningProgressNotifications: { + method: string; + progressParams: JsonObject; + }[] = [ + { + method: "item/reasoning/textDelta", + progressParams: { delta: "thinking after tool", contentIndex: 0 }, + }, + { + method: "item/reasoning/summaryTextDelta", + progressParams: { delta: "thinking after tool", summaryIndex: 0 }, + }, + { + method: "item/reasoning/summaryPartAdded", + progressParams: { summaryIndex: 0 }, + }, + ]; + + it.each(reasoningProgressNotifications)( + "uses the post-tool timeout after streamed reasoning progress from $method", + async ({ method, progressParams }) => { + const harness = createStartedThreadHarness(); + const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined); + const params = createParams( + path.join(tempDir, "session.jsonl"), + path.join(tempDir, "workspace"), + ); + params.timeoutMs = 60_000; + + const run = runCodexAppServerAttempt(params, { + turnCompletionIdleTimeoutMs: 20, + turnAssistantCompletionIdleTimeoutMs: 500, + postToolRawAssistantCompletionIdleTimeoutMs: 80, + turnTerminalIdleTimeoutMs: 500, + }); + await harness.waitForMethod("turn/start"); + + const toolResult = (await harness.handleServerRequest({ + id: "request-tool-1", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "call-1", + namespace: null, + tool: "message", + arguments: { action: "send", text: "already sent" }, + }, + })) as { success?: boolean }; + expect(toolResult.success).toBe(false); + await harness.notify({ + method: "item/started", + params: { + threadId: "thread-1", + turnId: "turn-1", + item: { id: "reasoning-1", type: "reasoning" }, + }, + }); + await harness.notify({ + method, + params: { + threadId: "thread-1", + turnId: "turn-1", + itemId: "reasoning-1", + ...progressParams, + }, + }); + + const result = await run; + expect(result.aborted).toBe(true); + expect(result.timedOut).toBe(true); + expect(result.promptError).toBe( + "codex app-server turn idle timed out waiting for turn/completed", + ); + const completionWarnCall = warn.mock.calls.find( + ([message]) => message === "codex app-server turn idle timed out waiting for completion", + ); + expect(completionWarnCall).toBeDefined(); + const completionWarnData = completionWarnCall?.[1] as + | { lastActivityReason?: string; timeoutMs?: number } + | undefined; + expect(completionWarnData?.timeoutMs).toBe(80); + expect(completionWarnData?.lastActivityReason).toBe(`notification:${method}`); + expect( + warn.mock.calls.some( + ([message]) => + message === "codex app-server turn idle timed out waiting for terminal event", + ), + ).toBe(false); + }, + ); + it("releases the session when Codex accepts a turn but never sends progress", async () => { const harness = createStartedThreadHarness(); const params = createParams( diff --git a/scripts/e2e/lib/fixtures/workspace.mjs b/scripts/e2e/lib/fixtures/workspace.mjs index 1e7374696037..00cdc48be415 100644 --- a/scripts/e2e/lib/fixtures/workspace.mjs +++ b/scripts/e2e/lib/fixtures/workspace.mjs @@ -53,24 +53,24 @@ function writeAgentsDeleteConfig() { } function assertAgentsDeleteResult([outputPath]) { - outputPath = requireArg(outputPath, "outputPath"); - const outputStat = fs.statSync(outputPath); + const resolvedOutputPath = requireArg(outputPath, "outputPath"); + const outputStat = fs.statSync(resolvedOutputPath); if (outputStat.isFile() && outputStat.size > AGENTS_DELETE_OUTPUT_MAX_BYTES) { throw new Error( `agents delete --json output exceeded ${AGENTS_DELETE_OUTPUT_MAX_BYTES} bytes:\nstdout tail=${readTextFileTail( - outputPath, + resolvedOutputPath, ERROR_DETAIL_TAIL_BYTES, )}`, ); } let parsed; try { - parsed = readJson(outputPath); + parsed = readJson(resolvedOutputPath); } catch (error) { console.error("agents delete --json did not emit valid JSON:"); - console.error(readTextFileTail(outputPath, ERROR_DETAIL_TAIL_BYTES).trim()); + console.error(readTextFileTail(resolvedOutputPath, ERROR_DETAIL_TAIL_BYTES).trim()); const message = error instanceof Error ? error.message.split("\n").at(0) : String(error); - throw new Error(`agents delete --json parse failed: ${message}`); + throw new Error(`agents delete --json parse failed: ${message}`, { cause: error }); } for (const [actual, expected, label] of [ [parsed.agentId, "ops", "agentId"],