fix: flush head before applying canonical catch-up entries on mobile
On mobile, the server drops live stream events for backgrounded or unfocused agents. When the user returns, a seq gap triggers a catch-up fetch whose canonical entries are appended to the tail — but the head was never flushed. Stale live items from before the gap stayed in the head and rendered after the newer catch-up entries, breaking chronological order. Worse, subsequent live events of the same kind (e.g. assistant_message) would append to the stale head item, garbling message content. Now the incremental path in processTimelineResponse flushes head → tail before reducing canonical entries, keeping the timeline ordered and the head clean for new live events.
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
import type { AgentStreamEventPayload } from "@server/shared/messages";
|
import type { AgentStreamEventPayload } from "@server/shared/messages";
|
||||||
import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
|
import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
|
||||||
import type { StreamItem } from "@/types/stream";
|
import type { StreamItem } from "@/types/stream";
|
||||||
import { applyStreamEvent, hydrateStreamState, reduceStreamUpdate } from "@/types/stream";
|
import {
|
||||||
|
applyStreamEvent,
|
||||||
|
flushHeadToTail,
|
||||||
|
hydrateStreamState,
|
||||||
|
reduceStreamUpdate,
|
||||||
|
} from "@/types/stream";
|
||||||
import {
|
import {
|
||||||
classifySessionTimelineSeq,
|
classifySessionTimelineSeq,
|
||||||
type SessionTimelineSeqDecision,
|
type SessionTimelineSeqDecision,
|
||||||
@@ -214,12 +219,24 @@ export function processTimelineResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (acceptedUnits.length > 0) {
|
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<StreamItem[]>(
|
nextTail = acceptedUnits.reduce<StreamItem[]>(
|
||||||
(state, { event, timestamp }) =>
|
(state, { event, timestamp }) =>
|
||||||
reduceStreamUpdate(state, event, timestamp, {
|
reduceStreamUpdate(state, event, timestamp, {
|
||||||
source: "canonical",
|
source: "canonical",
|
||||||
}),
|
}),
|
||||||
currentTail,
|
baseTail,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -729,7 +729,7 @@ function finalizeHeadItems(head: StreamItem[]): StreamItem[] {
|
|||||||
/**
|
/**
|
||||||
* Flush head items to tail, avoiding duplicates.
|
* 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) {
|
if (head.length === 0) {
|
||||||
return tail;
|
return tail;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user