fix(claude): keep a streamed reply on one message when message_start is lost - #77
Merged
Merged
Conversation
…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
commented
Jul 24, 2026
Yevanchen
left a comment
Collaborator
Author
There was a problem hiding this comment.
Clean-context review (agent, full production-evidence trace of session 01KY9Z5JEVPQP3F548XEK21MEG seq 36-43): APPROVE-WITH-NOTES.
- Mechanism confirmed: pre-PR, start-less
stream_eventframes 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 (anchorStreamingNativeMessageIdon first content frame, alias-bound assistant envelope viaresolveAssistantMessageNativeId) 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
hasStreamedTextdedup once identity is unified; full text still lands asmessage.addedsnapshot on the same messageId (healthy-path parity). - Edge cases checked: thinking blocks, mid-reply tool_use (
toolCallIdlookups 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
message_startframe was lost. Previously eachcontent_block_deltafell back to its own envelope uuid and minted a separate message.assistantenvelope consume an unconfirmed burst anchor (live or parked atmessage_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 frommessage_startkeep today's semantics: an envelope with a different native id remains a distinct message, so final-message binding is unchanged.Why
'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 beforemessage_stop. Oncemessage_startis 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 whenmessage_startis present; this closes the remaining fallback gap.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.message_stop) and asserts one message id across started/delta/snapshot/completed plus correct final-message binding.Risk
message_start); healthy streams resolve identically to today. Roll back by reverting the single commit.