diff --git a/packages/app/src/timeline/session-stream-reducers.test.ts b/packages/app/src/timeline/session-stream-reducers.test.ts index dab7ec467..5c7d7edf2 100644 --- a/packages/app/src/timeline/session-stream-reducers.test.ts +++ b/packages/app/src/timeline/session-stream-reducers.test.ts @@ -586,6 +586,39 @@ describe("processTimelineResponse", () => { expect(assistants[0]?.timelineCursor).toEqual({ epoch: "epoch-1", seq: 101 }); }); + it("keeps a newer live assistant continuation without a provider message ID", () => { + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [makeAssistantItem("painted replica")], + currentHead: [ + { + kind: "assistant_message", + id: "assistant-live", + text: "canonical prefix continuation", + timestamp: new Date(2001), + timelineCursor: { epoch: "epoch-1", seq: 101 }, + }, + ], + isInitializing: true, + hasActiveInitDeferred: true, + hasAuthoritativeBaseline: false, + payload: { + ...baseTimelineInput.payload, + direction: "tail", + startCursor: { seq: 61 }, + endCursor: { seq: 100 }, + entries: [makeTimelineEntry(100, "canonical prefix")], + }, + }); + + const assistants = [...result.tail, ...result.head].filter( + (item) => item.kind === "assistant_message", + ); + expect(assistants).toHaveLength(1); + expect(assistants[0]?.text).toBe("canonical prefix continuation"); + expect(assistants[0]?.timelineCursor).toEqual({ epoch: "epoch-1", seq: 101 }); + }); + it("keeps a canonical live user row newer than the bootstrap page", () => { const live = processAgentStreamEvent({ ...baseStreamInput, diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index 6586ad5e7..716a767b9 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -537,13 +537,19 @@ function preserveReplacementHead( return { tail: reconciledTail, head: unreconciledHead, acknowledgedClientMessageIds: [] }; } - const isNewerContinuation = - liveAssistant.messageId !== undefined && - liveAssistant.messageId === tailAssistant.messageId && + const hasNewerCursor = liveAssistant.timelineCursor !== undefined && tailAssistant.timelineCursor !== undefined && liveAssistant.timelineCursor.epoch === tailAssistant.timelineCursor.epoch && liveAssistant.timelineCursor.seq > tailAssistant.timelineCursor.seq; + const hasMatchingProviderMessageId = + liveAssistant.messageId !== undefined && liveAssistant.messageId === tailAssistant.messageId; + const hasIdlessTextContinuation = + liveAssistant.messageId === undefined && + tailAssistant.messageId === undefined && + liveAssistant.text.startsWith(tailAssistant.text); + const isNewerContinuation = + hasNewerCursor && (hasMatchingProviderMessageId || hasIdlessTextContinuation); if (isNewerContinuation) { const text = liveAssistant.text.startsWith(tailAssistant.text) ? liveAssistant.text