Skip to content

fix(stream): don't retry chunk timeouts after text has committed - #552

Merged
yogthos merged 2 commits into
mainfrom
fix/duplicate-response-after-stream-timeout
Jun 30, 2026
Merged

fix(stream): don't retry chunk timeouts after text has committed#552
yogthos merged 2 commits into
mainfrom
fix/duplicate-response-after-stream-timeout

Conversation

@yogthos

@yogthos yogthos commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

Assistant responses started rendering twice. A response would stream partway, the runtime notices would interleave (compaction / repair / retry banner), then the entire response would re-stream a second time. The first copy is cut off mid-sentence at the stall point — the tell-tale signature.

Root cause

#547 made stream-chunk timeouts retryable even after content had committed (retry.rs):

let retryable = policy.should_retry(attempts, kind)
    && (!committed || is_timeout_error(error));

The premise was that StreamEvent::Retry makes the consumer discard the partial. But the consumer reset (stream.rs PROV-5) only clears the message history — it does not touch text already rendered to the UI. So when attempt 2 re-streamed from scratch, its Token deltas appended right after attempt 1's already-rendered text, producing the duplicate.

Fix

Gate retry on !committed again:

let retryable = policy.should_retry(attempts, kind) && !committed;

The #545 mid-assembly-stall case is preserved: a timeout before any text commits (e.g. a lone tool-call fragment, which is_content_delta does not count as committed) still retries. Only timeouts after committed text now surface instead of duplicating.

Also corrects the now-inaccurate "retried automatically" wording in the timeout error message (rig_stream.rs) and docs/config.md, which only holds for the pre-commit case.

Testing

  • Replaced fix(stream): retry mid-assembly chunk timeouts instead of halting #547's retries_timeout_after_content_committed test (which encoded the buggy behavior) with surfaces_timeout_after_content_committed — asserts a post-commit timeout surfaces (counter stays 1, no Retry event, stream ends on Error). Confirmed it fails on the pre-fix code and passes after.
  • Added retries_timeout_before_content_committed as a regression guard for the Long Running Tasks: error: stream chunk timed out #545 case — a tool-call-only stream that times out still retries.
  • cargo clippy --bin dirge clean, cargo fmt --check clean.
  • Full agent::agent_loop::{retry,stream,rig_stream} suites green (48 tests), including the stall-nudge tests.

Closes the regression introduced in #547.

Yogthos added 2 commits June 30, 2026 15:03
#547 made stream-chunk timeouts retryable even once content had
committed, on the assumption that StreamEvent::Retry makes the consumer
discard the partial. But that reset (stream.rs PROV-5) only clears the
message history — it doesn't touch text already rendered to the UI. So
attempt 2 re-streamed the whole response and appended a second copy,
producing the duplicate output users saw when a provider stalled
mid-response.

Gate retry on !committed again. The #545 mid-assembly-stall case (a
timeout before any text commits, e.g. a lone tool-call fragment) still
retries; only timeouts after committed text now surface instead of
duplicating.

Also corrects the now-inaccurate "retried automatically" wording in the
timeout error message and docs/config.md, which only holds for the
pre-commit case.
The stall-recovery nudge ("you may have been stuck in a long reasoning
loop") fired for any pre-commit timeout. For a plain stream-chunk
timeout — "provider stalled or connection silently dropped", no tool
call open — that blame is mis-attributed: it's a transport drop, not a
reasoning loop, and the retry should just re-run clean.

Narrow is_stall_timeout to exclude the "connection silently dropped"
message. Request-level and mid-assembly timeouts (the real stall cases)
still nudge.
@yogthos
yogthos merged commit 30e57fb into main Jun 30, 2026
13 checks passed
yogthos pushed a commit that referenced this pull request Jun 30, 2026
[AGENTS] left-panel box listing running subagents (id_short disambiguates
same-profile rows), slash commands unified to a single source of truth,
critic scoped/disabled for read-only and design prompts, and the 0.14.0
(#547) duplicate-output regression fixed by gating stream-chunk retry on
!committed again. Plus a README note on building with newer libclang.
(#548, #549, #552, #553, #554)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant