Keep focused agent timelines live and catch up instantly (#2196)

* fix(app): keep focused agent timelines live

Window focus was conflated with app visibility, so switching OS windows could remove the selected timeline subscription. Separate those signals, grace every visibility-driven removal, and cover retained history until authoritative catch-up completes.

* fix(app): keep timeline catch-up failures non-blocking

* fix(app): surface timeline catch-up failures

* fix(app): preserve file preview refocus

* fix(app): preserve optimistic catch-up flow
This commit is contained in:
Mohamed Boudra
2026-07-18 16:30:59 +02:00
committed by GitHub
parent fcaa84f0e4
commit 7917532716
26 changed files with 527 additions and 64 deletions
@@ -1,4 +1,4 @@
import { expect, test } from "vitest";
import { expect, test, vi } from "vitest";
import type { ProjectedTimelineForwardFetchPlan } from "./timeline-sync-plan";
import {
createViewedTimelineSync,
@@ -191,6 +191,7 @@ test("unchanged visible-set publication does not cancel paged catch-up", async (
const world = new TimelineWorld();
world.sync.setConnected(true);
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("pending");
const membership = await world.nextMembership();
membership.succeed();
const firstPage = await world.nextFetch("agent-a");
@@ -198,8 +199,13 @@ test("unchanged visible-set publication does not cancel paged catch-up", async (
world.sync.replaceVisibleAgentIds("workspace", ["agent-a", "agent-a"]);
firstPage.respond({ hasNewer: true, seq: 5 });
const secondPage = await world.nextFetch("agent-a");
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("pending");
secondPage.respond({ hasNewer: false });
await vi.waitFor(() => {
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready");
});
expect(secondPage.request).toEqual({
direction: "after",
cursor: { epoch: "epoch-agent-a", seq: 5 },
@@ -280,6 +286,7 @@ test("disconnect cancels paging and reconnect restores membership before fresh c
const stalePage = await world.nextFetch("agent-a");
world.sync.setConnected(false);
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("pending");
stalePage.respond({ hasNewer: true, seq: 8 });
world.sync.setConnected(true);
const restoredMembership = await world.nextMembership();
@@ -287,6 +294,8 @@ test("disconnect cancels paging and reconnect restores membership before fresh c
const restoredPage = await world.nextFetch("agent-a");
restoredPage.respond({ hasNewer: false });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
expect(restoredMembership.agentIds).toEqual(["agent-a"]);
world.expectNoPendingFetch();
});
@@ -328,10 +337,13 @@ test("a failed catch-up reports once and retries through the explicit retry poli
const failed = await world.nextFetch("agent-a");
failed.fail("timeline unavailable");
const [error, retryCatchUp] = await Promise.all([world.nextError(), world.nextRetry()]);
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error");
retryCatchUp();
const retry = await world.nextFetch("agent-a");
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error");
retry.respond({ hasNewer: false });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
expect({ error, retryDirection: retry.request.direction }).toEqual({
error: "timeline unavailable",
@@ -396,12 +408,15 @@ test("membership failure autonomously retries without another visibility declara
const failed = await world.nextMembership();
failed.fail("subscription unavailable");
const [error, retryMembership] = await Promise.all([world.nextError(), world.nextRetry()]);
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error");
retryMembership();
const retry = await world.nextMembership();
retry.succeed();
const catchUp = await world.nextFetch("agent-a");
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error");
catchUp.respond({ hasNewer: false });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
expect({ error, failed: failed.agentIds, retry: retry.agentIds }).toEqual({
error: "subscription unavailable",
@@ -410,7 +425,7 @@ test("membership failure autonomously retries without another visibility declara
});
});
test("background sends an empty set and foreground restores visible membership", async () => {
test("background waits for grace before unsubscribing and catches up on return", async () => {
const world = new TimelineWorld();
world.sync.setConnected(true);
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
@@ -420,6 +435,8 @@ test("background sends an empty set and foreground restores visible membership",
initialCatchUp.respond({ hasNewer: false });
world.sync.setActive(false);
world.expectNoPendingMembership();
world.runUnsubscribeGrace();
const background = await world.nextMembership();
background.succeed();
world.sync.setActive(true);
@@ -434,6 +451,24 @@ test("background sends an empty set and foreground restores visible membership",
});
});
test("foregrounding within grace preserves the live membership", async () => {
const world = new TimelineWorld();
world.sync.setConnected(true);
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
const membership = await world.nextMembership();
membership.succeed();
const catchUp = await world.nextFetch("agent-a");
catchUp.respond({ hasNewer: false });
world.sync.setActive(false);
world.expectNoPendingMembership();
world.sync.setActive(true);
world.expectNoPendingUnsubscribe();
world.expectNoPendingMembership();
world.expectNoPendingFetch();
});
test("stale membership retry cannot overwrite a newer effective set", async () => {
const world = new TimelineWorld();
world.sync.setConnected(true);
@@ -483,10 +518,16 @@ test("quickly returning to an agent cancels its pending unsubscribe without anot
const initialCatchUp = await world.nextFetch("agent-a");
initialCatchUp.respond({ hasNewer: false });
await vi.waitFor(() => {
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready");
});
world.sync.replaceVisibleAgentIds("workspace", []);
world.expectNoPendingMembership();
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready");
world.expectNoPendingUnsubscribe();
world.expectNoPendingMembership();
world.expectNoPendingFetch();
@@ -500,6 +541,12 @@ test("unsubscribe grace expiry removes the agent exactly once", async () => {
membership.succeed();
const catchUp = await world.nextFetch("agent-a");
catchUp.respond({ hasNewer: false });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
const readinessChanges: string[] = [];
world.sync.subscribe(() => {
readinessChanges.push(world.sync.getAgentTimelineStatus("agent-a"));
});
world.sync.replaceVisibleAgentIds("workspace", []);
world.runUnsubscribeGrace();
@@ -507,6 +554,7 @@ test("unsubscribe grace expiry removes the agent exactly once", async () => {
unsubscribe.succeed();
expect(unsubscribe.agentIds).toEqual([]);
expect(readinessChanges.at(-1)).toBe("pending");
world.expectNoPendingUnsubscribe();
world.expectNoPendingMembership();
});
@@ -533,7 +581,7 @@ test("a new visible agent subscribes immediately while the previous agent linger
expect(settledMembership.agentIds).toEqual(["agent-b"]);
});
test("backgrounding clears pending unsubscribe grace and membership immediately", async () => {
test("backgrounding preserves an existing unsubscribe grace period", async () => {
const world = new TimelineWorld();
world.sync.setConnected(true);
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
@@ -544,25 +592,37 @@ test("backgrounding clears pending unsubscribe grace and membership immediately"
world.sync.replaceVisibleAgentIds("workspace", []);
world.sync.setActive(false);
world.expectNoPendingMembership();
world.runUnsubscribeGrace();
const unsubscribe = await world.nextMembership();
unsubscribe.succeed();
expect(unsubscribe.agentIds).toEqual([]);
world.expectNoPendingUnsubscribe();
world.expectNoPendingMembership();
});
test("disconnecting cancels pending unsubscribe grace without publishing on the closed socket", async () => {
const world = new TimelineWorld();
world.sync.setConnected(true);
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
world.sync.replaceVisibleAgentIds("workspace", ["agent-a", "agent-b"]);
const membership = await world.nextMembership();
membership.succeed();
const catchUp = await world.nextFetch("agent-a");
catchUp.respond({ hasNewer: false });
const [agentA, agentB] = await Promise.all([
world.nextFetch("agent-a"),
world.nextFetch("agent-b"),
]);
agentA.respond({ hasNewer: false });
agentB.respond({ hasNewer: false });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
world.sync.replaceVisibleAgentIds("workspace", []);
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
const agentAStatuses: string[] = [];
world.sync.subscribe(() => {
agentAStatuses.push(world.sync.getAgentTimelineStatus("agent-a"));
});
world.sync.setConnected(false);
expect(agentAStatuses).toEqual(["pending"]);
world.expectNoPendingUnsubscribe();
world.expectNoPendingMembership();
});
@@ -612,13 +672,17 @@ test("switching from legacy to selective delivery publishes membership and catch
world.sync.setConnected(true);
const legacyCatchUp = await world.nextFetch("agent-a");
legacyCatchUp.respond({ hasNewer: false });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
world.sync.setDeliveryMode("selective");
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("pending");
const membership = await world.nextMembership();
membership.succeed();
const catchUp = await world.nextFetch("agent-a");
catchUp.respond({ hasNewer: false });
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
expect(membership.agentIds).toEqual(["agent-a"]);
world.expectNoPendingMembership();
world.expectNoPendingFetch();