Skip to content

fix(claude): keep a streamed reply on one message when message_start is lost - #77

Merged
Yevanchen merged 1 commit into
mainfrom
fix/claude-stream-burst-anchoring
Jul 24, 2026
Merged

fix(claude): keep a streamed reply on one message when message_start is lost#77
Yevanchen merged 1 commit into
mainfrom
fix/claude-stream-burst-anchoring

Conversation

@Yevanchen

Copy link
Copy Markdown
Collaborator

Summary

  • Anchor a stream scope to a synthetic burst key on the first content-bearing frame, so every frame of a streamed burst resolves to the same assistant message even when the message_start frame was lost. Previously each content_block_delta fell back to its own envelope uuid and minted a separate message.
  • Let the aggregated assistant envelope consume an unconfirmed burst anchor (live or parked at message_stop) so it binds to the streamed message instead of re-emitting the full text as a new message; replayed envelopes stay bound via a native-id alias. Confirmed anchors from message_start keep today's semantics: an envelope with a different native id remains a distinct message, so final-message binding is unchanged.

Why

  • YEF-884: a "ping" reply rendered as three assistant messages — 'P', 'ong. What would you like to work on?', and the duplicated full text — in both the session event log and the live view. Captured the real wire with Claude Code CLI 2.1.170 (--output-format stream-json --include-partial-messages): every envelope carries a fresh uuid (including each delta), and the aggregated assistant envelope arrives with the native message id before message_stop. Once message_start is missed (prewarm attach race), uuid-keyed identity fractures the reply exactly as reported. fix(claude): key assistant messages by API message id during streaming #52 keyed messages by the API message id when message_start is present; this closes the remaining fallback gap.
  • The read-time repair for already-persisted rows lands separately in langgenius/mosoo (fold supersede); this change stops new fractured events at the source.

Verification

  • bun test (1074 pass; the one failing ACP file-system test also fails on a clean checkout in this environment), bun run tc, bun run lint, bun run fmt.
  • New regression test replays the captured YEF-884 wire shape (start-less per-uuid frames, envelope before message_stop) and asserts one message id across started/delta/snapshot/completed plus correct final-message binding.
  • Updated the two degraded-stream tests that encoded uuid-as-identity: parallel tool blocks are isolated by content-block index within one burst, thought streams by message boundary — matching the real wire, where envelope uuids are per-frame and cannot identify a message.

Risk

  • Behavior changes only for degraded streams (missing message_start); healthy streams resolve identically to today. Roll back by reverting the single commit.

…is lost

Stream envelopes carry a fresh uuid per frame, so once the
message_start frame is missed (prewarm attach race) every text delta
minted its own assistant message and the aggregated assistant envelope
re-emitted the full text as yet another message. One reply rendered as
'P' / 'ong. ...' / duplicate full-text messages (YEF-884).

Anchor the scope to a synthetic burst key on the first content frame so
all frames of the burst share one message, and let the aggregated
assistant envelope consume an unconfirmed anchor instead of minting a
duplicate. Confirmed anchors from message_start keep today's semantics:
an envelope with a different native id stays a distinct message.

@Yevanchen Yevanchen left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean-context review (agent, full production-evidence trace of session 01KY9Z5JEVPQP3F548XEK21MEG seq 36-43): APPROVE-WITH-NOTES.

  • Mechanism confirmed: pre-PR, start-less stream_event frames fell back to per-frame envelope uuid → new messageId per frame → N× message.started + trailing full-text delta on a third message — exactly the production fracture shape. The anchor (anchorStreamingNativeMessageId on first content frame, alias-bound assistant envelope via resolveAssistantMessageNativeId) repairs identity at the root instead of adding a second dedup; frame-count-invariant so it covers N restarts.
  • Trailing full-text duplicate: suppressed by the pre-existing hasStreamedText dedup once identity is unified; full text still lands as message.added snapshot on the same messageId (healthy-path parity).
  • Edge cases checked: thinking blocks, mid-reply tool_use (toolCallId lookups now hit under degraded streams), two genuine messages not merged (confirmed-anchor semantics preserved + regression test), per-turn state reset.
  • Notes (no change requested): (1) per-block envelopes on multi-block degraded bursts can split stream-frame parenting (text stays alias-bound); (2) a late burst-1 envelope after burst-2's first frame would merge bursts — impossible under the stated lost-prefix attach-race model. File as follow-ups only if telemetry shows multi-block degraded bursts.
  • New transcript fixture replays the exact production topology; CI green; MERGEABLE/CLEAN, no rebase needed.
  • Scope caveat: heals new events only; already-persisted fractured rows need the mosoo-side read-time fold (langgenius/mosoo#388 family).

Merging as-is.

@Yevanchen
Yevanchen merged commit 9d4415d into main Jul 24, 2026
2 checks passed
@Yevanchen
Yevanchen deleted the fix/claude-stream-burst-anchoring branch July 24, 2026 12:09
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