Revert the timeline optimistic and pagination rework (#2596)

* Add npm release track invariant and beta dist-tag procedure

* Revert "Keep older chat history and image previews stable (#2490)"

The timeline optimistic/pagination rework needs more work than main releases can wait for. Reverted together with #2484; both are reapplied on integration/timeline-optimistic-rework.

* Revert "Stop completed turns from appearing stuck (#2484)"

See the #2490 revert. #2490 built on this submission-authority change, so the two revert and reapply as a pair.
This commit is contained in:
Mohamed Boudra
2026-07-29 13:35:43 +02:00
committed by GitHub
parent ab24070075
commit acb8506d1f
81 changed files with 1721 additions and 7893 deletions
+1 -64
View File
@@ -80,16 +80,13 @@ class FakeDaemonClient {
this.setConnectionState({ status: "disconnected", reason: "client_closed" });
}
async sendAgentMessage(
...args: Parameters<DaemonClient["sendAgentMessage"]>
): ReturnType<DaemonClient["sendAgentMessage"]> {
async sendAgentMessage(...args: Parameters<DaemonClient["sendAgentMessage"]>): Promise<void> {
this.sentAgentMessages.push(args);
for (const waiter of this.sentMessageWaiters) waiter();
const response = this.sendAgentMessageResponses.shift();
if (response) await response;
const failure = this.sendAgentMessageFailures.shift();
if (failure) throw failure;
return {};
}
async waitForSentMessages(count: number): Promise<void> {
@@ -2262,66 +2259,6 @@ describe("HostRuntimeStore", () => {
useSessionStore.getState().clearSession(host.serverId);
});
it("submits an automatically drained message through the submission producer", async () => {
const host = makeHost({ serverId: "srv_drain_submission" });
const fakeClient = new FakeDaemonClient();
const send = new Deferred<void>();
fakeClient.sendAgentMessageResponses.push(send.promise);
const store = new HostRuntimeStore({
deps: {
createClient: () => fakeClient as unknown as DaemonClient,
connectToDaemon: async () => ({
client: fakeClient as unknown as DaemonClient,
serverId: host.serverId,
hostname: null,
}),
getClientId: async () => "cid_drain_submission",
},
});
const sessionStore = useSessionStore.getState();
sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1);
sessionStore.setQueuedMessages(
host.serverId,
new Map([
[
"agent",
[
{
id: "queued-with-attachment",
text: "read this file",
attachments: [
{
kind: "workspace_file" as const,
path: "src/main.ts",
selection: { kind: "whole_file" as const },
},
],
},
],
],
]),
);
store.drainQueuedAgentMessage(host.serverId, "agent");
await fakeClient.waitForSentMessages(1);
// The row and the pending submission must exist while the RPC is still in flight —
// the user sees their message and the working footer immediately, exactly as when
// they press send.
const session = useSessionStore.getState().sessions[host.serverId];
const tail = session?.agentStreamTail.get("agent") ?? [];
expect(tail).toHaveLength(1);
expect(tail[0]).toMatchObject({
kind: "user_message",
text: "read this file",
attachments: [{ type: "text", title: "main.ts", text: "Workspace file: src/main.ts" }],
});
expect(session?.messageSubmissions.get("agent")).toBeDefined();
send.resolve();
useSessionStore.getState().clearSession(host.serverId);
});
it("restores an automatically drained message when sending fails", async () => {
const host = makeHost({ serverId: "srv_failed_queue_drain" });
const fakeClient = new FakeDaemonClient();
+13 -13
View File
@@ -49,9 +49,11 @@ import {
} from "@/data/push-router";
import { mountBrowserAutomationDaemonClientHandler } from "@/browser-automation/handler";
import { schedulesQueryBaseKey } from "@/schedules/aggregated-schedules";
import { dispatchComposerAgentMessage, sendQueuedComposerMessageNow } from "@/composer/actions";
import { createMessageSubmissionWriter } from "@/composer/submission/writer";
import { resolveComposerAttachmentSubmitFormat } from "@/composer/attachments/submit";
import { sendQueuedComposerMessageNow } from "@/composer/actions";
import {
resolveComposerAttachmentSubmitFormat,
splitComposerAttachmentsForSubmit,
} from "@/composer/attachments/submit";
import { encodeImages } from "@/utils/encode-images";
import { DirectorySync, type RefreshAgentDirectoryResult } from "@/runtime/directory-sync";
import { ReplicaCache } from "@/runtime/replica-cache";
@@ -2069,16 +2071,14 @@ export class HostRuntimeStore {
submitMessage: async ({ text, attachments }) => {
const supportsForgeAttachments =
useSessionStore.getState().sessions[serverId]?.serverInfo?.features?.forgeSearch === true;
await dispatchComposerAgentMessage({
client,
agentId,
text,
attachments,
attachmentSubmitFormat: resolveComposerAttachmentSubmitFormat({
supportsForgeAttachments,
}),
encodeImages,
submission: createMessageSubmissionWriter(serverId),
const wirePayload = splitComposerAttachmentsForSubmit(attachments, {
format: resolveComposerAttachmentSubmitFormat({ supportsForgeAttachments }),
});
const images = await encodeImages(wirePayload.images);
await client.sendAgentMessage(agentId, text, {
messageId: next.id,
...(images && images.length > 0 ? { images } : {}),
attachments: wirePayload.attachments,
});
},
})
@@ -4,7 +4,6 @@ import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
import {
normalizeEmptyProjectDescriptor,
normalizeWorkspaceDescriptor,
selectAgentTimelineState,
useSessionStore,
} from "@/stores/session-store";
import type { StreamItem } from "@/types/stream";
@@ -167,14 +166,13 @@ 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).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?.agentAuthoritativeHistoryApplied.get("agent-1")).toBe(true);
expect(session?.agentTimelineCursor.get("agent-1")).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 12,
});
expect(session?.agentTimelineHasOlder.get("agent-1")).toBe(true);
});
it("persists only the focused agent view with a short timeline tail", async () => {
@@ -218,13 +216,6 @@ 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 () => {
@@ -252,38 +243,14 @@ describe("ReplicaCache", () => {
expect(Object.keys(useSessionStore.getState().sessions).sort()).toEqual(["host-a", "host-c"]);
});
it("rejects version 1 cache data and overwrites it on flush", async () => {
it("drops malformed or unknown cache versions", async () => {
const storage = new MemoryStorage();
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,
},
},
],
}),
);
storage.values.set("@paseo:replica-cache", JSON.stringify({ version: 999, hosts: [] }));
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: [],
});
});
});
+14 -21
View File
@@ -11,7 +11,6 @@ import {
import {
normalizeEmptyProjectDescriptor,
normalizeWorkspaceDescriptor,
selectAgentTimelineState,
useSessionStore,
type Agent,
type SessionReplica,
@@ -20,10 +19,9 @@ import {
} from "@/stores/session-store";
import type { StreamItem } from "@/types/stream";
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
import { getSendingClientMessageIds } from "@/composer/submission/model";
const STORAGE_KEY = "@paseo:replica-cache";
const CACHE_VERSION = 2;
const CACHE_VERSION = 1;
const PERSIST_DELAY_MS = 750;
const MAX_TIMELINE_ITEMS = 50;
const MAX_CACHE_BYTES = 1024 * 1024;
@@ -38,6 +36,14 @@ const StoredAgentSchema = z.object({
const StoredTimelineSchema = z.object({
agentId: z.string(),
items: z.unknown(),
cursor: z
.object({
epoch: z.string(),
startSeq: z.number().int().nonnegative(),
endSeq: z.number().int().nonnegative(),
})
.nullable(),
hasOlder: z.boolean(),
});
const StoredHostSchema = z.object({
@@ -134,6 +140,8 @@ function deserializeTimeline(stored: StoredHost["timeline"]): SessionReplica["ti
return {
agentId: stored.agentId,
items: decoded,
cursor: stored.cursor,
hasOlder: stored.hasOlder,
};
}
@@ -362,29 +370,14 @@ export class ReplicaCache {
(workspace) => workspace.workspaceDirectory === focusedAgent.cwd,
))
: undefined;
const localSubmissionIds = new Set(
getSendingClientMessageIds(
focusedAgentId ? session.messageSubmissions.get(focusedAgentId) : undefined,
),
);
const timelineState = focusedAgentId
? selectAgentTimelineState(session, focusedAgentId)
: { status: "cold" as const };
const items =
timelineState.status === "cold"
? undefined
: timelineState.items.filter(
(item) =>
item.kind !== "user_message" ||
item.messageId !== undefined ||
!item.clientMessageId ||
!localSubmissionIds.has(item.clientMessageId),
);
const items = focusedAgentId ? session.agentStreamTail.get(focusedAgentId) : undefined;
const timeline =
focusedAgent && items
? {
agentId: focusedAgent.id,
items: encodeDates(items.slice(-MAX_TIMELINE_ITEMS)),
cursor: session.agentTimelineCursor.get(focusedAgent.id) ?? null,
hasOlder: session.agentTimelineHasOlder.get(focusedAgent.id) ?? false,
}
: null;
const stored: StoredHost = {