Keep older chat history and image previews stable (#2490)
* fix(app): keep timeline history and previews stable Treat persisted timeline replicas as display-only so authoritative pagination always comes from the daemon. Require renewed user intent between older-history pages unless the viewport genuinely remains at the history edge. Model assistant image acquisition as loading, loaded, or failed so recreating a preview URL cannot be rendered as an error. * fix(app): keep file image previews current * fix(app): stabilize history pagination edges * fix(app): recover transient timeline loads * fix(app): retain assistant image previews * Stabilize history pagination lifecycle * Stabilize history settlement and image previews * Retry file images after reconnect * Keep active previews and pagination requests alive * Replace image hook tests with typed ports * Integrate timeline hydration with submission authority * Harden pre-hydration timeline reconciliation * Make rewind E2E use real scroll intent * Preserve live timeline state through hydration * Keep mounted image attachments retained * Protect preview persistence and lifecycle hydration * Preserve idless live assistant continuations * Preserve timeline rows across delayed hydration * test(app): cover real assistant image files * Preserve assistant tool ordering during hydration
This commit is contained in:
@@ -4,6 +4,7 @@ import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
||||
import {
|
||||
normalizeEmptyProjectDescriptor,
|
||||
normalizeWorkspaceDescriptor,
|
||||
selectAgentTimelineState,
|
||||
useSessionStore,
|
||||
} from "@/stores/session-store";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
@@ -166,13 +167,14 @@ describe("ReplicaCache", () => {
|
||||
expect(session?.agents.get("agent-1")?.updatedAt).toBeInstanceOf(Date);
|
||||
expect(session?.workspaces.get("workspace-1")?.statusEnteredAt).toBeInstanceOf(Date);
|
||||
expect(session?.agentStreamTail.get("agent-1")).toEqual([message("message-1", "Cached")]);
|
||||
expect(session?.agentAuthoritativeHistoryApplied.get("agent-1")).toBe(true);
|
||||
expect(session?.agentTimelineCursor.get("agent-1")).toEqual({
|
||||
epoch: "epoch-1",
|
||||
startSeq: 1,
|
||||
endSeq: 12,
|
||||
expect(session?.agentAuthoritativeHistoryApplied).toEqual(new Map());
|
||||
expect(session?.agentTimelineCursor).toEqual(new Map());
|
||||
expect(session?.agentTimelineHasOlder).toEqual(new Map());
|
||||
expect(session?.agentHistorySyncGeneration).toEqual(new Map());
|
||||
expect(selectAgentTimelineState(session, "agent-1")).toEqual({
|
||||
status: "painted",
|
||||
items: [message("message-1", "Cached")],
|
||||
});
|
||||
expect(session?.agentTimelineHasOlder.get("agent-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("persists only the focused agent view with a short timeline tail", async () => {
|
||||
@@ -216,6 +218,13 @@ describe("ReplicaCache", () => {
|
||||
expect(Array.from(session?.emptyProjects.keys() ?? [])).toEqual([]);
|
||||
expect(Array.from(timelines?.keys() ?? [])).toEqual(["agent-2"]);
|
||||
expect(timelines?.get("agent-2")).toEqual(secondTimeline.slice(-50));
|
||||
|
||||
const persisted = JSON.parse(storage.values.get("@paseo:replica-cache") ?? "null") as {
|
||||
version: number;
|
||||
hosts: Array<{ timeline: Record<string, unknown> | null }>;
|
||||
};
|
||||
expect(persisted.version).toBe(2);
|
||||
expect(Object.keys(persisted.hosts[0]?.timeline ?? {}).sort()).toEqual(["agentId", "items"]);
|
||||
});
|
||||
|
||||
it("evicts the least recently written host when the cache exceeds its byte budget", async () => {
|
||||
@@ -243,14 +252,38 @@ describe("ReplicaCache", () => {
|
||||
expect(Object.keys(useSessionStore.getState().sessions).sort()).toEqual(["host-a", "host-c"]);
|
||||
});
|
||||
|
||||
it("drops malformed or unknown cache versions", async () => {
|
||||
it("rejects version 1 cache data and overwrites it on flush", async () => {
|
||||
const storage = new MemoryStorage();
|
||||
storage.values.set("@paseo:replica-cache", JSON.stringify({ version: 999, hosts: [] }));
|
||||
storage.values.set(
|
||||
"@paseo:replica-cache",
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
hosts: [
|
||||
{
|
||||
serverId: SERVER_ID,
|
||||
agents: [],
|
||||
workspaces: [],
|
||||
emptyProjects: [],
|
||||
timeline: {
|
||||
agentId: "agent-1",
|
||||
items: [],
|
||||
cursor: { epoch: "poisoned", startSeq: 1, endSeq: 100 },
|
||||
hasOlder: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const cache = new ReplicaCache(storage);
|
||||
cache.setHosts([SERVER_ID]);
|
||||
|
||||
await cache.restore();
|
||||
await cache.flush();
|
||||
|
||||
expect(useSessionStore.getState().sessions[SERVER_ID]).toBeUndefined();
|
||||
expect(JSON.parse(storage.values.get("@paseo:replica-cache") ?? "null")).toEqual({
|
||||
version: 2,
|
||||
hosts: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user