fix(server): key assistant text blocks by message id instead of stream position - #7143
fix(server): key assistant text blocks by message id instead of stream position#7143Kiri110K wants to merge 3 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR changes the production Claude streaming state machine, including message identity, snapshot reconciliation, completion timing, and subagent event handling. Although regression tests cover several scenarios, the interaction of these stateful paths and their user-visible impact warrants human review. You can add or adjust custom eligibility rules. Learn more. |
|
I traced a production incident against T3 0.0.33 and it appears to be a direct real-world instance of the snapshot-only regression covered by this PR. The turn had many previously completed assistant messages. Its final message did: The complete text is present in T3's raw provider event log, but the completed turn has no assistant message linked in the local Following the current 0.0.33 adapter logic, snapshot text position 0 can resolve against the turn-wide The regression case described here as a snapshot-only second message — specifically avoiding claiming an already-completed id-less block — matches this incident very closely. The message-scoped matching in #7143 would prevent the completed earlier block from claiming this snapshot and should either find the actual pending block or synthesize the snapshot-only message. I found a second highly similar incident as well (79-char final snapshot after a long tool-heavy turn), although I did not re-verify every block-index/canonical-event detail of that one as exhaustively as the first. This may be useful as confirmation that the snapshot-only regression is occurring in real T3 0.0.33 sessions, not only in the synthetic regression test. |
…m position The adapter tracked assistant text blocks in one turn-long list and matched snapshot text to blocks by list position. Any turn with more than one assistant message could mismatch: a claude/assistant snapshot describes one message, not the whole turn. Read message_start (previously ignored) and key each text block by its message id plus the block index within that message. Snapshot backfill now only touches its own message's blocks, and a block whose stream was cut mid-message is completed from the snapshot when the snapshot text extends the streamed prefix. This fixes four defects with one root cause: - a stalled stream truncating the message to its first delta (pingdotgg#7137) - snapshots landing on the wrong block in multi-text-block messages - two consecutive messages merging when block indexes restart at zero - a subagent content_block_stop closing the parent's open block Fixes pingdotgg#7137 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t-only message Macroscope on the PR found that the id-less fallback in backfillAssistantTextBlocksFromSnapshot could resolve to a COMPLETED block at the same content index; completionEmitted then skips the message entirely, so an assistant message that never streamed loses its text. Exclude completed blocks from the id-less fallback unless their delivered text matches the snapshot — the bare exclusion Macroscope suggested would instead re-synthesize the ordinary stream -> stop -> snapshot flow as a duplicate whenever message ids are absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d14de2d to
b3ead4b
Compare
|
Rebased onto current main (b3ead4b). Conflict was only in |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b3ead4b. Configure here.
…ess block got its own snapshot Without message_start, the backfill fallback claimed any still-open block at the snapshot's content index. A block whose stream stalled and whose own snapshot had already been applied would then claim a later snapshot-only message at the same index; its text did not continue the stalled block, so it was logged as snapshot-diverged and dropped. A message delivers exactly one snapshot: once a block has its own, a second, different snapshot at that index is a new message and is now synthesized instead. Late stream deltas can still continue the block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Fixes #7137. This was the deeper of two alternative fixes; the smaller positional-repair one (#7142) was closed on 2026-08-28 during the backlog cleanup, with this PR kept open as the review path for incomplete Claude assistant text. Rebased onto current main on 2026-09-03; the diff is the adapter and its tests only.
What changed
ClaudeAdaptertracked assistant text blocks in one turn-long list and matched snapshot text to blocks by list position. Aclaude/assistantsnapshot describes one message, not the whole turn, so any turn with more than one assistant message could mismatch.The adapter now reads
message_start— previously ignored entirely — and keys every text block by its message id plus the block index within that message. Snapshot backfill resolves blocks of its own message only. A block whose stream was cut mid-message is completed from the snapshot when the snapshot text extends the streamed prefix; a snapshot that contradicts already-shown text is never written over it (warning logged instead).Why this fixes four defects with one root cause
text_delta(P), 3m16s of silence, nocontent_block_stop, full 2.4 KB snapshot at the end. Positional matching wrote the snapshot onto an earlier completed block and the real block was force-completed empty.content_block_stopclosed the parent's open block, truncating the parent message mid-stream.Worth noting for review: #6429 (thinking blocks in timelines, now closed) copied the positional scheme into a new
backfillReasoningBlocksFromSnapshot. Any future reasoning-block backfill should build on the message-scoped base from this PR instead of inheriting the same truncation bug.Tests
Diff is +147/−40 source; the rest is tests.
🤖 Generated with Claude Code
Note
Medium Risk
Changes live streaming and snapshot reconciliation for Claude assistant output—user-visible message text—with nuanced id-less and subagent paths, though behavior is heavily covered by new regression tests.
Overview
Fixes incomplete or merged Claude assistant text by scoping text blocks to each API message (
message_start→currentAssistantMessageId) plus content-block index, instead of matching snapshots to turn-wide block order.Snapshot backfill now finds the right block for that message (with fallbacks when ids are missing), materializes snapshot-only messages without stealing completed blocks, and on completion emits only the unseen suffix of snapshot text relative to deltas already sent (
streamedText). Subagent stream events no longer overwrite the parent message id or close the parent’s text block oncontent_block_stop.Adds integration tests for stalled-stream repair, multi-block messages, reused block indexes across messages, subagent isolation, and id-less / snapshot-only edge cases.
Reviewed by Cursor Bugbot for commit 27530a1. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix
ClaudeAdapterto key assistant text blocks by message id instead of stream positionmessage-startevents, so blocks from different messages with the same content index stay separatecontent_block_stopframes carrying a parent tool-use identifier can no longer close a parent assistant text block; only the parent's own stop frame completes itensureAssistantTextBlockandcompleteAssistantTextBlockin ClaudeAdapter.ts change block lookup and completion semantics; existing consumers relying on synthetic negative indexes or full-prefix replay will breakMacroscope summarized 27530a1.