Skip to content

Commit 1bb4916

Browse files
committed
fix(codex): time out silent app-server turns
1 parent b4f6cb2 commit 1bb4916

6 files changed

Lines changed: 70 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
1111
### Fixes
1212

1313
- Agents/auto-reply: honor `agents.defaults.silentReply` and per-surface group silent-reply policy when generic agent-run failure fallbacks decide whether to send visible fallback text. Fixes #82060. (#82086) Thanks @taozengabc.
14+
- Codex app-server: arm the short idle watchdog as soon as Codex accepts a turn, so accepted turns with no current-turn progress release the OpenClaw session lane before the outer model timeout. Fixes #82129. Thanks @Francois3d.
1415
- Control UI/WebChat: focus the composer when users click the visible input chrome and restore larger, labeled desktop composer controls while preserving compact mobile taps. Fixes #45656. Thanks @BunsDev.
1516
- System events: keep owner downgrades in structured metadata while rendering queued prompt text as plain `System:` lines, preserving least-privilege wakeups without prompt-visible trust labels. (#82067)
1617
- Providers/Xiaomi: preserve MiMo `reasoning_content` on multi-turn tool-call replay, including custom Xiaomi-compatible proxy routes, so follow-up turns no longer fail with `400 Param Incorrect`. Fixes #81419. (#81589) Thanks @lovelefeng-glitch and @jimdawdy-hub.

docs/plugins/codex-harness-reference.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Supported `appServer` fields:
9595
| `headers` | `{}` | Extra WebSocket headers. |
9696
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. |
9797
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
98-
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. |
98+
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. |
9999
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. |
100100
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start, resume, and turn. |
101101
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start and resume. |
@@ -253,12 +253,13 @@ Dynamic tool budgets are capped at 600000 ms. On timeout, OpenClaw aborts the
253253
tool signal where supported and returns a failed dynamic-tool response to Codex
254254
so the turn can continue instead of leaving the session in `processing`.
255255

256-
After OpenClaw responds to a Codex turn-scoped app-server request, the harness
257-
also expects Codex to finish the native turn with `turn/completed`. If the
258-
app-server goes quiet for `appServer.turnCompletionIdleTimeoutMs` after that
259-
response, OpenClaw best-effort interrupts the Codex turn, records a diagnostic
260-
timeout, and releases the OpenClaw session lane so follow-up chat messages are
261-
not queued behind a stale native turn.
256+
After Codex accepts a turn, and after OpenClaw responds to a turn-scoped
257+
app-server request, the harness expects Codex to make current-turn progress and
258+
eventually finish the native turn with `turn/completed`. If the app-server goes
259+
quiet for `appServer.turnCompletionIdleTimeoutMs`, OpenClaw best-effort
260+
interrupts the Codex turn, records a diagnostic timeout, and releases the
261+
OpenClaw session lane so follow-up chat messages are not queued behind a stale
262+
native turn.
262263

263264
Any non-terminal notification for the same turn, including
264265
`rawResponseItem/completed`, disarms that short watchdog because Codex has

docs/plugins/codex-harness.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ Supported `appServer` fields:
493493
| `headers` | `{}` | Extra WebSocket headers. |
494494
| `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. |
495495
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
496-
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after a turn-scoped Codex app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
496+
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
497497
| `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. |
498498
| `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. |
499499
| `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` is narrowed to `"workspace-write"`. |
@@ -511,16 +511,17 @@ budgets are capped at 600000 ms. On timeout, OpenClaw aborts the tool signal
511511
where supported and returns a failed dynamic-tool response to Codex so the turn
512512
can continue instead of leaving the session in `processing`.
513513

514-
After OpenClaw responds to a Codex turn-scoped app-server request, the harness
515-
also expects Codex to finish the native turn with `turn/completed`. If the
516-
app-server goes quiet for `appServer.turnCompletionIdleTimeoutMs` after that
517-
response, OpenClaw best-effort interrupts the Codex turn, records a diagnostic
518-
timeout, and releases the OpenClaw session lane so follow-up chat messages are
519-
not queued behind a stale native turn. Any non-terminal notification for the
520-
same turn, including `rawResponseItem/completed`, disarms that short watchdog
521-
because Codex has proven the turn is still alive; the longer terminal watchdog
522-
continues to protect genuinely stuck turns. Global app-server notifications,
523-
such as rate-limit updates, do not reset turn-idle progress. When Codex emits a
514+
After Codex accepts a turn, and after OpenClaw responds to a turn-scoped
515+
app-server request, the harness expects Codex to make current-turn progress and
516+
eventually finish the native turn with `turn/completed`. If the app-server goes
517+
quiet for `appServer.turnCompletionIdleTimeoutMs`, OpenClaw best-effort
518+
interrupts the Codex turn, records a diagnostic timeout, and releases the
519+
OpenClaw session lane so follow-up chat messages are not queued behind a stale
520+
native turn. Any non-terminal notification for the same turn, including
521+
`rawResponseItem/completed`, disarms that short watchdog because Codex has
522+
proven the turn is still alive; the longer terminal watchdog continues to
523+
protect genuinely stuck turns. Global app-server notifications, such as
524+
rate-limit updates, do not reset turn-idle progress. When Codex emits a
524525
completed `agentMessage` item and then goes quiet without `turn/completed`,
525526
OpenClaw treats the assistant output as effectively complete, best-effort
526527
interrupts the native Codex turn, and releases the session lane. Timeout

extensions/codex/openclaw.plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@
333333
},
334334
"appServer.turnCompletionIdleTimeoutMs": {
335335
"label": "Turn Completion Idle Timeout",
336-
"help": "Maximum quiet time after a turn-scoped Codex app-server request before OpenClaw interrupts the turn while waiting for turn/completed.",
336+
"help": "Maximum quiet time after Codex accepts a turn or after a turn-scoped app-server request before OpenClaw interrupts the turn while waiting for turn/completed.",
337337
"advanced": true
338338
},
339339
"appServer.approvalPolicy": {

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1921,7 +1921,7 @@ describe("runCodexAppServerAttempt", () => {
19211921
);
19221922
params.timeoutMs = 60_000;
19231923

1924-
const run = runCodexAppServerAttempt(params, { turnTerminalIdleTimeoutMs: 5 });
1924+
const run = runCodexAppServerAttempt(params, { turnCompletionIdleTimeoutMs: 5 });
19251925
await harness.waitForMethod("turn/start");
19261926

19271927
const result = await run;
@@ -1953,7 +1953,7 @@ describe("runCodexAppServerAttempt", () => {
19531953
);
19541954
params.timeoutMs = 200;
19551955

1956-
const run = runCodexAppServerAttempt(params, { turnTerminalIdleTimeoutMs: 15 });
1956+
const run = runCodexAppServerAttempt(params, { turnCompletionIdleTimeoutMs: 15 });
19571957
await harness.waitForMethod("turn/start");
19581958
await harness.notify(rateLimitsUpdated(Date.now() + 60_000));
19591959
await new Promise((resolve) => setTimeout(resolve, 20));
@@ -3842,6 +3842,45 @@ describe("runCodexAppServerAttempt", () => {
38423842
expect(result.timedOut).toBe(false);
38433843
});
38443844

3845+
it("does not time out when turn progress arrives before turn/start returns", async () => {
3846+
let harness: ReturnType<typeof createAppServerHarness>;
3847+
harness = createAppServerHarness(async (method) => {
3848+
if (method === "thread/start") {
3849+
return threadStartResult();
3850+
}
3851+
if (method === "turn/start") {
3852+
await harness.notify({
3853+
method: "turn/started",
3854+
params: {
3855+
threadId: "thread-1",
3856+
turnId: "turn-1",
3857+
turn: { id: "turn-1", status: "inProgress" },
3858+
},
3859+
});
3860+
return turnStartResult("turn-1", "inProgress");
3861+
}
3862+
return {};
3863+
});
3864+
const params = createParams(
3865+
path.join(tempDir, "session.jsonl"),
3866+
path.join(tempDir, "workspace"),
3867+
);
3868+
params.timeoutMs = 60_000;
3869+
3870+
const run = runCodexAppServerAttempt(params, {
3871+
turnCompletionIdleTimeoutMs: 5,
3872+
turnTerminalIdleTimeoutMs: 60_000,
3873+
});
3874+
await harness.waitForMethod("turn/start");
3875+
await new Promise((resolve) => setTimeout(resolve, 20));
3876+
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
3877+
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
3878+
3879+
const result = await run;
3880+
expect(result.aborted).toBe(false);
3881+
expect(result.timedOut).toBe(false);
3882+
});
3883+
38453884
it("completes when turn/start returns a terminal turn without a follow-up notification", async () => {
38463885
const harness = createAppServerHarness(async (method) => {
38473886
if (method === "thread/start") {

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,10 +1283,11 @@ export async function runCodexAppServerAttempt(
12831283
activeOpenClawDynamicToolCallIds,
12841284
)
12851285
) {
1286-
// The short completion-idle watchdog only guards the blind gap after
1287-
// OpenClaw hands a turn-scoped request result back to Codex. Bookkeeping
1288-
// that closes the just-served OpenClaw dynamic tool item is still part of
1289-
// that handoff, so keep the short watchdog armed for that notification.
1286+
// The short completion-idle watchdog guards blind gaps after Codex
1287+
// accepts a turn or after OpenClaw hands a turn-scoped request result
1288+
// back to Codex. Bookkeeping that closes the just-served OpenClaw
1289+
// dynamic tool item is still part of that handoff, so keep the short
1290+
// watchdog armed for that notification.
12901291
disarmTurnCompletionIdleWatch();
12911292
}
12921293
// Determine terminal-turn status before invoking the projector so a throw
@@ -1637,6 +1638,8 @@ export async function runCodexAppServerAttempt(
16371638
});
16381639
emitLifecycleStart();
16391640
const activeProjector = projector;
1641+
turnTerminalIdleWatchArmed = true;
1642+
touchTurnCompletionActivity("turn:start", { arm: true });
16401643
for (const notification of pendingNotifications.splice(0)) {
16411644
await enqueueNotification(notification);
16421645
}
@@ -1669,8 +1672,6 @@ export async function runCodexAppServerAttempt(
16691672
abort: () => runAbortController.abort("aborted"),
16701673
};
16711674
setActiveEmbeddedRun(params.sessionId, handle, params.sessionKey);
1672-
turnTerminalIdleWatchArmed = true;
1673-
touchTurnCompletionActivity("turn:start");
16741675

16751676
const timeout = setTimeout(
16761677
() => {

0 commit comments

Comments
 (0)