Skip to content

fix(streaming): add jittered exponential backoff between stream retries - #65832

Closed
drleadflow wants to merge 1 commit into
NousResearch:mainfrom
drleadflow:fix/stream-retry-backoff
Closed

fix(streaming): add jittered exponential backoff between stream retries#65832
drleadflow wants to merge 1 commit into
NousResearch:mainfrom
drleadflow:fix/stream-retry-backoff

Conversation

@drleadflow

Copy link
Copy Markdown
Contributor

Bug

The streaming retry loop in interruptible_streaming_api_call (agent/chat_completion_helpers.py, loop starting at line 2805 on current main) retries transient stream failures with zero delay. Both retry points — the mid-tool-call silent retry (continue at line 2955) and the plain transient-error retry (continue at 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=2 the 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 existing continue statements. It:

  • computes the delay via the existing 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;
  • sleeps in 0.2s slices, bailing out immediately if agent._interrupt_requested or the request has been cancelled, so /stop stays 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.py
  • tests/run_agent/test_stream_interrupt_retry.py
  • tests/run_agent/test_stream_stale_breaker_reset.py
  • tests/run_agent/test_partial_stream_finish_reason.py
  • tests/agent/test_bedrock_interrupt_post_worker.py
  • tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py

75 passed, 0 failed. The interrupt-retry suite in particular confirms /stop responsiveness 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 /stop responsive by sleeping in 0.2s slices with interrupt/cancel checks. Happy to close in favor of either if maintainers prefer.

🤖 Generated with Claude Code

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>
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Jul 16, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Adds jittered exponential backoff to stream retries — a solid resilience improvement. No security concerns.


Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-2084 dispatches Codex streaming to _run_codex_stream, whose connection and mid-stream retry branches immediately continue at agent/codex_runtime.py:1226 and :1282.
  • The PR has no regression test for the new wait behavior. tests/run_agent/test_stream_interrupt_retry.py:45-76 covers 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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit area/streaming Streaming responses: gateway delivery, provider wire labels Jul 18, 2026
@drleadflow drleadflow closed this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/streaming Streaming responses: gateway delivery, provider wire comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Stream retry system has no exponential backoff, misses chat_completions parse errors, and breaks stale-detect reconnect

4 participants