From 71e83dc42fd84b45a41766fb5e136b399917b78d Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 2 May 2026 19:57:43 +0700 Subject: [PATCH] fix(app): keep live assistant continuation with hydrated tail --- .../timeline/session-stream-reducers.test.ts | 42 +++++++++++++++++++ packages/app/src/types/stream.ts | 10 +++++ 2 files changed, 52 insertions(+) diff --git a/packages/app/src/timeline/session-stream-reducers.test.ts b/packages/app/src/timeline/session-stream-reducers.test.ts index f19ff6277..abd7455aa 100644 --- a/packages/app/src/timeline/session-stream-reducers.test.ts +++ b/packages/app/src/timeline/session-stream-reducers.test.ts @@ -1099,6 +1099,48 @@ describe("processAgentStreamEvents", () => { expect(finalAssistantItems).toHaveLength(1); expect(finalAssistantItems[0]?.text).toBe(`${seq186Text}${seq187Text}`); }); + + it("does not split an assistant sentence between hydrated tail and live head", () => { + const hydrated = processTimelineResponse({ + ...baseTimelineInput, + isInitializing: true, + hasActiveInitDeferred: true, + initRequestDirection: "tail", + payload: { + agentId: "agent-1", + direction: "tail", + reset: false, + epoch: "epoch-1", + startCursor: { seq: 10 }, + endCursor: { seq: 10 }, + entries: [makeTimelineEntry(10, "Call-site API — exactly one primitive. Not")], + error: null, + hasNewer: false, + hasOlder: false, + }, + }); + + const liveContinuation = processAgentStreamEvent({ + ...baseStreamInput, + event: makeTimelineEvent(" gateValue, not filterEnum."), + seq: 11, + epoch: "epoch-1", + currentTail: hydrated.tail, + currentHead: hydrated.head, + currentCursor: hydrated.cursor ?? undefined, + timestamp: new Date("2026-05-02T10:00:00.011Z"), + }); + + const finalAssistantItems = [...liveContinuation.tail, ...liveContinuation.head].filter( + (item): item is Extract => + item.kind === "assistant_message", + ); + + expect(finalAssistantItems).toHaveLength(1); + expect(finalAssistantItems[0]?.text).toBe( + "Call-site API — exactly one primitive. Not gateValue, not filterEnum.", + ); + }); }); describe("createAgentStreamReducerQueue", () => { diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index abc861aca..350cab6d4 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -991,6 +991,16 @@ export function applyStreamEvent(params: { flushHead(); } + if (incomingKind === "assistant_message" && nextHead.length === 0) { + const tailAssistant = nextTail.at(-1); + if (tailAssistant?.kind === "assistant_message") { + nextTail = nextTail.slice(0, -1); + nextHead = [tailAssistant]; + changedTail = true; + changedHead = true; + } + } + // For streamable kinds, apply to head if (incomingKind !== null && isStreamableKind(incomingKind)) { const reduced = reduceStreamUpdate(nextHead, event, timestamp, { source });