mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(codex): preserve post-tool reasoning liveness
Preserve the Codex post-tool continuation guard for raw reasoning completions and streamed reasoning progress so valid post-tool synthesis stays on the intended completion watchdog instead of falling through to terminal idle behavior. Verified with focused Codex watchdog tests, test typecheck, scripts lint, autoreview, and CI run 27086637988. Thanks @fuller-stack-dev. Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void> = async () => undefined;
|
||||
let handleRequest:
|
||||
| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)
|
||||
@@ -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(
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user