import { DatabaseSync } from "node:sqlite"; import { ReplicaCache } from "@/runtime/replica-cache"; import { createSqliteReplicaRowStore, type ReplicaSqliteConnection, type SqliteValue, } from "@/runtime/replica-cache/row-store-sqlite"; import { afterEach, describe, expect, it } from "vitest"; import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; import type { CachedTimeline } from "@/runtime/replica-cache"; import { selectAgentTimelineState, useSessionStore } from "@/stores/session-store"; import { useCreateFlowStore, type PendingCreateAttempt } from "@/stores/create-flow-store"; import type { StreamItem } from "@/types/stream"; import { createTimelineReplica, createViewedTimelineOwner, type TimelineReplicaStorage, type ViewedTimelineOwner, } from "./viewed-timeline-sync"; const SERVER_ID = "timeline-replica-host"; const AGENT_ID = "agent-1"; function item(id: string, text: string, seq: number): StreamItem { return { kind: "assistant_message", id, text, timestamp: new Date("2026-08-26T10:00:00.000Z"), timelineCursor: { epoch: "epoch-1", seq }, }; } function cachedTimeline(): CachedTimeline { return { agentId: AGENT_ID, items: [item("cached", "cached", 4)], range: { epoch: "epoch-1", startSeq: 1, endSeq: 4 }, hasOlder: true, }; } function createOwner(storage: TimelineReplicaStorage): ViewedTimelineOwner { const replica = createTimelineReplica({ serverId: SERVER_ID, storage, prepareAgent: async () => undefined, }); return createViewedTimelineOwner({ serverId: SERVER_ID, replica, replaceDemandedAgentIds: () => undefined, drainQueuedAgentMessage: () => undefined, ports: { observe: () => ({ ready: Promise.resolve(), release: async () => undefined }), readCursor: () => undefined, fetchPage: async () => ({ hasNewer: false, endCursor: null }), fetchLatestTail: async () => ({ hasNewer: false, endCursor: null }), reportError: () => undefined, schedule: () => () => undefined, }, }); } function applySynced(agentId: string, seq: number): void { useSessionStore.getState().applyAgentTimelineResponseState(SERVER_ID, agentId, { items: [item(`network-${agentId}`, "network", seq)], head: [], range: { epoch: "epoch-1", startSeq: 1, endSeq: seq }, older: "available", newer: false, synchronized: true, acknowledgedClientMessageIds: [], }); } afterEach(() => useSessionStore.getState().clearSession(SERVER_ID)); describe("viewed timeline persistence", () => { it("shares an in-flight cache preparation with the viewed owner", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); let release!: (value: CachedTimeline) => void; let reads = 0; const read = new Promise((resolve) => { release = resolve; }); const replica = createTimelineReplica({ serverId: SERVER_ID, storage: { readTimeline: () => { reads += 1; return read; }, commitTimeline: () => undefined, }, prepareAgent: async () => undefined, }); const routePreparation = replica.prepare(AGENT_ID); const ownerPreparation = replica.prepare(AGENT_ID); release(cachedTimeline()); await Promise.all([routePreparation, ownerPreparation]); expect(reads).toBe(1); expect(replica.readCursor(AGENT_ID)).toEqual({ epoch: "epoch-1", endSeq: 4 }); }); it("paints cached history without claiming authoritative synchronization", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); const owner = createOwner({ readTimeline: async () => cachedTimeline(), commitTimeline: () => undefined, }); owner.replaceVisibleAgentIds("test", [AGENT_ID]); await expect .poll(() => selectAgentTimelineState(useSessionStore.getState().sessions[SERVER_ID], AGENT_ID), ) .toEqual({ status: "painted", items: cachedTimeline().items }); owner.dispose(); }); it("reconciles an overlapping projected message against its cached cursor", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); const partial = "```mermaid\nflowchart LR\n Start --> Mid"; const complete = `${partial}dle\n Middle --> Done\n\`\`\``; const owner = createOwner({ readTimeline: async () => ({ agentId: AGENT_ID, items: [item("cached", partial, 4)], range: { epoch: "epoch-1", startSeq: 1, endSeq: 4 }, hasOlder: false, }), commitTimeline: () => undefined, }); owner.replaceVisibleAgentIds("test", [AGENT_ID]); await expect .poll(() => selectAgentTimelineState(useSessionStore.getState().sessions[SERVER_ID], AGENT_ID), ) .toMatchObject({ status: "painted" }); owner.applyTimelineResponse({ requestId: "page-after-cache", agentId: AGENT_ID, agent: null, direction: "after", projection: "projected", reset: false, epoch: "epoch-1", window: { minSeq: 1, maxSeq: 5, nextSeq: 6 }, startCursor: { epoch: "epoch-1", seq: 5 }, endCursor: { epoch: "epoch-1", seq: 5 }, entries: [ { provider: "mock", item: { type: "assistant_message", text: complete }, timestamp: "2026-08-26T10:00:00.000Z", seqStart: 2, seqEnd: 5, sourceSeqRanges: [{ startSeq: 2, endSeq: 5 }], collapsed: ["assistant_merge"], }, ], error: null, hasNewer: false, hasOlder: false, staleCursor: false, gap: false, }); const session = useSessionStore.getState().sessions[SERVER_ID]; expect([ ...(session?.agentStreamTail.get(AGENT_ID) ?? []), ...(session?.agentStreamHead.get(AGENT_ID) ?? []), ]).toMatchObject([{ kind: "assistant_message", text: complete }]); owner.dispose(); }); it("reopens painted live mutations without persisting authoritative coverage", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); let durable: CachedTimeline | undefined = cachedTimeline(); let pending: CachedTimeline | undefined; const storage: TimelineReplicaStorage = { readTimeline: async () => durable, commitTimeline: (_serverId, _agentId, timeline) => { pending = timeline; }, }; const first = createOwner(storage); first.replaceVisibleAgentIds("test", [AGENT_ID]); await expect .poll(() => selectAgentTimelineState(useSessionStore.getState().sessions[SERVER_ID], AGENT_ID), ) .toMatchObject({ status: "painted" }); first.enqueueStreamEvent(AGENT_ID, { event: { type: "timeline", provider: "codex", item: { type: "assistant_message", text: "live", messageId: "live" }, } as AgentStreamEventPayload, seq: 5, epoch: "epoch-1", timestamp: new Date("2026-08-26T10:00:01.000Z"), }); first.flushStreamAgent(AGENT_ID); expect(pending).toMatchObject({ range: null, hasOlder: false }); expect(pending?.items.map((entry) => entry.id)).toEqual(["cached", expect.any(String)]); durable = pending; first.dispose(); useSessionStore.getState().clearSession(SERVER_ID); useSessionStore.getState().initializeSession(SERVER_ID, null); const reopened = createOwner(storage); reopened.replaceVisibleAgentIds("test", [AGENT_ID]); await expect .poll(() => selectAgentTimelineState(useSessionStore.getState().sessions[SERVER_ID], AGENT_ID), ) .toMatchObject({ status: "painted", items: [ expect.objectContaining({ id: "cached" }), expect.objectContaining({ text: "live" }), ], }); reopened.dispose(); }); it("does not let a late cache read overwrite newer network state", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); let release!: (value: CachedTimeline) => void; const read = new Promise((resolve) => { release = resolve; }); const owner = createOwner({ readTimeline: () => read, commitTimeline: () => undefined, }); owner.replaceVisibleAgentIds("test", [AGENT_ID]); applySynced(AGENT_ID, 8); release(cachedTimeline()); await expect .poll(() => selectAgentTimelineState(useSessionStore.getState().sessions[SERVER_ID], AGENT_ID), ) .toMatchObject({ status: "synced", range: { endSeq: 8 } }); owner.dispose(); }); it("paints cached rows without replacing a live head that arrives during preparation", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); let release!: (value: CachedTimeline) => void; const read = new Promise((resolve) => { release = resolve; }); const replica = createTimelineReplica({ serverId: SERVER_ID, storage: { readTimeline: () => read, commitTimeline: () => undefined, }, prepareAgent: async () => undefined, }); const preparation = replica.prepare(AGENT_ID); useSessionStore.getState().setAgentStreamState(SERVER_ID, AGENT_ID, { head: [item("live", "live", 5)], }); release(cachedTimeline()); await preparation; expect(replica.readCursor(AGENT_ID)).toEqual({ epoch: "epoch-1", endSeq: 4 }); expect( selectAgentTimelineState(useSessionStore.getState().sessions[SERVER_ID], AGENT_ID), ).toEqual({ status: "painted", items: cachedTimeline().items }); expect(useSessionStore.getState().sessions[SERVER_ID]?.agentStreamHead.get(AGENT_ID)).toEqual([ item("live", "live", 5), ]); }); it("reconciles a live head that overlaps the cached canonical timeline", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); let release!: (value: CachedTimeline) => void; const read = new Promise((resolve) => { release = resolve; }); const replica = createTimelineReplica({ serverId: SERVER_ID, storage: { readTimeline: () => read, commitTimeline: () => undefined, }, prepareAgent: async () => undefined, }); const preparation = replica.prepare(AGENT_ID); useSessionStore.getState().setAgentStreamState(SERVER_ID, AGENT_ID, { head: [item("cached", "cached", 4), item("live", "live", 5)], }); release(cachedTimeline()); await preparation; const session = useSessionStore.getState().sessions[SERVER_ID]; expect([ ...(session?.agentStreamTail.get(AGENT_ID) ?? []), ...(session?.agentStreamHead.get(AGENT_ID) ?? []), ]).toEqual([item("cached", "cached", 4), item("live", "live", 5)]); expect(replica.readCursor(AGENT_ID)).toEqual({ epoch: "epoch-1", endSeq: 4 }); }); it("reconciles cached rows with a non-authoritative timeline painted during preparation", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); let release!: (value: CachedTimeline) => void; const read = new Promise((resolve) => { release = resolve; }); const replica = createTimelineReplica({ serverId: SERVER_ID, storage: { readTimeline: () => read, commitTimeline: () => undefined, }, prepareAgent: async () => undefined, }); const preparation = replica.prepare(AGENT_ID); useSessionStore.getState().applyAgentTimelineResponseState(SERVER_ID, AGENT_ID, { items: [item("live", "live", 5)], head: [], range: null, older: "none", newer: false, synchronized: false, acknowledgedClientMessageIds: [], }); release(cachedTimeline()); await preparation; const session = useSessionStore.getState().sessions[SERVER_ID]; expect([ ...(session?.agentStreamTail.get(AGENT_ID) ?? []), ...(session?.agentStreamHead.get(AGENT_ID) ?? []), ]).toEqual([item("cached", "cached", 4), item("live", "live", 5)]); expect(replica.readCursor(AGENT_ID)).toEqual({ epoch: "epoch-1", endSeq: 4 }); }); it("persists accepted live stream commits through the owner", () => { useSessionStore.getState().initializeSession(SERVER_ID, null); applySynced(AGENT_ID, 8); const commits: CachedTimeline[] = []; const owner = createOwner({ readTimeline: async () => undefined, commitTimeline: (_serverId, _agentId, timeline) => commits.push(timeline), }); owner.enqueueStreamEvent(AGENT_ID, { event: { type: "timeline", provider: "codex", item: { type: "assistant_message", text: "live", messageId: "live" }, } as AgentStreamEventPayload, seq: 9, epoch: "epoch-1", timestamp: new Date("2026-08-26T10:00:01.000Z"), }); owner.flushStreamAgent(AGENT_ID); expect(commits.at(-1)?.items.at(-1)).toMatchObject({ text: "live" }); expect(commits.at(-1)?.range?.endSeq).toBe(9); owner.dispose(); }); it("applies and persists authoritative pages inside the owner", () => { useSessionStore.getState().initializeSession(SERVER_ID, null); const keys: string[] = []; const owner = createOwner({ readTimeline: async () => undefined, commitTimeline: (_serverId, agentId) => keys.push(agentId), }); owner.applyTimelineResponse({ requestId: "page-1", agentId: AGENT_ID, agent: null, direction: "tail", projection: "projected", reset: false, epoch: "epoch-1", window: { minSeq: 1, maxSeq: 0, nextSeq: 1 }, startCursor: null, endCursor: null, entries: [], error: null, hasNewer: false, hasOlder: false, staleCursor: false, gap: false, }); expect( selectAgentTimelineState(useSessionStore.getState().sessions[SERVER_ID], AGENT_ID), ).toMatchObject({ status: "synced" }); expect(keys).toEqual([AGENT_ID]); owner.dispose(); }); it("persists demanded agents independently", () => { useSessionStore.getState().initializeSession(SERVER_ID, null); const keys: string[] = []; const owner = createOwner({ readTimeline: async () => undefined, commitTimeline: (_serverId, agentId) => keys.push(agentId), }); applySynced(AGENT_ID, 8); applySynced("agent-2", 3); for (const [agentId, seq] of [ [AGENT_ID, 9], ["agent-2", 4], ] as const) { owner.enqueueStreamEvent(agentId, { event: { type: "timeline", provider: "codex", item: { type: "assistant_message", text: agentId, messageId: `live-${agentId}` }, } as AgentStreamEventPayload, seq, epoch: "epoch-1", timestamp: new Date("2026-08-26T10:00:01.000Z"), }); owner.flushStreamAgent(agentId); } expect(keys).toEqual([AGENT_ID, "agent-2"]); owner.dispose(); }); }); describe("create handoff lifetime", () => { const DRAFT_ID = "draft-1"; afterEach(() => useCreateFlowStore.getState().clearAll()); function sentCreateHandoff(): void { useCreateFlowStore.getState().setPending({ draftId: DRAFT_ID, serverId: SERVER_ID, agentId: AGENT_ID, clientMessageId: "client-message-1", text: "Create this chat.", timestamp: 0, }); useCreateFlowStore.getState().markLifecycle({ draftId: DRAFT_ID, lifecycle: "sent" }); } function createHandoff(): PendingCreateAttempt | undefined { return useCreateFlowStore.getState().pendingByDraftId[DRAFT_ID]; } function syncedTailPage(): Parameters[0] { return { requestId: "tail-1", agentId: AGENT_ID, agent: null, direction: "tail", projection: "projected", reset: false, epoch: "epoch-1", window: { minSeq: 1, maxSeq: 0, nextSeq: 1 }, startCursor: null, endCursor: null, entries: [], error: null, hasNewer: false, hasOlder: false, staleCursor: false, gap: false, }; } interface HeldCatchUpOwner { owner: ViewedTimelineOwner; release: () => void; } async function ownerWithHeldCatchUp(): Promise { let releaseTailFetch!: () => void; let tailFetchStarted = false; const tailFetched = new Promise((resolve) => { releaseTailFetch = resolve; }); const replica = createTimelineReplica({ serverId: SERVER_ID, storage: { readTimeline: async () => undefined, commitTimeline: () => undefined }, prepareAgent: async () => undefined, }); const owner = createViewedTimelineOwner({ serverId: SERVER_ID, replica, replaceDemandedAgentIds: () => undefined, drainQueuedAgentMessage: () => undefined, ports: { observe: () => ({ ready: Promise.resolve(), release: async () => undefined }), readCursor: () => undefined, fetchPage: async () => { tailFetchStarted = true; await tailFetched; return { hasNewer: false, endCursor: null }; }, fetchLatestTail: async () => ({ hasNewer: false, endCursor: null }), reportError: () => undefined, schedule: () => () => undefined, }, }); owner.setConnected(true); owner.replaceVisibleAgentIds("test", [AGENT_ID]); await expect.poll(() => tailFetchStarted).toBe(true); return { owner, release: releaseTailFetch }; } it("keeps the handoff through the first synchronized page and releases it when the chat is current", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); sentCreateHandoff(); const { owner, release } = await ownerWithHeldCatchUp(); owner.applyTimelineResponse(syncedTailPage()); expect( selectAgentTimelineState(useSessionStore.getState().sessions[SERVER_ID], AGENT_ID), ).toMatchObject({ status: "synced" }); expect(createHandoff()).toMatchObject({ agentId: AGENT_ID, lifecycle: "sent" }); release(); await expect.poll(() => createHandoff()).toBeUndefined(); owner.dispose(); }); it("releases the handoff when the chat's tab closes before catch-up completes", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); sentCreateHandoff(); const { owner, release } = await ownerWithHeldCatchUp(); owner.replaceVisibleAgentIds("test", []); owner.replaceOpenTabAgentIds([]); expect(createHandoff()).toBeUndefined(); release(); owner.dispose(); }); }); function createSqliteCache() { const database = new DatabaseSync(":memory:"); let beforeRead = async () => {}; const connection: ReplicaSqliteConnection = { async exec(sql) { database.exec(sql); }, async run(sql, params = []) { database.prepare(sql).run(...params); }, async all(sql: string, params: readonly SqliteValue[] = []) { const rows = database.prepare(sql).all(...params) as Row[]; if (sql.includes("FROM rows") && sql.includes("WHERE")) await beforeRead(); return rows; }, async transaction(operation) { database.exec("BEGIN IMMEDIATE"); try { await operation(connection); database.exec("COMMIT"); } catch (error) { database.exec("ROLLBACK"); throw error; } }, }; const storage = createSqliteReplicaRowStore({ open: async () => connection }, 1); const cache = new ReplicaCache(storage, { clearLegacyCache: async () => {} }); return { cache, database, holdRead: (operation: () => Promise) => { beforeRead = operation; }, }; } describe("SQLite baseline restoration", () => { it.each([false, true])( "keeps display-only cached history under a live event (synced=%s)", async (synced) => { useSessionStore.getState().initializeSession(SERVER_ID, null); const { cache, database, holdRead } = createSqliteCache(); cache.setHosts([SERVER_ID]); cache.commitTimeline(SERVER_ID, AGENT_ID, { ...cachedTimeline(), items: [item("earlier", "earlier", 1), ...cachedTimeline().items], range: null, }); await cache.flush(); holdRead(async () => { if (synced) applySynced(AGENT_ID, 8); else useSessionStore.getState().setAgentStreamState(SERVER_ID, AGENT_ID, { head: [item("cached", "cached", 4), item("live", "live", 5)], }); }); const replica = createTimelineReplica({ serverId: SERVER_ID, storage: cache, prepareAgent: async () => {}, }); await replica.prepare(AGENT_ID); const session = useSessionStore.getState().sessions[SERVER_ID]; const timeline = selectAgentTimelineState(session, AGENT_ID); expect(timeline.status).toBe(synced ? "synced" : "painted"); expect([ ...(timeline.status === "cold" ? [] : timeline.items), ...(session?.agentStreamHead.get(AGENT_ID) ?? []), ]).toEqual( synced ? [item(`network-${AGENT_ID}`, "network", 8)] : [item("earlier", "earlier", 1), item("cached", "cached", 4), item("live", "live", 5)], ); expect(replica.readCursor(AGENT_ID)).toBeUndefined(); database.close(); }, ); it("starts the timeline disk read while agent preparation is pending", async () => { useSessionStore.getState().initializeSession(SERVER_ID, null); const { cache, database, holdRead } = createSqliteCache(); cache.setHosts([SERVER_ID]); cache.commitTimeline(SERVER_ID, AGENT_ID, cachedTimeline()); await cache.flush(); let readStarted = false; let release!: () => void; const agentReady = new Promise((resolve) => { release = resolve; }); holdRead(async () => { readStarted = true; }); const replica = createTimelineReplica({ serverId: SERVER_ID, storage: cache, prepareAgent: () => agentReady, }); const preparation = replica.prepare(AGENT_ID); try { await expect.poll(() => readStarted, { timeout: 500 }).toBe(true); } finally { release(); await preparation; database.close(); } }); });