Skip to content

fix(claude,web): surface assistant content lost to a mid-stream API retry#4

Open
Antisophy wants to merge 3 commits into
CyberShadow:masterfrom
Antisophy:fix/assistant-content-after-api-retry
Open

fix(claude,web): surface assistant content lost to a mid-stream API retry#4
Antisophy wants to merge 3 commits into
CyberShadow:masterfrom
Antisophy:fix/assistant-content-after-api-retry

Conversation

@Antisophy

Copy link
Copy Markdown
Contributor

Problem

When the Anthropic API retries a request mid-stream, the Claude CLI can stop emitting stream_event partials for the rest of that run. cydo builds the visible message only from those content_block_* stream events, so when they stop, the assistant turn can end up rendering as nothing, leaving the reply only inside the session-complete result block, which itself shows collapsed as a bare checkmark divider, leaving discovery of the full response very unlikely. When it happened to me, the only way I discovered that I actually was receiving responses was through a desktop notification I was lucky to catch when cydo wasn't focused, leading me to click around to try to find the response I'd seen in the notification and finally expand the checkmark divider, which I'd previously dismissed as uninteractive.

Fix

Two commits, the cause and a safety net:

  • fix(claude): promote assistant content when stream events are missing: when a complete assistant event arrives with no active stream blocks, promote its content blocks straight to item/started + item/completed (the same way the sub-agent path already does) so the turn renders. Promoted item ids use a session-wide counter (these per-block events carry no stream index), and an api_retry system event resets stream tracking so a half-opened block from the aborted attempt can't leave stale state that suppresses the fallback.
  • fix(web): keep session result expanded when its text never rendered: the result event always carries a copy of the reply; when that copy is the only surviving record, compare it to the last main-turn assistant message and, if it isn't already on screen, start the result block expanded (like error results already do) instead of a bare checkmark.

The result event always carries a copy of the final reply text,
normally redundant with the streamed assistant message. When the turn's
content is lost (e.g. the CLI stops emitting stream events after API
retries), that copy is the only surviving record of the reply, yet it
rendered collapsed as a bare checkmark divider, indistinguishable from
"no response".

Compare the result text against the last main-turn assistant message
when the result arrives; if it isn't already on screen, flag it and
start the result block expanded, like error results already do.
After an API retry, Claude CLI stops emitting
stream_event partials for the rest of the process run. Live translation
promoted content to visible items only from content_block_* stream
events, treating complete assistant events as metadata-only turn/delta,
so entire turns rendered as nothing; the final reply text survived only
inside the collapsed session-complete result block.

When an assistant event arrives and no stream blocks are active, promote
its content blocks directly (item/started + item/completed), the same
way the sub-agent path already does. Generated item ids use a
session-wide counter since per-block assistant events carry no stream
index. Also reset stream tracking when an api_retry system event passes
through, so a mid-stream abort can't leave stale state that suppresses
the fallback.
@Antisophy Antisophy force-pushed the fix/assistant-content-after-api-retry branch 2 times, most recently from 9435ab9 to a9393c2 Compare June 25, 2026 03:56
translateAssistantLive lives on the process-spawning ClaudeCodeSession, so the promotion fallback cannot be driven from a unit test directly. Extract the per-block promotion into a pure static promoteContentBlocks (hoisting the ClaudeBlock parse struct to class scope) and unit-test that a stream-less text block becomes item/started + item/completed and a tool_use block keeps its own id. No behavior change.
@Antisophy Antisophy force-pushed the fix/assistant-content-after-api-retry branch from a9393c2 to 60ac1d2 Compare June 25, 2026 04:06
@Antisophy

Copy link
Copy Markdown
Contributor Author

Split the backend change into two commits so the behavior fix is reviewable on its own: fix(claude): promote assistant content... is just the fallback plus the api_retry reset, and the following test(claude): unit-test assistant content promotion extracts the per-block promotion into a pure static promoteContentBlocks and adds the unit test. (The web commit already carried its own vitest coverage.)

Reason for the extraction: the promotion lives on ClaudeCodeSession, whose constructor spawns the claude process, so translateAssistantLive can't be driven from a unit test directly. Moving the per-block loop into a pure function (and hoisting the ClaudeBlock parse struct to class scope) makes it testable: a stream-less text block must become item/started + item/completed, and a tool_use block must keep its own id. The extraction is behavior-preserving; translateAssistantLive just calls the helper and emits the results.

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