Resume cached timelines without replaying their tail (#3329)

* perf(timeline): resume cached history from its exact range

Warm launches were repainting cached history and then downloading the same bounded tail to rediscover that nothing changed. Persist synchronization authority only when the complete current canonical window can be encoded losslessly; otherwise retain the existing display-only fallback.

* test(timeline): hide replica setup behind E2E helper

* fix(timeline): bound persisted resume overflow

* test(timeline): cover workspace eviction catch-up order

* test(timeline): read persisted range from IndexedDB
This commit is contained in:
Mohamed Boudra
2026-08-14 12:57:44 +02:00
committed by GitHub
parent 30195d3bd1
commit f0bd2c8483
18 changed files with 707 additions and 75 deletions
+46 -7
View File
@@ -19,7 +19,10 @@ import {
type TimelineReducerSideEffect,
} from "@/timeline/session-stream-reducers";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { isTimelineResumeSnapshotAuthoritative } from "@/timeline/timeline-sync-plan";
import {
isTimelineResumeSnapshotAuthoritative,
planTimelineTailFetch,
} from "@/timeline/timeline-sync-plan";
import {
createViewedTimelineSync,
type TimelineDeliveryMode,
@@ -33,6 +36,7 @@ import {
type AgentAttentionNotificationPayload,
type NotificationPermissionRequest,
} from "@getpaseo/protocol/agent-attention-notification";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { AgentSessionConfig } from "@getpaseo/protocol/agent-types";
import type { GitSetupOptions } from "@getpaseo/protocol/messages";
@@ -67,6 +71,20 @@ import { applyCheckoutStatusUpdateFromEvent } from "@/git/checkout-status-cache"
import { useProviderSubagentStore } from "@/subagents/provider-store";
import { revalidateSessionAfterResume } from "@/contexts/session-resume-revalidation";
type TimelineResponsePayload = Extract<
SessionOutboundMessage,
{ type: "fetch_agent_timeline_response" }
>["payload"];
function consumeForcedTimelineTailReplacement(
payload: TimelineResponsePayload,
replacements: Set<string>,
): TimelineResponsePayload {
if (payload.direction !== "tail") return payload;
if (!replacements.delete(payload.agentId)) return payload;
return { ...payload, reset: true };
}
// Re-export types from session-store and draft-store for backward compatibility
export type { DraftInput } from "@/stores/draft-store";
export type {
@@ -350,6 +368,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
const _sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const attentionNotifiedRef = useRef<Map<string, number>>(new Map());
const appStateRef = useRef(AppState.currentState);
const forcedTimelineTailReplacements = useRef(new Set<string>());
const viewedTimelineSyncRef = useRef<ViewedTimelineSync | null>(null);
const audioOutputBuffersRef = useRef<Map<string, BufferedAudioChunk[]>>(new Map());
const activeAudioGroupsRef = useRef<Set<string>>(new Set());
@@ -528,12 +547,11 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
);
const applyTimelineResponse = useCallback(
(
payload: Extract<
SessionOutboundMessage,
{ type: "fetch_agent_timeline_response" }
>["payload"],
) => {
(receivedPayload: TimelineResponsePayload) => {
const payload = consumeForcedTimelineTailReplacement(
receivedPayload,
forcedTimelineTailReplacements.current,
);
const agentId = payload.agentId;
const initKey = getInitKey(serverId, agentId);
const shouldMarkAuthoritativeHistoryApplied = isTimelineResumeSnapshotAuthoritative({
@@ -642,6 +660,15 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
const sync = createViewedTimelineSync({
initialDeliveryMode,
setSubscription: (agentIds) => client.setAgentTimelineSubscription(agentIds),
readCursor: (agentId) => {
const timeline = selectAgentTimelineState(
useSessionStore.getState().sessions[serverId],
agentId,
);
return timeline.status === "synced" && timeline.range
? { epoch: timeline.range.epoch, endSeq: timeline.range.endSeq }
: undefined;
},
fetchPage: async (agentId, request) => {
const session = useSessionStore.getState().sessions[serverId];
const initKey = getInitKey(serverId, agentId);
@@ -672,6 +699,18 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
throw error;
}
},
fetchLatestTail: async (agentId) => {
forcedTimelineTailReplacements.current.add(agentId);
try {
return await getHostRuntimeStore().fetchAgentTimeline(
serverId,
agentId,
planTimelineTailFetch(),
);
} finally {
forcedTimelineTailReplacements.current.delete(agentId);
}
},
reportError: (error) => {
console.warn("[Session] viewed timeline synchronization failed", { serverId, error });
},