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
@@ -31,6 +31,82 @@ export interface PromptJumpRequestTracker {
requests(): Array<{ cursorSeq: number | null; limit: number | null; mergeWindow: boolean }>;
}
export interface TimelineRequestTracker {
nextRequest(): Promise<{
direction: string | null;
cursor: { epoch: string; seq: number } | null;
}>;
requests(): Array<{
direction: string | null;
cursor: { epoch: string; seq: number } | null;
}>;
waitForResponse(): Promise<void>;
}
export async function trackAgentTimelineRequests(
page: Page,
agentId: string,
): Promise<TimelineRequestTracker> {
type Request = ReturnType<TimelineRequestTracker["requests"]>[number];
const seen: Request[] = [];
const waiters: Array<(request: Request) => void> = [];
let responseSeen = false;
let resolveResponse: (() => void) | null = null;
const response = new Promise<void>((resolve) => {
resolveResponse = resolve;
});
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
const server = ws.connectToServer();
ws.onMessage((message) => {
const sessionMessage = getSessionMessage(message);
if (
sessionMessage?.type === "fetch_agent_timeline_request" &&
sessionMessage.agentId === agentId
) {
const rawCursor = sessionMessage.cursor;
const cursor =
rawCursor &&
typeof rawCursor === "object" &&
typeof (rawCursor as { epoch?: unknown }).epoch === "string" &&
typeof (rawCursor as { seq?: unknown }).seq === "number"
? {
epoch: (rawCursor as { epoch: string }).epoch,
seq: (rawCursor as { seq: number }).seq,
}
: null;
const request = {
direction: typeof sessionMessage.direction === "string" ? sessionMessage.direction : null,
cursor,
};
seen.push(request);
waiters.shift()?.(request);
}
server.send(message);
});
server.onMessage((message) => {
const sessionMessage = getSessionMessage(message);
const payload = sessionMessage ? getPayload(sessionMessage) : null;
if (
sessionMessage?.type === "fetch_agent_timeline_response" &&
payload?.agentId === agentId
) {
responseSeen = true;
resolveResponse?.();
}
ws.send(message);
});
});
return {
nextRequest() {
const request = seen[0];
if (request) return Promise.resolve(request);
return new Promise((resolve) => waiters.push(resolve));
},
requests: () => [...seen],
waitForResponse: () => (responseSeen ? Promise.resolve() : response),
};
}
export async function trackPromptJumpRequests(
page: Page,
agentId: string,
@@ -5,10 +5,16 @@ const STORE_NAME = "key-value";
const STORAGE_KEY = "@paseo:replica-cache";
export interface ReplicaCacheRecord {
version?: number;
hosts?: Array<{
timeline?: {
agentId?: string;
items?: Array<Record<string, unknown>>;
range?: {
endSeq?: number;
epoch?: string;
startSeq?: number;
} | null;
} | null;
}>;
}
@@ -40,6 +40,12 @@ interface TimelineViewportSnapshot {
scrollTop: number;
}
interface PersistedCanonicalTimelineRange {
epoch: string;
startSeq: number;
endSeq: number;
}
export interface TimelinePresentationSnapshot {
marker: string;
position: TimelinePromptPositionSnapshot;
@@ -298,6 +304,32 @@ export async function reloadAgentTimelineFromPersistedReplica(
await expectTimelinePromptVisible(page, agent.newestPrompt);
}
export async function waitForPersistedCanonicalTimelineRange(
page: Page,
agentId: string,
): Promise<PersistedCanonicalTimelineRange> {
const readRange = async () => {
const cache = await readReplicaCache(page);
if (cache?.version !== 6) return null;
const range = cache.hosts?.find((host) => host.timeline?.agentId === agentId)?.timeline?.range;
if (
typeof range?.epoch !== "string" ||
typeof range.startSeq !== "number" ||
typeof range.endSeq !== "number"
) {
return null;
}
return { epoch: range.epoch, startSeq: range.startSeq, endSeq: range.endSeq };
};
await expect.poll(readRange).not.toBeNull();
const range = await readRange();
if (!range) {
throw new Error(`Persisted canonical timeline range is missing for ${agentId}`);
}
return range;
}
export async function holdNextOlderTimelinePage(
page: Page,
agent: LongTimelineAgent,
@@ -40,12 +40,20 @@ export function rememberTimelineRequestCounts(gate: DaemonWebSocketGate): Timeli
};
}
export function expectOneTailCheckWithoutCatchUpPages(
export function expectOneResumeCheckWithoutTail(
gate: DaemonWebSocketGate,
before: TimelineRequestCounts,
): void {
expect(gate.getTimelineRequestCount("after") - before.after).toBe(1);
expect(gate.getTimelineRequestCount("tail") - before.tail).toBe(0);
}
export function expectResumeOverflowFallsBackToOneTail(
gate: DaemonWebSocketGate,
before: TimelineRequestCounts,
): void {
expect(gate.getTimelineRequestCount("after") - before.after).toBe(1);
expect(gate.getTimelineRequestCount("tail") - before.tail).toBe(1);
expect(gate.getTimelineRequestCount("after") - before.after).toBe(0);
}
export async function disconnectViewedTimeline(
@@ -56,7 +64,7 @@ export async function disconnectViewedTimeline(
await expectReconnectingToastVisible(page);
}
export async function restoreViewedTimelineWithHeldTail(
export async function restoreViewedTimelineWithHeldResponse(
page: Page,
gate: DaemonWebSocketGate,
): Promise<void> {