Skip to content

fix(server): key assistant text blocks by message id instead of stream position - #7143

Open
Kiri110K wants to merge 3 commits into
pingdotgg:mainfrom
Kiri110K:fix/claude-adapter-message-scoped-blocks
Open

fix(server): key assistant text blocks by message id instead of stream position#7143
Kiri110K wants to merge 3 commits into
pingdotgg:mainfrom
Kiri110K:fix/claude-adapter-message-scoped-blocks

Conversation

@Kiri110K

@Kiri110K Kiri110K commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

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

ClaudeAdapter tracked assistant text blocks in one turn-long list and matched snapshot text to blocks by list position. A claude/assistant snapshot 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

  • A stalled stream truncates the message to its first delta — the incident in [Bug]: A stalled text stream silently truncates the assistant message to its first delta #7137: one text_delta (P), 3m16s of silence, no content_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.
  • Multi-text-block messages (text → tool call → text) could backfill the wrong block, since position-in-turn and position-in-message disagree.
  • Two consecutive messages merged into one when the second message restarted block indexes at zero while the first was still open.
  • A subagent's content_block_stop closed 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

  • Four new regression tests, one per defect above, each replaying the SDK event sequence; all four fail without the fix and pass with it.
  • Full server suite: 230 files passed, 2510 tests passed, 7 skipped, on top of current main.

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_startcurrentAssistantMessageId) 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 on content_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 ClaudeAdapter to key assistant text blocks by message id instead of stream position

  • Replaces the turn-wide synthetic negative block-index counter with the assistant API message identifier captured from message-start events, so blocks from different messages with the same content index stay separate
  • Snapshot completion now compares snapshot fallback text against accumulated streamed text and emits only the missing suffix instead of replaying the already-delivered prefix
  • Subagent content_block_stop frames carrying a parent tool-use identifier can no longer close a parent assistant text block; only the parent's own stop frame completes it
  • Id-less streamed blocks fall back to compatible-block reuse, but reject a second snapshot for an already-snapshotted block and synthesize new runtime items for unmatched snapshot fragments
  • Risk: ensureAssistantTextBlock and completeAssistantTextBlock in ClaudeAdapter.ts change block lookup and completion semantics; existing consumers relying on synthetic negative indexes or full-prefix replay will break

Macroscope summarized 27530a1.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7622e3ef-7768-483a-9103-5b59798ba62c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@IcTxDiogo

Copy link
Copy Markdown

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:

message_start
content_block_start(index=0, type=text)
[~27s, no text_delta/content_block_stop/message_stop]
claude/assistant   # complete 1666-char snapshot
claude/result/success  # same 1666 chars

The complete text is present in T3's raw provider event log, but the completed turn has no assistant message linked in the local state.sqlite projection.

Following the current 0.0.33 adapter logic, snapshot text position 0 can resolve against the turn-wide assistantTextBlockOrder[0], which in this turn belongs to an earlier already-completed assistant message. That is consistent with the final snapshot being swallowed instead of producing the final assistant item.

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.

Kiri110K and others added 2 commits September 3, 2026 16:19
…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>
@Kiri110K
Kiri110K force-pushed the fix/claude-adapter-message-scoped-blocks branch from d14de2d to b3ead4b Compare September 3, 2026 08:33
@Kiri110K

Kiri110K commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (b3ead4b). Conflict was only in ClaudeTurnState: kept main's new latestAssistantUsage / compactedSinceLatestAssistantUsage fields alongside currentAssistantMessageId, and dropped nextSyntheticAssistantBlockIndex as this PR already did. The stray pnpm-lock.yaml hunk (two deprecated: annotations from a local install) is gone; the diff is now the adapter and its tests only. ClaudeAdapter.test.ts: 83/83, server typecheck clean.

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts
Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: A stalled text stream silently truncates the assistant message to its first delta

2 participants