fix(stream): preserve per-segment ids so assistant parts keep chronological order#164
Merged
Merged
Conversation
…ogical order normalizePersistedPart carries id/partId through for text and reasoning parts (absent id = legacy single-segment flow, unchanged). finalize gains a per-id contract: concat(text segments) must equal the final text — equal → untouched, extended → id-less remainder segment appended, diverged → collapse to one authoritative segment at the last text position. Id-less flows are byte-identical to the previous behavior.
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.
Problem
normalizePersistedPartstripsidfrom text/reasoning parts, sogetPartKeycollapses every segment totext:current/reasoning:current: all reasoning merges into one part, all text into one part, and interleaving (reasoning → tool → reasoning → text) is destroyed at persist time.finalizeAssistantPartsthen stamps the full final text into every text part.Harness adapters already emit stable per-segment part ids (OpenCode native ids; claude-code streaming path assigns a UUID per thinking block) — the fidelity is thrown away here.
Change
normalizePersistedPart: text/reasoning parts carryid(orpartId) through when present. Absent id = legacy single-segment flow, unchanged — an id is never invented.mergePersistedPart: an empty text snapshot no longer erases accumulated text (now matches the reasoning branch).finalizeAssistantPartsgains a per-id contract with the invariant concat(text segments) === final text:content/post-processing consume — it must win, while reasoning/tool chronology survives)Back-compat
Id-less flows (creative-agent call sites, old persisted rows, router-style text streams) behave exactly as before — covered by dedicated tests.
Tests
New
tests/stream-normalizer.test.ts: id preservation/no-invention, per-id keying vs*:currentfallback, per-id merge semantics, all finalize branches, and end-to-end order integrity forreasoning → tool → reasoning → text.Context
Foundation for tangle-network/gtm-agent#448 (chronological chat transcript); gtm consumes this via the auto-published patch bump. Note: will conflict textually with
fix/dangling-tool-terminalizationif that lands too — whichever merges second needs a trivial rebase (the terminalize wrapping is tool-only, orthogonal to these branches).