Repository navigation
fix(codex): add progress-stall watchdog for keepalive-only Codex hangs - #27
Merged
Merged
Conversation
The chatgpt.com/backend-api/codex endpoint has a failure mode where it keeps the socket alive with periodic keepalive / response.in_progress frames but never emits a real delta or completes. Both existing watchdogs were satisfied by these frames (the no-byte TTFB detector saw events arriving; the stream-idle detector saw the keepalive gap stay under its threshold), so the call rode the socket all the way to the blunt wall-clock stale timeout — burning the full timeout on every retry (observed: 90s x 6 retries ~= 9 min) before falling back. run_codex_stream already reset _codex_stream_last_progress_ts but never set or read it — a dead, half-wired signal. Wire it up: - codex_runtime._on_event now stamps _codex_stream_last_progress_ts only on REAL forward progress (text/reasoning deltas, function-call frames, output-item completions, terminal/error events), distinct from _codex_stream_last_event_ts which any frame (incl. keepalive) refreshes. - A new progress-stall watchdog in interruptible_api_call kills a stream that is emitting frames but making zero forward progress past the fast-reconnect cutoff (default = the no-byte TTFB cutoff, clamped below the stale timer), so the retry loop reconnects in ~1s instead of waiting out the blunt stale timeout. Tunable via HERMES_CODEX_PROGRESS_STALE_TIMEOUT_SECONDS (0 disables). Verified against the captured prod failure signature: a keepalive-only stall that burned 92s now dies at ~42s with codex_progress_stall_kill. Two regression tests added; the no-byte and slow-first-event paths are unchanged.
🔎 Lint report:
|
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 27, 2026
- kanban_db #23: the in-txn takeover audit fires only when the txn changed the target card's guarded columns (status/assignee/priority/session_id), not on any side write (reclaim_refused), so a refused mutation never records or re-homes. No longer reads conn.total_changes (fixes write_txn_busy_retry fakes, CI slice 13). - kanban_db #44: birth home re-resolved at the TOP of the write txn, before the duplicate / parent checks. - kanban_db #70: no own-claim exception by profile name; without a run id only a lapsed run (claim expired, no live owner) can be closed. - checkout_admission #52: read-check-replace under a per-consumer flock. - discord #30/#31: cancellation recorded before fut.done(); a cancelled disconnect waits for the in-flight flush and still runs every flush. - run.py #36: lock held until the worker write finishes on cancel; every write runs in FIFO order (no skipped-but-reported resets). - ci_overflow_integration #27: documented + tested that a secondary-limit 403 arrives as RateLimited(429) and fails closed. New tests, each red against the previous commit.
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 27, 2026
…, PR954) PATCH a nonexistent variable: 403 = denied (PASS), anything else fails closed. The live slot variables are never GET-then-PATCHed, so a fail-open identity can no longer revert a controller change. Tests: test_variable_write_probe_never_writes_an_existing_variable (red on base), test_variable_write_probe_fails_closed_unless_403.
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 27, 2026
- kanban_db #23: the in-txn takeover audit fires only when the txn changed the target card's guarded columns (status/assignee/priority/session_id), not on any side write (reclaim_refused), so a refused mutation never records or re-homes. No longer reads conn.total_changes (fixes write_txn_busy_retry fakes, CI slice 13). - kanban_db #44: birth home re-resolved at the TOP of the write txn, before the duplicate / parent checks. - kanban_db #70: no own-claim exception by profile name; without a run id only a lapsed run (claim expired, no live owner) can be closed. - checkout_admission #52: read-check-replace under a per-consumer flock. - discord #30/#31: cancellation recorded before fut.done(); a cancelled disconnect waits for the in-flight flush and still runs every flush. - run.py #36: lock held until the worker write finishes on cancel; every write runs in FIFO order (no skipped-but-reported resets). - ci_overflow_integration #27: documented + tested that a secondary-limit 403 arrives as RateLimited(429) and fails closed. New tests, each red against the previous commit.
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 27, 2026
…, PR954) PATCH a nonexistent variable: 403 = denied (PASS), anything else fails closed. The live slot variables are never GET-then-PATCHed, so a fail-open identity can no longer revert a controller change. Tests: test_variable_write_probe_never_writes_an_existing_variable (red on base), test_variable_write_probe_fails_closed_unless_403.
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 27, 2026
- kanban_db #23: the in-txn takeover audit fires only when the txn changed the target card's guarded columns (status/assignee/priority/session_id), not on any side write (reclaim_refused), so a refused mutation never records or re-homes. No longer reads conn.total_changes (fixes write_txn_busy_retry fakes, CI slice 13). - kanban_db #44: birth home re-resolved at the TOP of the write txn, before the duplicate / parent checks. - kanban_db #70: no own-claim exception by profile name; without a run id only a lapsed run (claim expired, no live owner) can be closed. - checkout_admission #52: read-check-replace under a per-consumer flock. - discord #30/#31: cancellation recorded before fut.done(); a cancelled disconnect waits for the in-flight flush and still runs every flush. - run.py #36: lock held until the worker write finishes on cancel; every write runs in FIFO order (no skipped-but-reported resets). - ci_overflow_integration #27: documented + tested that a secondary-limit 403 arrives as RateLimited(429) and fails closed. New tests, each red against the previous commit.
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 28, 2026
…, PR954) PATCH a nonexistent variable: 403 = denied (PASS), anything else fails closed. The live slot variables are never GET-then-PATCHed, so a fail-open identity can no longer revert a controller change. Tests: test_variable_write_probe_never_writes_an_existing_variable (red on base), test_variable_write_probe_fails_closed_unless_403.
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 28, 2026
- kanban_db #23: the in-txn takeover audit fires only when the txn changed the target card's guarded columns (status/assignee/priority/session_id), not on any side write (reclaim_refused), so a refused mutation never records or re-homes. No longer reads conn.total_changes (fixes write_txn_busy_retry fakes, CI slice 13). - kanban_db #44: birth home re-resolved at the TOP of the write txn, before the duplicate / parent checks. - kanban_db #70: no own-claim exception by profile name; without a run id only a lapsed run (claim expired, no live owner) can be closed. - checkout_admission #52: read-check-replace under a per-consumer flock. - discord #30/#31: cancellation recorded before fut.done(); a cancelled disconnect waits for the in-flight flush and still runs every flush. - run.py #36: lock held until the worker write finishes on cancel; every write runs in FIFO order (no skipped-but-reported resets). - ci_overflow_integration #27: documented + tested that a secondary-limit 403 arrives as RateLimited(429) and fails closed. New tests, each red against the previous commit.
Kyzcreig
pushed a commit
that referenced
this pull request
Sep 28, 2026
…xempt an aggregate scan (t_e9ca7d13)
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 28, 2026
…xempt an aggregate scan (t_e9ca7d13)
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Sep 28, 2026
…xempt an aggregate scan (t_e9ca7d13)
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Oct 4, 2026
…w#27) fleet-floor-workflows#27 dropped the ref-keyed cancel-in-progress group from the reusable override-lint.yml, so a push + PR-body edit on one head no longer leaves a cancelled override_lint twin. Pin is the #27 merge commit (REST merge_commit_sha). Card: t_67545768
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Oct 14, 2026
…8370e) SHA-only. Callee concurrency/on: non-comment content identical to the old pin (js-ci, js-typecheck, js-dep-audit, sast, secret-scan; override-lint: comment-only diff since b701cc0). R18 control at 68136b5 rc=0: push:main backstop-safe, PR supersedes.
ang-fleet-workers Bot
added a commit
that referenced
this pull request
Oct 14, 2026
…8370e) SHA-only. Callee concurrency/on: non-comment content identical to the old pin (js-ci, js-typecheck, js-dep-audit, sast, secret-scan; override-lint: comment-only diff since b701cc0). R18 control at 68136b5 rc=0: push:main backstop-safe, PR supersedes.
Kyzcreig
added a commit
that referenced
this pull request
Oct 14, 2026
… (t_7458370e) (#1994) * ci: reusable callees never cancel a caller's push:main backstop (R18) (t_7458370e) tests.yml, e2e-desktop{,-core,-update}.yml, lockfile-diff.yml and windows-install-update-e2e.yml are workflow_call callees of ci.yaml/ci-local.yaml. Their github.ref-keyed cancel-in-progress group applies to every caller run, so the next merge cancelled the running push:main backstop (2 of the last 18 ci.yaml push:main runs ended cancelled). On refs/heads/main the group is now github.run_id and cancel-in-progress is false. Every other ref (PR, merge_group queue refs, wine2e-install/**) keeps its previous group and cancel. Verified: ci_speed_lint R18 ERROR 6 -> 0 on these files; yaml.safe_load over all workflows ok. * ci: pin fleet-floor-workflows callers to 68136b5 (#27 #28 #29) (t_7458370e) SHA-only. Callee concurrency/on: non-comment content identical to the old pin (js-ci, js-typecheck, js-dep-audit, sast, secret-scan; override-lint: comment-only diff since b701cc0). R18 control at 68136b5 rc=0: push:main backstop-safe, PR supersedes. --------- Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com> Co-authored-by: ang-fleet-workers[bot] <333956806+ang-fleet-workers[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
chatgpt.com/backend-api/codexhas a failure mode where it keeps thesocket alive with periodic keepalive /
response.in_progressframes butnever emits a real delta or completes. Both existing Codex watchdogs
are satisfied by these frames:
_codex_stream_last_event_tsis set), so it never fires;So the call rides the socket all the way to the blunt wall-clock stale
timeout and burns it on every retry — observed in production:
90s × 6 retries ≈ 9 minof dead time before the fallback model kicks in. A freshreconnect to the same backend succeeds in ~1s (verified by direct probe).
This killed a scheduled digest run during a ~35-min Codex blip: 22 consecutive
Non-streaming API call stale for 90skills, the 40s fast-reconnect cutoffarmed every retry but never won.
Root cause
run_codex_streamreset_codex_stream_last_progress_ts(chat_completion_helpers.py:378) but never set or read it anywhere — a dead, half-wired signal. The intended "forward progress" watchdog was stubbed and never finished.Fix
Wire up the progress signal and add the watchdog:
codex_runtime._on_eventnow stamps_codex_stream_last_progress_tsonly on real forward progress — text/reasoning deltas,function_callframes,response.output_item.done, terminal/errorevents — distinct from_codex_stream_last_event_ts, which any frame (including keepalives) refreshes.interruptible_api_callkills a stream that is emitting frames but making zero forward progress past the fast-reconnect cutoff (default = the no-byte TTFB cutoff, clamped strictly below the stale timer), so the retry loop reconnects in ~1s instead of waiting out the blunt stale timeout. Tunable viaHERMES_CODEX_PROGRESS_STALE_TIMEOUT_SECONDS(0disables). Close reason:codex_progress_stall_kill.The no-byte (first-byte) and stream-idle paths are unchanged; the new branch only arms once at least one event has arrived, so it never races the no-byte detector.
Verification
codex_progress_stall_kill.test_progress_stall_kills_keepalive_only_stream— keepalive-only stream is killed fast via the progress path, not the 90s stale path.test_progress_stall_does_not_kill_streaming_progress— a stream making real progress for longer than the fast cutoff is not killed.test_progress_stall_kills_keepalive_only_streamFAILS (rides to the ~90s stale timer,elapsed≫ 20s); with the fix it passes at ~42s.tests/agent/test_codex_ttfb_watchdog.pysuite: 13 passed (11 existing + 2 new), on both the fork base and upstream base.