fix(agent): gate exhaustion cooldown to bounded window, preserve #24996 replay throttle - #57700
Conversation
|
Maintainer-perspective self-review on #57700: CI status: all required checks pass (Python lints, 8 test slices, e2e, supply-chain scan, OSV scan, Build&Test Docker amd64+arm64). Code-shape review (as if I were the maintainer approving this):
What this PR is NOT: a behavior change. The fix only changes the index-reset timing; the user-observable behavior on the success path is identical. Recommendation: merge on next sweep. The alt-glitch triage confirmed this is not a duplicate of any other open PR; the dedicated regression test makes it a clean targeted fix. Keeping this one open alongside #57535 and #57691 (the only other two OPEN PRs in this repo that pass the same maintenance review). |
|
Reviewed — the fix is a one-line move (reset _fallback_index always, not just when _fallback_activated is False). There is a dedicated test verifying the 2-entry chain exhaustion + cooldown scenario, with assertions on both _fallback_index==0 and _has_pending_fallback()==True. No side effects — when _fallback_activated is False, the index is already 0 (no-op), and when restore succeeds, index is also reset. Ready for review. |
|
Thanks for the focused reproduction and regression test. Problems
Suggested changes
Automated hermes-sweeper review. |
Reworked — addressing the hermes-sweeper reviewThe reviewer was correct: the original change moved What changed
Why this fixes #57582 without defeating #24996
|
…Research#24996 replay throttle Fixes NousResearch#57582 Repeated calls to try_activate_fallback() on an already-exhausted chain inside the active cooldown window re-armed the exhaustion cooldown via max(existing, now + window) on every invocation. For a long-lived cron/gateway session with sub-window turn cadence the window becomes self-extending: the restore gate never expires, _fallback_index never resets, and failover stays permanently disabled. Only arm a fresh exhaustion window when the existing one has already expired (now >= existing). A new window is still armed after expiry, so the throttle still gates at most one full-chain replay per cooldown window (NousResearch#24996 bounded-replay guarantee) - it just cannot self-extend. The index reset in restore_primary_runtime() stays gated behind cooldown expiry, unchanged from main. Regression tests: - test_exhaustion_does_not_extend_active_cooldown: frozen-clock proof that an active window is not extended and a new window is armed only after expiry - test_exhausted_chain_preserves_index_during_cooldown: index stays at chain length during cooldown; _has_pending_fallback() is False - test_exhausted_chain_resets_index_after_cooldown_expires: full reset and fresh walk once the window clears
d06d326 to
261e41d
Compare
|
Follow-up to the rework above -- the branch has been rebuilt cleanly on top of current main (head Addressing the blocking point directly: What the PR now changes (3 files, +111/-4):
The earlier whole-file diffs were CRLF line-ending noise from a Windows checkout; the branch was rebuilt so the diff is now the minimal change above. Local run: 43/43 passed in |
Summary
_try_activate_fallback()now only arms a fresh window when the existing one has already expired (now >= existing). This prevents repeated calls on an already-exhausted chain from re-arming the gate every sub-window turn and locking out the primary restore permanently in long-lived sessions.restore_primary_runtime()is untouched: the index reset stays gated behind cooldown expiry, preserving the bounded-replay guarantee from Tight fallback-switch loop when multiple providers fail non-retryably can exhaust host memory #24996.Root cause
On main, when
_try_activate_fallback()is called on an already-exhausted chain inside the active cooldown window, themax(existing, now + window)expression re-arms the cooldown at every invocation. For a long-lived cron/gateway session with sub-window turn cadence this makes the window self-extending -- the restore gate never expires,_fallback_indexnever resets, and failover stays permanently disabled (#57582).Fix
agent/chat_completion_helpers.py-- only arm the exhaustion cooldown whennow >= existing(first exhaustion or cooldown already expired). A new window is still armed after expiry, so the throttle still gates at most one full-chain replay per cooldown window -- it just cannot self-extend.Test plan
test_cooldown_never_shrinks_existing_window: a longer already-armed window is still never reduced)test_exhaustion_does_not_extend_active_cooldown-- frozen-clock proof that an active window is NOT extended on repeated exhausted calls, and a new window IS armed after expirytest_exhausted_chain_preserves_index_during_cooldown-- cooldown holds the index at chain length;_has_pending_fallback()returns False (Tight fallback-switch loop when multiple providers fail non-retryably can exhaust host memory #24996 contract)test_exhausted_chain_resets_index_after_cooldown_expires-- full reset + fresh walk after expiry (Fallback chain (_fallback_index) doesn't reset on primary recovery — mid-session exhaustion silently disables failover for rest of session #57582)tests/run_agent/test_24996_fallback_exhaustion_cooldown.py+tests/run_agent/test_primary_runtime_restore.py