Fix mobile agent timeline catch-up

This commit is contained in:
Mohamed Boudra
2026-05-29 09:31:16 +07:00
parent 2e1b907c63
commit 65ace20880
11 changed files with 410 additions and 77 deletions
+65 -26
View File
@@ -14,7 +14,12 @@ import {
type TimelineReducerSideEffect,
} from "@/timeline/session-stream-reducers";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { TIMELINE_FETCH_PAGE_SIZE } from "@/timeline/timeline-fetch-policy";
import {
isTimelineCatchUpComplete,
planResumeTimelineSync,
planTimelineCatchUpAfter,
planTimelineCatchUpFollowUp,
} from "@/timeline/timeline-sync-plan";
import type { AgentAttachment, SessionOutboundMessage } from "@getpaseo/protocol/messages";
import { parseServerInfoStatusPayload } from "@getpaseo/protocol/messages";
import {
@@ -500,6 +505,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
const revalidationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const revalidationInFlightRef = useRef<Promise<void> | null>(null);
const revalidationQueuedRef = useRef(false);
const timelineCatchUpInFlightRef = useRef<Set<string>>(new Set());
const wasConnectedRef = useRef(isConnected);
const audioOutputBuffersRef = useRef<Map<string, BufferedAudioChunk[]>>(new Map());
const activeAudioGroupsRef = useRef<Set<string>>(new Set());
@@ -746,6 +752,27 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}, AUTHORITATIVE_REVALIDATION_DEBOUNCE_MS);
}, [client, flushAuthoritativeRevalidation, isConnected]);
const requestCanonicalCatchUp = useCallback(
(agentId: string, cursor: { epoch: string; endSeq: number }) => {
const request = planTimelineCatchUpAfter({ epoch: cursor.epoch, seq: cursor.endSeq });
const key = `${agentId}:${request.cursor.epoch}:${request.cursor.seq}`;
const inFlight = timelineCatchUpInFlightRef.current;
if (inFlight.has(key)) {
return;
}
inFlight.add(key);
void client
.fetchAgentTimeline(agentId, request)
.catch((error) => {
console.warn("[Session] failed to fetch canonical catch-up timeline", agentId, error);
})
.finally(() => {
inFlight.delete(key);
});
},
[client],
);
const handleAppResumed = useCallback(
(awayMs: number) => {
scheduleAuthoritativeRevalidation();
@@ -754,15 +781,19 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
const session = useSessionStore.getState().sessions[serverId];
const agentId = session?.focusedAgentId;
if (agentId) {
void client
.fetchAgentTimeline(agentId, {
direction: "tail",
limit: TIMELINE_FETCH_PAGE_SIZE,
projection: "canonical",
})
.catch((error) => {
const plan = planResumeTimelineSync({
cursor: session?.agentTimelineCursor.get(agentId),
});
if (plan.direction === "after") {
requestCanonicalCatchUp(agentId, {
epoch: plan.cursor.epoch,
endSeq: plan.cursor.seq,
});
} else {
void client.fetchAgentTimeline(agentId, plan).catch((error) => {
console.warn("[Session] failed to fetch tail timeline on resume", agentId, error);
});
}
}
}
@@ -771,7 +802,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}
bumpHistorySyncGeneration(serverId);
},
[bumpHistorySyncGeneration, client, scheduleAuthoritativeRevalidation, serverId],
[
bumpHistorySyncGeneration,
client,
requestCanonicalCatchUp,
scheduleAuthoritativeRevalidation,
serverId,
],
);
// Client activity tracking (heartbeat, push token registration)
@@ -1018,22 +1055,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
[serverId, upsertWorkspaceSetupProgress],
);
const requestCanonicalCatchUp = useCallback(
(agentId: string, cursor: { epoch: string; endSeq: number }) => {
void client
.fetchAgentTimeline(agentId, {
direction: "after",
cursor: { epoch: cursor.epoch, seq: cursor.endSeq },
limit: TIMELINE_FETCH_PAGE_SIZE,
projection: "canonical",
})
.catch((error) => {
console.warn("[Session] failed to fetch canonical catch-up timeline", agentId, error);
});
},
[client],
);
const applyTimelineResponse = useCallback(
(
payload: Extract<
@@ -1043,8 +1064,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
) => {
const agentId = payload.agentId;
const initKey = getInitKey(serverId, agentId);
const catchUpComplete = isTimelineCatchUpComplete({
direction: payload.direction,
hasNewer: payload.hasNewer,
error: payload.error,
});
const shouldMarkAuthoritativeHistoryApplied =
payload.direction === "tail" || payload.direction === "after";
payload.direction === "tail" || (payload.direction === "after" && catchUpComplete);
// Read current store state
const session = useSessionStore.getState().sessions[serverId];
@@ -1114,6 +1140,19 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
applyAgentUpdatePayload,
});
const followUp = planTimelineCatchUpFollowUp({
direction: payload.direction,
hasNewer: payload.hasNewer,
endCursor: payload.endCursor,
error: payload.error,
});
if (followUp?.direction === "after") {
requestCanonicalCatchUp(agentId, {
epoch: followUp.cursor.epoch,
endSeq: followUp.cursor.seq,
});
}
finalizeTimelineApplication({
result,
agentId,