fix(streaming): add jittered exponential backoff between stream retries - #65832
Closed
drleadflow wants to merge 1 commit into
Closed
fix(streaming): add jittered exponential backoff between stream retries#65832drleadflow wants to merge 1 commit into
drleadflow wants to merge 1 commit into
Conversation
The streaming retry loop reconnected back-to-back with zero delay on transient failures (timeouts, connection drops, SSE parse errors). During a provider outage this hammers the endpoint with immediate reconnects and compounds rate-limit exposure — every retry lands while the provider is still down or throttling. Reuse the existing agent.retry_utils.jittered_backoff helper (the same policy the conversation loop uses) before both retry points: the mid-tool-call silent retry and the plain transient-error retry. The backoff sleeps in 0.2s slices and bails out early on interrupt or request cancellation, so /stop stays as responsive as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tonydwb
reviewed
Jul 16, 2026
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Adds jittered exponential backoff to stream retries — a solid resilience improvement. No security concerns.
Reviewed by Hermes Agent
teknium1
reviewed
Jul 18, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for the focused retry-loop improvement. The zero-delay retry premise is confirmed on current main at agent/chat_completion_helpers.py:3025 and :3091.
Problems
- The same retry class remains in the Codex Responses stream path:
agent/chat_completion_helpers.py:2077-2084dispatches Codex streaming to_run_codex_stream, whose connection and mid-stream retry branches immediately continue atagent/codex_runtime.py:1226and:1282. - The PR has no regression test for the new wait behavior.
tests/run_agent/test_stream_interrupt_retry.py:45-76covers an interrupt set before retry, not interruption while a backoff is in progress.
Suggested changes
- Cover the Codex Responses retry branches too, or narrow the stated scope to the two retry paths changed here.
- Add deterministic coverage for nonzero/increasing delays and interruption during the sliced wait.
Automated hermes-sweeper review.
|
|
||
| _max_stream_retries = env_int("HERMES_STREAM_RETRIES", 2) | ||
|
|
||
| def _backoff_before_retry(_attempt: int) -> None: |
Contributor
There was a problem hiding this comment.
Please add deterministic coverage for this helper: assert retry delays are requested and that setting _interrupt_requested during the sliced wait prevents the next stream request. The existing interrupt test covers a flag set before retry, not this new wait path.
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.
Bug
The streaming retry loop in
interruptible_streaming_api_call(agent/chat_completion_helpers.py, loop starting at line 2805 on currentmain) retries transient stream failures with zero delay. Both retry points — the mid-tool-call silent retry (continueat line 2955) and the plain transient-error retry (continueat line 3021) — reconnect back-to-back immediately.Failure scenario
During a provider outage or throttling window, every reconnect lands while the provider is still down: with
HERMES_STREAM_RETRIES=2the agent burns all retries within milliseconds and surfaces a hard failure to the user, when waiting 1–3 seconds would have succeeded. Against a rate-limiting provider, instant reconnects compound the 429 pressure (each doomed retry is another counted request), making the throttling worse and — with credential pools — accelerating pool exhaustion.Fix
Add a
_backoff_before_retry(attempt)helper inside_call()and invoke it immediately before each of the two existingcontinuestatements. It:agent.retry_utils.jittered_backoff(attempt + 1, base_delay=1.0, max_delay=15.0)— no new backoff policy, the same one the conversation loop uses;agent._interrupt_requestedor the request has been cancelled, so/stopstays exactly as responsive as before.Additions only (+23, no lines removed or moved), so it composes trivially with the other churn in this file and cannot change behavior on the success path or the retries-exhausted path.
Testing
Verified against the repo's own streaming test suites (all pass with the patch applied):
tests/run_agent/test_streaming.pytests/run_agent/test_stream_interrupt_retry.pytests/run_agent/test_stream_stale_breaker_reset.pytests/run_agent/test_partial_stream_finish_reason.pytests/agent/test_bedrock_interrupt_post_worker.pytests/run_agent/test_28161_anthropic_stream_pool_cleanup.py75 passed, 0 failed. The interrupt-retry suite in particular confirms
/stopresponsiveness is unaffected.Honest note: no new regression test is included. A good one would stub the stream to fail twice, patch
time.sleep/time.monotonic, and assert a nonzero, increasing delay is requested between attempts — and that setting the interrupt flag mid-backoff returns immediately. Say so in the PR and offer to add it.Scope
agent/chat_completion_helpers.py— 1 file, +23 / -0 (one helper, two one-line call sites).Related
Fixes #60029. I'm aware of the existing PRs #60299 and #60031 for the same issue — offering this as a narrower alternative: it reuses the repo's existing
agent.retry_utils.jittered_backoff(the same policy the conversation loop already uses) rather than introducing new backoff code, and keeps/stopresponsive by sleeping in 0.2s slices with interrupt/cancel checks. Happy to close in favor of either if maintainers prefer.🤖 Generated with Claude Code