Skip to content

Commit 78ec4fa

Browse files
Usernameclaude
authored andcommitted
fix(codex): prevent false turn idle timeouts under event-loop pressure (#86948)
The completion-idle timer (60 s) fires based on when notifications are *processed* inside the chained notification queue, not when they are *received* from the app-server binary over stdio. Since ea16a5e added a `setImmediate` yield between each queued dispatch, every notification is delayed by at least one event-loop tick. Under heavy load (tool-call-intensive turns) the ticks accumulate, and the `turn/completed` notification can arrive from the binary but sit in the queue for longer than 60 s — at which point the idle timer fires and aborts the turn. Fix: 1. Touch `turnCompletionLastActivityAt` and `turnAttemptLastProgressAt` at *enqueue* time (when readline delivers the line) so the idle timer sees live binary I/O even when the queue is backed up. 2. Guard all three idle-timeout handlers (`fireTurnCompletionIdleTimeout`, `fireTurnAttemptIdleTimeout`, `fireTurnTerminalIdleTimeout`) with the existing `terminalTurnNotificationQueued` flag so they never fire when `turn/completed` has already been received but not yet processed. Closes #86948 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d6b7f4 commit 78ec4fa

1 file changed

Lines changed: 19 additions & 1 deletion

File tree

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1979,7 +1979,12 @@ export async function runCodexAppServerAttempt(
19791979
};
19801980

19811981
const fireTurnAttemptIdleTimeout = () => {
1982-
if (completed || runAbortController.signal.aborted || !turnAttemptIdleWatchArmed) {
1982+
if (
1983+
completed ||
1984+
terminalTurnNotificationQueued ||
1985+
runAbortController.signal.aborted ||
1986+
!turnAttemptIdleWatchArmed
1987+
) {
19831988
return;
19841989
}
19851990
const idleMs = Math.max(0, Date.now() - turnAttemptLastProgressAt);
@@ -2014,6 +2019,7 @@ export async function runCodexAppServerAttempt(
20142019
const fireTurnCompletionIdleTimeout = () => {
20152020
if (
20162021
completed ||
2022+
terminalTurnNotificationQueued ||
20172023
runAbortController.signal.aborted ||
20182024
!turnCompletionIdleWatchArmed ||
20192025
activeAppServerTurnRequests > 0
@@ -2053,6 +2059,7 @@ export async function runCodexAppServerAttempt(
20532059
const fireTurnTerminalIdleTimeout = () => {
20542060
if (
20552061
completed ||
2062+
terminalTurnNotificationQueued ||
20562063
runAbortController.signal.aborted ||
20572064
!turnTerminalIdleWatchArmed ||
20582065
activeAppServerTurnRequests > 0
@@ -2581,6 +2588,17 @@ export async function runCodexAppServerAttempt(
25812588
if (isTerminalTurnNotificationForTurn(notification, turnId)) {
25822589
terminalTurnNotificationQueued = true;
25832590
}
2591+
// Touch idle-watch timestamps at receive time (not just at process time)
2592+
// so that the completion-idle timer sees live binary I/O even when the
2593+
// notification queue is backed up behind setImmediate yields under heavy
2594+
// event-loop load. See openclaw/openclaw#86948.
2595+
if (correlation.matchesActiveTurn !== false) {
2596+
const now = Date.now();
2597+
turnCompletionLastActivityAt = now;
2598+
turnCompletionLastActivityReason = `notification:${notification.method}`;
2599+
turnAttemptLastProgressAt = now;
2600+
turnAttemptLastProgressReason = `notification:${notification.method}`;
2601+
}
25842602
notificationQueue = notificationQueue.then(
25852603
() => handleNotification(notification),
25862604
() => handleNotification(notification),

0 commit comments

Comments
 (0)