pingdotgg / pingdotgg/t3code

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

Open
#7,137 1 comment 0 reactions 0 assignees View on GitHub

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:

  1. 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).
  2. Have the provider emit, for the final message: message_start, content_block_start,
    one text_delta — then nothing.
  3. Emit the full message as one claude/assistant snapshot, followed by claude/result.
    No content_block_stop, no message_delta, no message_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:

  1. It aligns snapshot text blocks against turnState.assistantTextBlockOrder by 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 holding P was never touched.
  2. Even on the correct block it only fills fallbackText when that field is empty, and only
    completes the block when streamClosed is already true. A stream that dies mid-message
    leaves streamClosed false.

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's content_block_stop closing 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.