fix(app): start a catch-up's follow-up page after rows applied meanwhile

A recovery paged forward from each page's end cursor without looking at
what the chat had applied since. When the latest-tail fallback brought
the chat to the end during a recovery, the recovery kept paging through
history it already held, one round trip per page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
paseo-bot[bot]
2026-11-15 00:05:12 +00:00
co-authored by Claude Opus 5.5
parent 368bdcdb72
commit 7dec67fbf9
2 changed files with 38 additions and 1 deletions
@@ -497,6 +497,40 @@ test("a chat without coverage that recovers a gap loads the latest tail when his
world.sync.dispose();
});
test("a recovery's follow-up page starts after rows another fetch applied meanwhile", async () => {
const world = new TimelineWorld();
world.sync.setConnected(true);
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
(await world.nextMembership()).succeed();
(await world.nextFetch("agent-a")).respond({ hasNewer: false, seq: 0 });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
world.sync.recoverGap("agent-a", { epoch: "epoch-agent-a", endSeq: 0 });
const firstPage = await world.nextFetch("agent-a");
world.cursors.set("agent-a", { epoch: "epoch-agent-a", endSeq: 40 });
firstPage.respond({ hasNewer: true, seq: 40 });
const latestTail = await world.nextFetch("agent-a");
expect(latestTail.request.direction).toBe("tail");
world.sync.recoverGap("agent-a", { epoch: "epoch-agent-a", endSeq: 40 });
const recovery = await world.nextFetch("agent-a");
world.cursors.set("agent-a", { epoch: "epoch-agent-a", endSeq: 300 });
latestTail.respond({ hasNewer: false, seq: 300 });
recovery.respond({ hasNewer: true, seq: 80 });
const followUp = await world.nextFetch("agent-a");
expect(followUp.request).toEqual({
direction: "after",
cursor: { epoch: "epoch-agent-a", seq: 300 },
limit: 40,
projection: "projected",
wholeTurns: true,
});
followUp.respond({ hasNewer: false, seq: 300 });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
world.sync.dispose();
});
test("reports a chat current only once its parked follow-up page completes", async () => {
const world = new TimelineWorld();
world.sync.setConnected(true);
@@ -742,7 +742,10 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed
await fetchUntilCurrent(
agentId,
generation,
planTimelineCatchUpAfter(page.endCursor),
planTimelineCatchUpPastApplied(
planTimelineCatchUpAfter(page.endCursor),
ports.readCursor(agentId),
),
false,
);
return;