fix(agent): handle partial room timestamps without crashing

This commit is contained in:
2026-10-25 10:16:37 -07:00
parent b743523fd5
commit a68caac378
7 changed files with 218 additions and 27 deletions
+86 -1
View File
@@ -21,6 +21,91 @@ const sourceA = "4599d348-e053-4937-987d-1392746fb2ce", sourceB = "22222222-2222
const deferred = () => { let resolve; const promise = new Promise(yes => { resolve = yes; }); return { promise, resolve }; };
const sourceRoom = () => applyAgentPage(applyAgentReadState(createAgentRoomState(), { sourceId: sourceA, cursor: "90", updatedAt: null }), page("100", [message(100)]));
test("redacted historical tool rows survive REST loading, socket replay and outbox rendering", async () => {
const redacted = { id: "tool:1:protected", role: "tool", name: "owner_phone_context" };
const input = page("3", [message(1), redacted, message(3)]);
const client = new AgentRoomClient({ fetch: async () => Response.json(input) });
let state = createAgentRoomState();
await syncAgentHistory(client, "0", update => { state = applyAgentPage(state, update); });
const initial = state.messages;
assert.deepEqual((await client.history()).messages, (await client.messages("0")).messages,
"recent/older history uses the same boundary as forward replay");
const row = initial.find(row => row.id === redacted.id);
assert.equal(row.createdAt, "1970-01-01T00:00:00.000Z", "unknown times are stable, never fabricated as now");
assert.equal(row.content, "owner_phone_context");
assert.equal(state.cursor, "3");
const event = parseAgentRoomEvent(JSON.stringify({ type: "messages", ...input }));
state = applyAgentEvent(state, event);
assert.deepEqual(state.messages, initial, "replaying partial history is idempotent");
const box = createAgentOptimisticOutbox({ submit: async () => assert.fail("no send"),
storage: { read: () => [], write: () => {} }, changed: () => {} });
assert.deepEqual(box.display(agentDisplayMessages(state)), initial);
});
test("partial tool updates retain known time, terminal metadata and chronological ordering", () => {
const id = "tool:1:partial";
const start = message(2, "tool", { id, metadata: { kind: "tool_call", entryId: 2, status: "initiated", startedAt: "2026-10-14T10:00:02.000Z" } });
const done = message(3, "tool", { id, createdAt: undefined, metadata: { kind: "tool_call", entryId: 3, status: "completed", completedAt: "2026-10-14T10:00:03.000Z" } });
const partial = { id, role: "tool", name: "owner_phone_context" };
const rows = mergeAgentMessages([message(1), start], [done, partial, start, message(4)]);
assert.deepEqual(rows.map(row => row.id), ["entry:1", id, "entry:4"]);
assert.equal(rows[1].createdAt, start.createdAt);
assert.equal(rows[1].metadata.status, "completed");
assert.equal(rows[1].metadata.completedAt, done.metadata.completedAt);
assert.equal(mergeAgentMessages([], [done])[0].createdAt, done.metadata.completedAt);
assert.equal(mergeAgentMessages([partial], [start])[0].createdAt, start.createdAt);
});
test("an unknown historical tool start never masks a known completion timestamp", () => {
const unknown = "1970-01-01T00:00:00.000Z", known = "2026-10-14T10:00:03.000Z";
const start = message(2, "tool", { id: "tool:1:historical", createdAt: unknown,
metadata: { kind: "tool_call", entryId: 2, status: "initiated", startedAt: unknown } });
const done = message(3, "tool", { id: start.id, createdAt: known,
metadata: { kind: "tool_call", entryId: 3, status: "completed", completedAt: known } });
for (const rows of [[start, done], [done, start], [{ ...start, metadata: { ...start.metadata, ...done.metadata } }]]) {
const merged = mergeAgentMessages([], rows);
assert.equal(merged[0].createdAt, known);
assert.equal(merged[0].metadata.status, "completed");
}
});
test("missing, null, invalid and tied timestamps have stable ordering independent of arrival order", () => {
const rows = [message(11, "assistant", { createdAt: null }), message(2, "assistant", { createdAt: undefined }),
message(3, "assistant", { createdAt: "not-a-date" }), message(4, "assistant", { createdAt: 0 }),
message(5, "assistant", { createdAt: null, metadata: { kind: "message" } })];
const expected = ["entry:2", "entry:3", "entry:4", "entry:11", "entry:5"];
assert.deepEqual(mergeAgentMessages([], rows).map(row => row.id), expected);
assert.deepEqual(mergeAgentMessages([], [...rows].reverse()).map(row => row.id), expected);
const tied = [message(11, "assistant", { createdAt: "2026-10-14T10:00:00Z" }), message(2, "assistant", { createdAt: "2026-10-14T11:00:00+01:00" })];
assert.deepEqual(mergeAgentMessages([], tied).map(row => row.id), ["entry:2", "entry:11"]);
});
test("REST descriptors and socket activity normalize partial queued messages consistently", async () => {
const pending = message(1, "user", { id: "inbound:sybil:partial", createdAt: undefined, metadata: { kind: "message", status: "queued" } });
const value = { id: "1", activity: activity([pending]) };
const client = new AgentRoomClient({ fetch: async () => Response.json(value) });
const descriptor = await client.descriptor();
const fromRest = applyAgentEvent(createAgentRoomState(), { type: "activity", conversationId: descriptor.id, activity: descriptor.activity });
const fromSocket = applyAgentEvent(createAgentRoomState(), parseAgentRoomEvent(JSON.stringify({ type: "activity", conversationId: "1", activity: value.activity })));
assert.deepEqual(agentDisplayMessages(fromRest), agentDisplayMessages(fromSocket));
assert.equal(agentDisplayMessages(fromRest)[0].createdAt, "1970-01-01T00:00:00.000Z");
const malformed = { ...pending, content: undefined };
assert.throws(() => applyAgentEvent(fromRest, { type: "activity", conversationId: "1", activity: activity([malformed]) }), /Invalid agent message/);
assert.equal(fromRest.cursor, "0");
});
test("invalid message bodies fail at both transport boundaries before advancing replay", async () => {
for (const malformed of [null, { role: "assistant", content: "missing ID" }, message(2, "assistant", { content: null }), message(2, ["assistant"])]) {
const input = page("3", [message(1), malformed]);
const client = new AgentRoomClient({ fetch: async () => Response.json(input) });
let applied = false;
await assert.rejects(syncAgentHistory(client, "0", () => { applied = true; }), /Invalid agent message/);
await assert.rejects(client.history(), /Invalid agent message/);
assert.equal(applied, false);
assert.throws(() => parseAgentRoomEvent(JSON.stringify({ type: "messages", ...input })), /Invalid agent message/);
}
});
test("tool starts and results merge across replay without downgrading completed cards", () => {
const start = message(2, "tool", { id: "tool:1:call1", metadata: { kind: "tool_call", entryId: 2, status: "initiated", args: { query: "cookie" }, startedAt: "2026-10-14T10:00:00Z" } });
const done = message(3, "tool", { id: start.id, metadata: { kind: "tool_call", entryId: 3, status: "completed", resultPreview: "found" } });
@@ -312,7 +397,7 @@ test("client uses Sybil auth and routes while websocket credentials remain in th
let token = "first-session-token";
const client = new AgentRoomClient({ baseUrl: "https://sybil.example/api/", token: () => token, fetch: async (url, init) => {
calls.push({ url, init }); return new Response(JSON.stringify(init.method === "POST"
? { id: 1, status: "queued", messageId: "inbound:sybil:request-1" } : { cursor: "0", updatedAt: null }), { status: 200 });
? { id: 1, status: "queued", messageId: "inbound:sybil:request-1" } : url.includes("/messages?") ? page("0") : { cursor: "0", updatedAt: null }), { status: 200 });
} });
await client.messages("0");
token = "second-session-token";