[Bug]: A stalled text stream silently truncates the assistant message to its first delta
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
The trigger is a text stream that stalls after its first delta and never emits
content_block_stop. The SDK then delivers the finished message as a single
claude/assistant snapshot. I hit this once organically; the deterministic repro is the
event sequence itself, which is what a unit test would feed the adapter:
- Send a prompt that produces at least one assistant text message before the final one
(any turn with a tool call and a short "doing X now" line in front of it). - Have the provider emit, for the final message:
message_start,content_block_start,
onetext_delta— then nothing. - Emit the full message as one
claude/assistantsnapshot, followed byclaude/result.
Nocontent_block_stop, nomessage_delta, nomessage_stop.
Expected behavior
The snapshot is the authoritative copy of the message, so the assistant message should be
stored and rendered in full.
Actual behavior
The message is persisted as its first streamed delta only — in my case the single
character P, from a 2.4 KB answer. The rest is dropped before it ever reaches the event
store, so restarting the app does not help: the projection is rebuilt from events that
never contained the text. The full text survives only in the Claude Code transcript on
disk, which the UI does not read.
Root cause
apps/server/src/provider/Layers/ClaudeAdapter.ts has exactly the right safety net —
backfillAssistantTextBlocksFromSnapshot — but it cannot fire here, for two independent
reasons:
- It aligns snapshot text blocks against
turnState.assistantTextBlockOrderby position
from the start of the list. That list accumulates for the whole turn and is never
reset between assistant messages. My turn had one earlier text message, so position 0 of
the snapshot resolved to that earlier, already-completed block. The full text was written
onto the wrong block; the block holdingPwas never touched. - Even on the correct block it only fills
fallbackTextwhen that field is empty, and only
completes the block whenstreamClosedis already true. A stream that dies mid-message
leavesstreamClosedfalse.
claude/result then force-completes the dangling block. Its fallbackText is empty, so
item.completed carries no text at all and there is nothing left to recover from.
Worth flagging: open PR #6429 introduces backfillReasoningBlocksFromSnapshot, which copies
the same positional alignment and the same streamClosed gate for reasoning blocks. If this
is fixed only for text blocks, the same defect ships again for thinking.
Impact
Major degradation or frequent failure
Version or commit
main @ 2f486ab8, desktop app 0.0.33
Environment
macOS (Darwin 25.5), Claude provider, claude-opus-5[1m], effort high, Claude Code 2.1.227
Logs or stack traces
# provider event log, one turn, timestamps verbatim
08:59:56.924 claude/stream_event/message_start
08:59:56.924 claude/stream_event/content_block_start
08:59:56.924 claude/stream_event/content_block_delta/text_delta text: "P"
-- 3m16s of complete silence, no events of any kind --
09:03:13.279 claude/assistant full message, 2437 bytes
09:03:13.283 claude/result/success
09:03:16.022 item.completed itemType=assistant_message <- no text field at all
# resulting row in state.sqlite, projection_thread_messages
# text='P' length=1 is_streaming=0 created_at == updated_at
Workaround
None. The text is unrecoverable from the app; it exists only in the Claude Code session
transcript under ~/.claude/projects/.
Update: both fix variants are now up as PRs — pick one, they are alternatives, not a pair:
- #7142 — small (+36/−11 source): keeps positional matching, aligns snapshot blocks to the tail of the block list and lets backfill extend a block whose stream was cut. Fixes this bug only.
- #7143 — deeper (+147/−40 source): reads
message_start(previously ignored) and keys text blocks by message id + in-message index. Also fixes multi-text-block messages, consecutive-message merging, and a subagent'scontent_block_stopclosing the parent's block — and gives #6429's reasoning blocks a correct base to build on.
Both are on top of current main, each with regression tests that fail without the fix; full server suite green on both.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/server/src/provider/Layers/ClaudeAdapter.ts, focusing on backfillAssistantTextBlocksFromSnapshot and the message event handling described in the issue. Compare the alternative approaches and regression tests in PRs #7142 and #7143, then run the relevant tests and full server suite. Done means a stalled stream followed by a complete claude/assistant snapshot persists and renders the full text, including the related reasoning-block case if that approach is chosen.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 20/100