web: open Agent at recent history and load older pages on demand

This commit is contained in:
2026-10-16 21:18:15 -07:00
parent 671ee98ab1
commit 7b48201e28
9 changed files with 490 additions and 28 deletions
+74 -1
View File
@@ -4,6 +4,7 @@ import {
AgentRoomClient, AgentReadSourceChangedError, advanceAgentCursor, agentAuthFrame, agentDisplayMessages, agentUnreadCount,
applyAgentEvent, applyAgentPage, applyAgentReadState, createAgentReadSession, createAgentReadWriter,
createAgentRoomState, mergeAgentMessages, normalizeAgentCursor, parseAgentRoomEvent, syncAgentHistory,
applyAgentHistoryPage, agentUnreadCountIsPartial,
} from "../src/lib/agent-room.ts";
import { createAgentOptimisticOutbox } from "../src/lib/agent-optimistic-outbox.ts";
@@ -32,7 +33,7 @@ test("tool starts and results merge across replay without downgrading completed
assert.equal(messages[0].createdAt, start.createdAt);
});
test("initial synchronization reads every page including invisible entries and counts only unread assistant messages", async () => {
test("forward recovery reads every missing page including invisible entries and counts only unread assistant messages", async () => {
const requests = [];
const responses = [page("2", [message(1), message(2, "user")], true), page("4", [], true), page("7", [message(5), message(6, "tool"), message(7)], false)];
let state = createAgentRoomState();
@@ -47,6 +48,78 @@ test("initial synchronization reads every page including invisible entries and c
assert.equal(agentUnreadCount(state), 1);
});
test("recent history is one cursorless window and older history uses the returned source boundary", async () => {
const requests = [];
const client = new AgentRoomClient({ fetch: async (url, options) => {
requests.push({ url, signal: options.signal });
return new Response(JSON.stringify({ ...page("1200", [message(1200)], true), beforeCursor: "1101" }));
} });
const controller = new AbortController();
let state = applyAgentHistoryPage(createAgentRoomState(), await client.history(undefined, controller.signal));
assert.equal(requests.length, 1, "hasMore must not drain old history");
assert.equal(requests[0].url, "/api/v1/agent/messages?limit=100");
assert.equal(requests[0].signal, controller.signal);
assert.equal(state.historyLoaded, true);
assert.equal(state.cursor, "1200");
assert.equal(state.beforeCursor, "1101", "boundary is not derived from visible message 1200");
assert.equal(state.hasOlder, true);
await client.history(state.beforeCursor, controller.signal);
assert.equal(requests[1].url, "/api/v1/agent/messages?before=1101&limit=100");
});
test("delayed older pages preserve newer edits, live cursor, source and read state", () => {
let state = applyAgentHistoryPage(sourceRoom(), { ...page("100", [message(100)], true), beforeCursor: "90" });
state = applyAgentPage(state, page("110", [message(100, "assistant", { content: "New streamed text" }), message(110)]));
const next = applyAgentHistoryPage(state, { ...page("89", [message(80), message(100, "assistant", { content: "Old snapshot" })], true), beforeCursor: "70" }, "90");
assert.equal(next.messages.find(m => m.id === "entry:100").content, "New streamed text");
assert.equal(next.cursor, "110");
assert.equal(next.readCursor, "90");
assert.equal(next.sourceId, sourceA);
assert.equal(next.beforeCursor, "70");
assert.equal(next.hasOlder, true);
assert.equal(applyAgentHistoryPage(next, page("89", [message(80)], false), "90"), next, "retired boundary cannot replace the next window");
});
test("empty internal pages advance older boundaries without chasing another page or moving read state", () => {
const initial = applyAgentHistoryPage(createAgentRoomState(), { ...page("1200", [], true), beforeCursor: "1101" });
assert.equal(initial.historyLoaded, true);
assert.equal(initial.messages.length, 0);
const next = applyAgentHistoryPage(initial, { ...page("1100", [], true), beforeCursor: "1001" }, "1101");
assert.equal(next.beforeCursor, "1001");
assert.equal(next.cursor, "1200");
assert.equal(next.readCursor, "0");
assert.equal(next.hasOlder, true);
const end = applyAgentHistoryPage(next, { ...page("0"), beforeCursor: "1001" }, "1001");
assert.equal(end.hasOlder, false);
assert.equal(end.cursor, "1200");
});
test("malformed or stalled older pages cannot change history or delivery boundaries", () => {
const state = applyAgentHistoryPage(createAgentRoomState(), { ...page("100", [message(100)], true), beforeCursor: "90" });
for (const bad of [
{ ...page("89", [], true), beforeCursor: "90" },
{ ...page("89", [], true), beforeCursor: null },
{ ...page("91", [], false), beforeCursor: "80" },
{ ...page("89", [], false), beforeCursor: "91" },
{ ...page("bad", [], false), beforeCursor: "80" },
]) assert.throws(() => applyAgentHistoryPage(state, bad, "90"), /cursor|boundary/);
assert.throws(() => applyAgentHistoryPage(state, { ...page("89"), conversationId: "different" }, "90"), /conversation changed/);
assert.equal(state.beforeCursor, "90");
assert.equal(state.cursor, "100");
});
test("unread counts disclose unknown older replies until reading the latest rendered window", () => {
let state = applyAgentHistoryPage(createAgentRoomState(), { ...page("1200", [message(1199), message(1200, "user")], true), beforeCursor: "1101" });
assert.equal(agentUnreadCount(state), 1);
assert.equal(agentUnreadCountIsPartial(state), true);
state = applyAgentReadState(state, { cursor: "1100", updatedAt: null });
assert.equal(agentUnreadCountIsPartial(state), false);
assert.equal(agentUnreadCount(state), 1);
state = applyAgentReadState(state, { cursor: "1200", updatedAt: null });
assert.equal(agentUnreadCount(state), 0);
assert.equal(state.hasOlder, true, "reading latest does not fetch or discard older pages");
});
test("queued user messages keep stable IDs and disappear if withdrawn or promoted", () => {
const pending = message(1, "user", { id: "inbound:sybil:request1", metadata: { kind: "message", status: "queued" } });
let state = applyAgentEvent(createAgentRoomState(), { type: "activity", conversationId: "1", activity: activity([pending]) });