diff --git a/packages/app/src/contexts/session-stream-reducers.ts b/packages/app/src/contexts/session-stream-reducers.ts index 6623fba94..c7fb61e51 100644 --- a/packages/app/src/contexts/session-stream-reducers.ts +++ b/packages/app/src/contexts/session-stream-reducers.ts @@ -1,7 +1,12 @@ import type { AgentStreamEventPayload } from "@server/shared/messages"; import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle"; import type { StreamItem } from "@/types/stream"; -import { applyStreamEvent, hydrateStreamState, reduceStreamUpdate } from "@/types/stream"; +import { + applyStreamEvent, + flushHeadToTail, + hydrateStreamState, + reduceStreamUpdate, +} from "@/types/stream"; import { classifySessionTimelineSeq, type SessionTimelineSeqDecision, @@ -214,12 +219,24 @@ export function processTimelineResponse( } if (acceptedUnits.length > 0) { + // Flush head to tail before appending canonical entries so that + // chronological ordering is preserved. This matters on mobile where + // the server drops live events for backgrounded/unfocused agents: + // when the client catches up, the head may still hold stale live + // items from before the gap that must be committed ahead of the + // canonical gap-fill entries. + const baseTail = + currentHead.length > 0 ? flushHeadToTail(currentTail, currentHead) : currentTail; + if (currentHead.length > 0) { + nextHead = []; + } + nextTail = acceptedUnits.reduce( (state, { event, timestamp }) => reduceStreamUpdate(state, event, timestamp, { source: "canonical", }), - currentTail, + baseTail, ); } diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index 677667a2e..86f979870 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -729,7 +729,7 @@ function finalizeHeadItems(head: StreamItem[]): StreamItem[] { /** * Flush head items to tail, avoiding duplicates. */ -function flushHeadToTail(tail: StreamItem[], head: StreamItem[]): StreamItem[] { +export function flushHeadToTail(tail: StreamItem[], head: StreamItem[]): StreamItem[] { if (head.length === 0) { return tail; }