import assert from "node:assert/strict"; import test from "node:test"; import { buildTranscript } from "./agentSessionTranscript.ts"; import { buildTranscriptDisplayBlocks, flattenDisplayBlocks, } from "./agentSessionTranscriptGrouping.ts"; import { formatToolTitle } from "./agentSessionToolCatalog.ts"; const baseEvent = { seq: 1, timestamp: "2026-06-18T00:00:00Z", kind: "acp_write", agentIndex: 0, channelId: "11111111-1111-1111-1111-111111111111", sessionId: "sess-1", turnId: "turn-1", }; const PROMPT_EVENT_ID = "c".repeat(64); function acpToolUpdate(seq, update) { return { ...baseEvent, seq, kind: "acp_read", payload: { method: "session/update", params: { sessionId: baseEvent.sessionId, update, }, }, }; } function toolItems(events) { return buildTranscript(events).filter((item) => item.type === "tool"); } function activityTitle(item) { return formatToolTitle(item.buzzToolName ?? item.toolName, item.title); } // --- stub-overflow vanish (pins the pre-existing degraded-frame behavior) --- test("buildTranscript drops a session/prompt turn whose frame was stubbed by the size trimmer", () => { // When fit_observer_event_to_budget cannot shrink a frame below the cap it // replaces the whole payload with {elided, originalBytes} (no `method`), so // the method-keyed acp_write dispatch matches no arm and there is no terminal // else: the turn produces ZERO transcript items. This is worse than a // "1 section" collapse (the item vanishes entirely) and is pre-existing, // outside the format_prompt seam. Pin it so a later change can't silently // regress the vanish-vs-degrade behavior without updating this test. const stubbed = { ...baseEvent, payload: { elided: "acp_write payload too large", originalBytes: 123456, }, }; assert.deepEqual(buildTranscript([stubbed]), []); }); // --- positive control: a well-formed multi-block prompt DOES render --- test("buildTranscript renders Prompt context + user message for a multi-block session/prompt frame", () => { // Guards the vanish assertion above against a false pass from a broken // import or dispatch: a normal per-section prompt frame must still produce a // user message and a "Prompt context" metadata item. const event = { ...baseEvent, payload: { method: "session/prompt", params: { sessionId: "sess-1", prompt: [ { type: "text", text: "[Agent Memory — core]\nremember this" }, { type: "text", text: "[Context]\nScope: thread" }, { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${PROMPT_EVENT_ID.toUpperCase()}\nFrom: x (hex: ${"a".repeat(64)})\nContent: hello`, }, ], }, }, }; const items = buildTranscript([event]); const titles = items.map((i) => i.title); assert.ok( items.some((i) => i.type === "metadata" && i.title === "Prompt context"), `expected a Prompt context metadata item, got titles: ${titles.join(", ")}`, ); const promptContext = items.find((i) => i.title === "Prompt context"); assert.deepEqual( promptContext.sections.map((s) => s.title), ["Agent Memory — core", "Context", "Buzz event: @mention"], "every section header is counted", ); const userMessage = items.find((i) => i.type === "message"); assert.equal(userMessage.messageId, PROMPT_EVENT_ID); }); test("buildTranscript falls back to a single turn trigger id for older prompt frames", () => { const promptEvent = { ...baseEvent, seq: 2, payload: { method: "session/prompt", params: { sessionId: "sess-1", prompt: [ { type: "text", text: `[Buzz event: @mention]\nFrom: x (hex: ${"a".repeat(64)})\nContent: hello`, }, ], }, }, }; const [userMessage] = buildTranscript([ { ...baseEvent, seq: 1, kind: "turn_started", payload: { triggeringEventIds: [PROMPT_EVENT_ID], }, }, promptEvent, ]).filter((candidate) => candidate.type === "message"); assert.equal(userMessage.messageId, PROMPT_EVENT_ID); }); test("buildTranscript keeps read_file activity categorized by the actual tool when output names Buzz tools", () => { const [item] = toolItems([ acpToolUpdate(10, { sessionUpdate: "tool_call", toolCallId: "call-read-file", status: "executing", title: "read_file", kind: "read_file", rawInput: { path: "desktop/src/features/agents/ui/agentSessionToolCatalog.ts", }, }), acpToolUpdate(11, { sessionUpdate: "tool_call_update", toolCallId: "call-read-file", status: "completed", title: "read_file", kind: "read_file", rawInput: { path: "desktop/src/features/agents/ui/agentSessionToolCatalog.ts", }, content: { type: "text", text: 'const BUZZ_READ_TOOLS = new Set(["get_feed", "get_event"]);\nconst BUZZ_WRITE_TOOLS = new Set(["delete_message"]);', }, }), ]); assert.equal(item.toolName, "read_file"); assert.equal(item.buzzToolName, null); assert.equal(item.title, "read_file"); assert.equal(activityTitle(item), "read_file"); assert.equal(item.status, "completed"); assert.match(item.result, /get_feed/); assert.match(item.result, /delete_message/); }); test("buildTranscript keeps shell activity categorized by the actual tool when grep output names Buzz tools", () => { const [item] = toolItems([ acpToolUpdate(20, { sessionUpdate: "tool_call", toolCallId: "call-shell-rg", status: "executing", title: "shell", kind: "shell", rawInput: { command: 'rg -n "get_event|delete_message" desktop/src', }, }), acpToolUpdate(21, { sessionUpdate: "tool_call_update", toolCallId: "call-shell-rg", status: "completed", title: "shell", kind: "shell", rawInput: { command: 'rg -n "get_event|delete_message" desktop/src', }, rawOutput: 'desktop/src/features/agents/ui/agentSessionToolCatalog.ts:83: "get_event",\n' + 'desktop/src/features/agents/ui/agentSessionToolCatalog.ts:92: "delete_message",', }), ]); assert.equal(item.toolName, "shell"); assert.equal(item.buzzToolName, null); assert.equal(activityTitle(item), "shell"); assert.equal(item.status, "completed"); assert.match(item.result, /get_event/); assert.match(item.result, /delete_message/); }); test("buildTranscript categorizes explicit Buzz tool calls for the activity bar", () => { const [item] = toolItems([ acpToolUpdate(30, { sessionUpdate: "tool_call", toolCallId: "call-get-feed", status: "executing", title: "Tool call", toolName: "get_feed", rawInput: { limit: 20 }, }), acpToolUpdate(31, { sessionUpdate: "tool_call_update", toolCallId: "call-get-feed", status: "completed", title: "Tool call", toolName: "get_feed", content: { type: "text", text: "[]" }, }), ]); assert.equal(item.toolName, "get_feed"); assert.equal(item.buzzToolName, "get_feed"); assert.equal(activityTitle(item), "Get Feed"); assert.deepEqual(item.args, { limit: 20 }); assert.equal(item.status, "completed"); }); function sessionUpdate(seq, update, overrides = {}) { return { ...baseEvent, ...overrides, seq, kind: "acp_read", payload: { method: "session/update", params: { sessionId: overrides.sessionId ?? baseEvent.sessionId, update, }, }, }; } function assistantChunk(seq, messageId, text, overrides = {}) { return sessionUpdate( seq, { sessionUpdate: "agent_message_chunk", messageId, content: { type: "text", text }, }, overrides, ); } test("buildTranscript preserves author pubkeys on user message chunks", () => { const authorPubkey = "b".repeat(64); const [item] = buildTranscript([ sessionUpdate(25, { sessionUpdate: "user_message_chunk", messageId: "user-chunk-1", authorPubkey, content: { type: "text", text: "please keep this visible" }, }), ]).filter((candidate) => candidate.type === "message"); assert.equal(item.role, "user"); assert.equal(item.authorPubkey, authorPubkey); assert.equal(item.messageId, null); }); test("buildTranscript preserves real event ids on user message chunks", () => { const authorPubkey = "b".repeat(64); const messageId = "d".repeat(64); const [item] = buildTranscript([ sessionUpdate(26, { sessionUpdate: "user_message_chunk", messageId, authorPubkey, content: { type: "text", text: "this came from a channel message" }, }), ]).filter((candidate) => candidate.type === "message"); assert.equal(item.role, "user"); assert.equal(item.messageId, messageId); }); test("buildTranscript de-duplicates repeated tool updates into one canonical row", () => { const items = toolItems([ acpToolUpdate(40, { sessionUpdate: "tool_call", toolCallId: "call-dupe", status: "executing", title: "shell", kind: "shell", rawInput: { command: "echo hi" }, }), acpToolUpdate(41, { sessionUpdate: "tool_call_update", toolCallId: "call-dupe", status: "completed", title: "shell", kind: "shell", rawOutput: "hi", }), acpToolUpdate(42, { sessionUpdate: "tool_call_update", toolCallId: "call-dupe", status: "completed", title: "shell", kind: "shell", rawOutput: "hi", }), ]); assert.equal(items.length, 1); assert.equal(items[0].id, `tool:${baseEvent.channelId}:call-dupe`); assert.equal(items[0].status, "completed"); assert.equal(items[0].result, "hi"); }); test("buildTranscript keeps a completed tool terminal when a late executing call arrives", () => { const [item] = toolItems([ acpToolUpdate(50, { sessionUpdate: "tool_call_update", toolCallId: "call-regression", status: "completed", title: "shell", kind: "shell", rawOutput: "done", }), acpToolUpdate(51, { sessionUpdate: "tool_call", toolCallId: "call-regression", status: "executing", title: "shell", kind: "shell", rawInput: { command: "echo done" }, }), ]); assert.equal(item.status, "completed"); assert.equal(item.completedAt, baseEvent.timestamp); assert.deepEqual(item.args, { command: "echo done" }); assert.equal(item.result, "done"); }); test("buildTranscript rebuilds out-of-order tool frames as one canonical row with retained ids", () => { const [item] = toolItems([ sessionUpdate( 60, { sessionUpdate: "tool_call_update", toolCallId: "call-out-of-order", status: "completed", title: "read_file", kind: "read_file", rawOutput: "file contents", }, { channelId: "22222222-2222-2222-2222-222222222222", sessionId: "sess-2", turnId: "turn-2", timestamp: "2026-06-18T00:00:05Z", }, ), sessionUpdate( 61, { sessionUpdate: "tool_call", toolCallId: "call-out-of-order", status: "executing", title: "read_file", kind: "read_file", rawInput: { path: "AGENTS.md" }, }, { channelId: "22222222-2222-2222-2222-222222222222", sessionId: "sess-2", turnId: "turn-2", timestamp: "2026-06-18T00:00:04Z", }, ), ]); assert.equal( item.id, "tool:22222222-2222-2222-2222-222222222222:call-out-of-order", ); assert.equal(item.status, "completed"); assert.deepEqual(item.args, { path: "AGENTS.md" }); assert.equal(item.channelId, "22222222-2222-2222-2222-222222222222"); assert.equal(item.turnId, "turn-2"); assert.equal(item.sessionId, "sess-2"); }); test("buildTranscript coalesces assistant chunks until the message is sealed", () => { const messages = buildTranscript([ assistantChunk(70, "msg-1", "Hello "), assistantChunk(71, "msg-1", "world"), ]).filter((item) => item.type === "message" && item.role === "assistant"); assert.equal(messages.length, 1); assert.equal(messages[0].text, "Hello world"); assert.equal(messages[0].id, `assistant:${baseEvent.channelId}:msg-1`); }); test("buildTranscript starts a continuation for same-message chunks after sealing", () => { const messages = buildTranscript([ assistantChunk(80, "msg-2", "First"), acpToolUpdate(81, { sessionUpdate: "tool_call", toolCallId: "call-seal", status: "executing", title: "shell", kind: "shell", }), assistantChunk(82, "msg-2", "Second"), ]).filter((item) => item.type === "message" && item.role === "assistant"); assert.equal(messages.length, 2); assert.equal(messages[0].text, "First"); assert.equal(messages[1].text, "Second"); assert.match(messages[1].id, /:c\d+$/); }); test("buildTranscript preserves channel, turn, and session ids through message updates", () => { const [message] = buildTranscript([ assistantChunk(90, "msg-identity", "One ", { channelId: "33333333-3333-3333-3333-333333333333", sessionId: "sess-identity", turnId: "turn-identity", }), assistantChunk(91, "msg-identity", "Two", { channelId: "33333333-3333-3333-3333-333333333333", sessionId: null, turnId: null, }), ]).filter((item) => item.type === "message" && item.role === "assistant"); assert.equal(message.text, "One Two"); assert.equal(message.channelId, "33333333-3333-3333-3333-333333333333"); assert.equal(message.turnId, "turn-identity"); assert.equal(message.sessionId, "sess-identity"); }); test("buildTranscript promotes ACP plan updates to first-class plan items", () => { const items = buildTranscript([ sessionUpdate(90, { sessionUpdate: "plan", content: { type: "text", text: "- [ ] Build registry" }, }), ]); assert.equal(items.length, 1); assert.equal(items[0].type, "plan"); assert.equal(items[0].renderClass, "plan"); assert.match(items[0].text, /Build registry/); }); test("buildTranscript parses standard ACP plan entries[] into a checklist", () => { const items = buildTranscript([ sessionUpdate(90, { sessionUpdate: "plan", entries: [ { status: "completed", content: "Read the ticket", priority: "medium" }, { status: "in_progress", content: "Write the fix", priority: "medium" }, { status: "pending", content: "Open the PR", priority: "medium" }, ], }), ]); assert.equal(items.length, 1); assert.equal(items[0].type, "plan"); assert.equal(items[0].renderClass, "plan"); assert.equal( items[0].text, [ "- [x] Read the ticket", "- [ ] Write the fix (in progress)", "- [ ] Open the PR", ].join("\n"), ); }); test("buildTranscript summarizes plan entries[] updates with correct N/M complete", () => { const items = buildTranscript([ sessionUpdate(90, { sessionUpdate: "plan", entries: [ { status: "pending", content: "Read the ticket" }, { status: "pending", content: "Write the fix" }, ], }), sessionUpdate(91, { sessionUpdate: "plan", entries: [ { status: "completed", content: "Read the ticket" }, { status: "in_progress", content: "Write the fix" }, ], }), ]); const updateMarker = items.find( (item) => item.type === "plan" && item.isUpdate, ); assert.ok(updateMarker, "expected a plan update marker item"); assert.equal(updateMarker.title, "Plan updated"); assert.equal(updateMarker.text, "1/2 complete"); }); test("buildTranscript treats an empty plan entries[] as an empty checklist, not a JSON fallback", () => { const items = buildTranscript([ sessionUpdate(90, { sessionUpdate: "plan", entries: [], }), ]); assert.equal(items.length, 1); assert.equal(items[0].type, "plan"); assert.equal(items[0].text, ""); }); test("buildTranscript falls back to raw JSON when a plan update has neither entries nor content", () => { const items = buildTranscript([ sessionUpdate(90, { sessionUpdate: "plan", }), ]); assert.equal(items.length, 1); assert.equal(items[0].type, "plan"); assert.match(items[0].text, /"sessionUpdate":\s*"plan"/); }); test("buildTranscript stores first-class render class descriptors for tool items", () => { const [item] = toolItems([ acpToolUpdate(91, { sessionUpdate: "tool_call", toolCallId: "call-edit", status: "completed", title: "str_replace", kind: "str_replace", rawInput: { path: "src/app.ts" }, }), ]); assert.equal(item.renderClass, "file-edit"); assert.equal(item.descriptor.label, "Edited file"); assert.equal(item.descriptor.preview, "src/app.ts"); }); test("buildTranscript surfaces session/request_permission as a permission lifecycle item", () => { const transcript = buildTranscript([ { seq: 1, timestamp: "2026-06-30T09:00:00.000Z", kind: "acp_read", agentIndex: 0, channelId: "channel-1", sessionId: "session-1", turnId: "turn-1", payload: { jsonrpc: "2.0", method: "session/request_permission", params: { toolCallId: "tool-1", title: "Confirm force-with-lease push to block/buzz.", options: [ { optionId: "allow_once", kind: "allow_once", name: "Allow" }, { optionId: "reject_once", kind: "reject_once", name: "Reject" }, ], }, }, }, ]); assert.equal(transcript.length, 1); assert.equal(transcript[0].type, "lifecycle"); assert.equal(transcript[0].renderClass, "permission"); assert.equal(transcript[0].title, "Permission requested"); assert.match(transcript[0].text, /Confirm force-with-lease push/); }); test("buildTranscript stamps completedAt when a terminal tool update is inserted first", () => { const transcript = buildTranscript([ { seq: 1, timestamp: "2026-06-30T09:00:00.000Z", kind: "acp_read", agentIndex: 0, channelId: "channel-1", sessionId: "session-1", turnId: "turn-1", payload: { jsonrpc: "2.0", method: "session/update", params: { sessionId: "session-1", update: { sessionUpdate: "tool_call_update", toolCallId: "tool-1", toolName: "dev__shell", status: "completed", rawInput: { command: "echo hi" }, content: [{ type: "text", text: "hi" }], }, }, }, }, ]); assert.equal(transcript[0].type, "tool"); assert.equal(transcript[0].completedAt, "2026-06-30T09:00:00.000Z"); }); test("buildTranscript preserves permission, free-form status, and raw rail render classes", () => { const transcript = buildTranscript([ { ...baseEvent, seq: 1, kind: "acp_read", payload: { method: "session/request_permission", params: { title: "Confirm force-with-lease push", toolCallId: "tool-push", options: [ { optionId: "allow_once", kind: "allow_once", name: "Allow" }, { optionId: "reject_once", kind: "reject_once", name: "Reject" }, ], }, }, }, { ...baseEvent, seq: 2, kind: "acp_read", payload: { type: "observer_connected", title: "Observer connected", text: "ACP stream attached", }, }, { ...baseEvent, seq: 3, kind: "raw_json_rpc", payload: { method: "workspace/diagnostic", params: { ok: true }, }, }, ]); const permissionItem = transcript.find( (item) => item.id.startsWith("permission:") && item.renderClass === "permission", ); assert.ok( permissionItem, "permission request should flow through the reducer", ); assert.doesNotMatch( permissionItem.text, /^Permission requested\b/, "permission detail should not duplicate the row title", ); assert.ok( transcript.some( (item) => item.renderClass === "status" && item.title === "Observer connected", ), "free-form status fixture should flow through the reducer", ); assert.ok( transcript.some( (item) => item.type === "metadata" && item.renderClass === "raw-rail", ), "raw_json_rpc fixture should flow through the reducer", ); }); test("buildTranscript separates repeated lifecycle text", () => { const events = [ { seq: 1, timestamp: "2026-06-30T09:00:00.000Z", kind: "turn_error", agentIndex: 0, channelId: "channel-1", sessionId: "session-1", turnId: "turn-1", payload: { outcome: "recovered", error: "first" }, }, { seq: 2, timestamp: "2026-06-30T09:00:01.000Z", kind: "turn_error", agentIndex: 0, channelId: "channel-1", sessionId: "session-1", turnId: "turn-1", payload: { outcome: "recovered", error: "second" }, }, ]; const [item] = buildTranscript(events); assert.equal(item.type, "lifecycle"); assert.equal(item.text, "recovered: first\nrecovered: second"); }); // --- permission outcome (Fix #3) --- function makePermissionRequest(seq, requestId, turnId = "turn-1") { return { seq, timestamp: "2026-06-30T10:00:00.000Z", kind: "acp_read", agentIndex: 0, channelId: "channel-1", sessionId: "session-1", turnId, payload: { jsonrpc: "2.0", id: requestId, method: "session/request_permission", params: { title: "Confirm push", toolCallId: "tool-1", options: [ { optionId: "allow_once", kind: "allow_once", name: "Allow" }, { optionId: "reject_once", kind: "reject_once", name: "Reject" }, ], }, }, }; } function makePermissionResponse(seq, requestId, outcome, optionId = null) { const resultOutcome = outcome === "selected" ? { outcome: "selected", optionId } : { outcome }; return { seq, timestamp: "2026-06-30T10:00:01.000Z", kind: "acp_write", agentIndex: 0, channelId: "channel-1", sessionId: "session-1", turnId: "turn-1", payload: { jsonrpc: "2.0", id: requestId, result: { outcome: resultOutcome }, }, }; } test("buildTranscript appends Approved outcome when allow_once is selected", () => { const transcript = buildTranscript([ makePermissionRequest(1, "req-1"), makePermissionResponse(2, "req-1", "selected", "allow_once"), ]); assert.equal(transcript.length, 1); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.renderClass, "permission"); assert.equal(item.outcome, "Approved (allow_once)"); assert.doesNotMatch(item.text ?? "", /Approved/); }); test("buildTranscript appends Denied outcome when reject_once is selected", () => { const transcript = buildTranscript([ makePermissionRequest(1, "req-2"), makePermissionResponse(2, "req-2", "selected", "reject_once"), ]); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.outcome, "Denied (reject_once)"); assert.doesNotMatch(item.text ?? "", /Denied/); }); test("buildTranscript appends Cancelled outcome on cancelled response", () => { const transcript = buildTranscript([ makePermissionRequest(1, "req-3"), makePermissionResponse(2, "req-3", "cancelled"), ]); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.outcome, "Cancelled"); assert.doesNotMatch(item.text ?? "", /Cancelled/); }); test("buildTranscript no-ops on a permission response with an unmatched id", () => { const transcript = buildTranscript([ makePermissionRequest(1, "req-4"), makePermissionResponse(2, "req-WRONG", "selected", "allow_once"), ]); // The permission item exists but has no outcome appended — the mismatched // response id must not crash or attach to the wrong item. assert.equal(transcript.length, 1); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.renderClass, "permission"); assert.equal(item.outcome, undefined); assert.doesNotMatch(item.text ?? "", /Approved/); assert.doesNotMatch(item.text ?? "", /Denied/); }); test("buildTranscript appends Approved outcome for a numeric JSON-RPC id (selected allow_once)", () => { // JSON-RPC 2.0 allows numeric ids; the ACP runtime preserves them as // serde_json::Value. asString() drops numbers, so this exercises the // jsonRpcId() helper path that handles finite-number ids. const transcript = buildTranscript([ makePermissionRequest(1, 42), makePermissionResponse(2, 42, "selected", "allow_once"), ]); assert.equal(transcript.length, 1); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.renderClass, "permission"); assert.equal(item.outcome, "Approved (allow_once)"); assert.doesNotMatch(item.text ?? "", /Approved/); }); test("buildTranscript appends Cancelled outcome for a numeric JSON-RPC id (cancelled)", () => { const transcript = buildTranscript([ makePermissionRequest(1, 99), makePermissionResponse(2, 99, "cancelled"), ]); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.outcome, "Cancelled"); assert.doesNotMatch(item.text ?? "", /Cancelled/); }); test('buildTranscript does not collide between numeric id 1 and string id "1"', () => { // JSON.stringify produces "1" for number 1 and "\"1\"" for string "1", // so requests with these two different id types must NOT cross-attach. const transcriptNumeric = buildTranscript([ makePermissionRequest(1, 1), makePermissionResponse(2, 1, "selected", "allow_once"), ]); const transcriptString = buildTranscript([ makePermissionRequest(1, "1"), makePermissionResponse(2, "1", "selected", "reject_once"), ]); assert.equal(transcriptNumeric[0].outcome, "Approved (allow_once)"); assert.equal(transcriptString[0].outcome, "Denied (reject_once)"); }); // ─── observer parity: new session/update classifier cases ──────────────────── test("buildTranscript renders current_mode_update as a lifecycle status line", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "current_mode_update", currentModeId: "plan", }), ]); assert.equal(transcript.length, 1); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.renderClass, "status"); assert.equal(item.title, "Mode"); assert.equal(item.text, "plan"); assert.equal(item.acpSource, "current_mode_update"); }); test("buildTranscript suppresses current_mode_update when currentModeId is missing", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "current_mode_update" }), ]); assert.equal(transcript.length, 0); }); test("buildTranscript renders usage_update as a lifecycle status line with tokens", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "usage_update", used: 1500, size: 8192, }), ]); assert.equal(transcript.length, 1); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.renderClass, "status"); assert.equal(item.title, "Usage"); assert.equal(item.text, "Tokens: 1500/8192"); assert.equal(item.acpSource, "usage_update"); }); test("buildTranscript renders usage_update with cost when present", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "usage_update", used: 800, size: 4096, cost: { amount: 0.0025, currency: "USD" }, }), ]); assert.equal(transcript.length, 1); assert.equal(transcript[0].text, "Tokens: 800/4096 ($0.0025 USD)"); }); test("buildTranscript coalesces usage_update to latest-per-turn (replace, not append)", () => { // Three usage frames in the same turn must produce exactly ONE lifecycle item // showing the LAST value — not an accumulation of all three. const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "usage_update", used: 100, size: 8192 }), acpToolUpdate(2, { sessionUpdate: "usage_update", used: 300, size: 8192 }), acpToolUpdate(3, { sessionUpdate: "usage_update", used: 600, size: 8192 }), ]); const usageItems = transcript.filter((i) => i.acpSource === "usage_update"); assert.equal(usageItems.length, 1, "must coalesce to one item"); assert.equal(usageItems[0].text, "Tokens: 600/8192"); }); test("buildTranscript suppresses usage_update when used or size is missing", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "usage_update", used: 100 }), // size absent acpToolUpdate(2, { sessionUpdate: "usage_update", size: 8192 }), // used absent ]); assert.equal(transcript.length, 0); }); test("buildTranscript renders available_commands_update as a lifecycle status line", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "available_commands_update", availableCommands: [ { name: "create_plan", description: "Create a plan" }, { name: "research_codebase", description: "Research the codebase" }, ], }), ]); assert.equal(transcript.length, 1); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.renderClass, "status"); assert.equal(item.title, "Commands"); assert.equal(item.text, "Commands available: 2"); assert.equal(item.acpSource, "available_commands_update"); }); test("buildTranscript renders available_commands_update with zero commands", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "available_commands_update", availableCommands: [], }), ]); assert.equal(transcript.length, 1); assert.equal(transcript[0].text, "Commands available: 0"); }); test("buildTranscript renders config_option_update as a lifecycle status line", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "config_option_update", configOptions: [ { id: "model", name: "Model", type: "select", currentValue: "gpt-4o" }, { id: "mode", name: "Mode", type: "select", currentValue: "auto" }, ], }), ]); assert.equal(transcript.length, 1); const item = transcript[0]; assert.equal(item.type, "lifecycle"); assert.equal(item.renderClass, "status"); assert.equal(item.title, "Config"); assert.equal(item.text, "Model = gpt-4o, Mode = auto"); assert.equal(item.acpSource, "config_option_update"); }); test("buildTranscript suppresses config_option_update when configOptions is empty", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "config_option_update", configOptions: [], }), ]); assert.equal(transcript.length, 0); }); test("buildTranscript does not render keepalive (stays in else-dropped bucket)", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "keepalive" }), ]); assert.equal(transcript.length, 0); }); test("buildTranscript does not render unknown session/update types (firehose safety net)", () => { const transcript = buildTranscript([ acpToolUpdate(1, { sessionUpdate: "some_future_type", value: 42 }), ]); assert.equal(transcript.length, 0); }); // --- system-prompt ordering --- test("observer feed renders system-prompt before prompt-context in display order (first turn, realistic pool.rs sequence)", () => { // Reproduces the real ordering bug: pool.rs emits turn_started BEFORE session/new, // so turn_started creates the turn bucket first. Without the grouper holding the // system-prompt as a standalone block, displayOrder becomes // [turn(turn-1), single(system-prompt)] and System prompt renders after Prompt context. // The fix: system-prompt items (acpSource "session/new") are held in // pendingSystemPrompts[] and emitted as standalone blocks BEFORE the first turn. const events = [ { seq: 1, timestamp: "2026-07-01T10:00:00.000Z", kind: "turn_started", agentIndex: 0, channelId: "ch-1", sessionId: null, turnId: "turn-1", payload: { source: "channel", triggeringEventIds: [] }, }, { seq: 2, timestamp: "2026-07-01T10:00:00.100Z", kind: "acp_write", agentIndex: 0, channelId: "ch-1", sessionId: null, turnId: "turn-1", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nYou are a helpful assistant.\n\n[System]\nObserver Agent.", }, }, }, { seq: 3, timestamp: "2026-07-01T10:00:00.200Z", kind: "session_resolved", agentIndex: 0, channelId: "ch-1", sessionId: "sess-1", turnId: "turn-1", payload: { sessionId: "sess-1", isNewSession: true }, }, { seq: 4, timestamp: "2026-07-01T10:00:01.000Z", kind: "acp_write", agentIndex: 0, channelId: "ch-1", sessionId: "sess-1", turnId: "turn-1", payload: { jsonrpc: "2.0", id: 2, method: "session/prompt", params: { sessionId: "sess-1", prompt: [ { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${"a".repeat(64)}\nFrom: x (hex: ${"b".repeat(64)})\nContent: hello`, }, { type: "text", text: "[Thread context]\nPrior messages here." }, ], }, }, }, ]; // Route through the display layer — this is the layer that contained the bug. const rawItems = buildTranscript(events); const displayItems = flattenDisplayBlocks( buildTranscriptDisplayBlocks(rawItems), ); const systemPromptIdx = displayItems.findIndex( (i) => i.title === "System prompt", ); const promptContextIdx = displayItems.findIndex( (i) => i.title === "Prompt context", ); assert.ok(systemPromptIdx !== -1, "expected a System prompt item"); assert.ok(promptContextIdx !== -1, "expected a Prompt context item"); assert.ok( systemPromptIdx < promptContextIdx, `expected System prompt (idx ${systemPromptIdx}) before Prompt context (idx ${promptContextIdx}) in display order`, ); // Also verify the fix input: system-prompt item must have turnId=null so the // display grouper treats it as a standalone entry, not a turn-bucket item. const systemPromptRawIdx = rawItems.findIndex( (i) => i.title === "System prompt", ); assert.equal( rawItems[systemPromptRawIdx].turnId ?? null, null, "system-prompt item must have turnId=null to avoid turn-bucket grouping", ); }); test("observer feed renders system-prompt before prompt-context in display order (multi-turn)", () => { // On subsequent turns, session/new does not re-fire. The system-prompt standalone // block emitted before turn-1 must not re-appear in turn-2's display output. const events = [ { seq: 1, timestamp: "2026-07-01T10:00:00.000Z", kind: "turn_started", agentIndex: 0, channelId: "ch-1", sessionId: null, turnId: "turn-1", payload: { source: "channel", triggeringEventIds: [] }, }, { seq: 2, timestamp: "2026-07-01T10:00:00.100Z", kind: "acp_write", agentIndex: 0, channelId: "ch-1", sessionId: null, turnId: "turn-1", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nYou are helpful.\n\n[System]\nObserver.", }, }, }, { seq: 3, timestamp: "2026-07-01T10:00:00.200Z", kind: "session_resolved", agentIndex: 0, channelId: "ch-1", sessionId: "sess-1", turnId: "turn-1", payload: { sessionId: "sess-1", isNewSession: true }, }, { seq: 4, timestamp: "2026-07-01T10:00:01.000Z", kind: "acp_write", agentIndex: 0, channelId: "ch-1", sessionId: "sess-1", turnId: "turn-1", payload: { jsonrpc: "2.0", id: 2, method: "session/prompt", params: { sessionId: "sess-1", prompt: [ { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${"a".repeat(64)}\nFrom: x (hex: ${"b".repeat(64)})\nContent: turn 1`, }, { type: "text", text: "[Thread context]\nEmpty." }, ], }, }, }, { seq: 5, timestamp: "2026-07-01T10:05:00.000Z", kind: "turn_started", agentIndex: 0, channelId: "ch-1", sessionId: "sess-1", turnId: "turn-2", payload: { source: "channel", triggeringEventIds: [] }, }, { seq: 6, timestamp: "2026-07-01T10:05:01.000Z", kind: "acp_write", agentIndex: 0, channelId: "ch-1", sessionId: "sess-1", turnId: "turn-2", payload: { jsonrpc: "2.0", id: 3, method: "session/prompt", params: { sessionId: "sess-1", prompt: [ { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${"c".repeat(64)}\nFrom: x (hex: ${"d".repeat(64)})\nContent: turn 2`, }, { type: "text", text: "[Thread context]\nOne prior message." }, ], }, }, }, ]; const rawItems = buildTranscript(events); const displayItems = flattenDisplayBlocks( buildTranscriptDisplayBlocks(rawItems), ); const systemPromptIdx = displayItems.findIndex( (i) => i.title === "System prompt", ); // Both turns produce a Prompt context — grab the first one (turn-1). const firstPromptContextIdx = displayItems.findIndex( (i) => i.title === "Prompt context", ); assert.ok(systemPromptIdx !== -1, "expected a System prompt item"); assert.ok( firstPromptContextIdx !== -1, "expected at least one Prompt context item", ); assert.ok( systemPromptIdx < firstPromptContextIdx, `expected System prompt (idx ${systemPromptIdx}) before first Prompt context (idx ${firstPromptContextIdx}) in display order`, ); const systemPromptRawIdx = rawItems.findIndex( (i) => i.title === "System prompt", ); assert.equal( rawItems[systemPromptRawIdx].turnId ?? null, null, "system-prompt item must have turnId=null", ); }); test("steer ingress bundles its prompt context into the steer prompt segment, not a standalone row", () => { // The stray "Prompt context · 1 section" row was leaking from the goose // session/steer path: it upserted metadata without an acpSource, so display // grouping never consumed it into a prompt bundle. It must now ride behind // the steer message bubble's checks-icon dialog like session/prompt context. const events = [ { seq: 1, timestamp: "2026-07-01T10:00:00.000Z", kind: "acp_write", agentIndex: 0, channelId: "ch-1", sessionId: "sess-1", turnId: "turn-1", payload: { jsonrpc: "2.0", id: 5, method: "_goose/unstable/session/steer", params: { sessionId: "sess-1", prompt: [ { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${"e".repeat(64)}\nFrom: x (hex: ${"f".repeat(64)})\nContent: steer me`, }, { type: "text", text: "[Thread context]\nPrior messages here." }, ], }, }, }, ]; const rawItems = buildTranscript(events); const steerMessage = rawItems.find((item) => item.type === "message"); const steerContext = rawItems.find((item) => item.type === "metadata"); assert.equal(steerMessage?.acpSource, "session/steer:user"); assert.equal(steerContext?.acpSource, "session/steer:context"); const [block] = buildTranscriptDisplayBlocks(rawItems); assert.equal(block.kind, "turn"); const promptSegment = block.segments.find( (segment) => segment.kind === "prompt", ); assert.ok(promptSegment, "expected steer message to render a prompt bundle"); assert.equal(promptSegment.context?.id, steerContext.id); assert.ok( !block.segments.some( (segment) => segment.kind === "item" && segment.item.type === "metadata", ), "steer context must not leak as a standalone metadata row", ); }); // --- session/prompt late delivery (live subscription timing race) --- test("buildTranscript correctly renders prompt segment when session/prompt arrives after status lifecycle events", () => { // Simulates the live-subscription timing race: status events (commands, mode, // usage) arrive first because the desktop subscribed slightly after turn start, // then session/prompt arrives later (e.g. via reconnect replay or archive // backfill). buildTranscript is called in out-of-order sequence order but // processTranscriptEvent handles insertion — the full rebuild path in // appendAgentEvent (slow path for out-of-order) re-processes events sorted by // timestamp+seq, so the prompt segment must appear. const TURN = "turn-oot"; const SESS = "sess-oot"; const CH = "22222222-2222-2222-2222-222222222222"; const AUTHOR_HEX = "b".repeat(64); const EVENT_HEX = "d".repeat(64); const makeEvent = (seq, kind, timestamp, payload) => ({ seq, kind, timestamp, agentIndex: 0, channelId: CH, sessionId: SESS, turnId: TURN, payload, }); // Status events arrive first (lower seq but same timestamp as prompt) const commandsEvent = makeEvent(2, "acp_read", "2026-06-18T00:01:01Z", { method: "session/update", params: { sessionId: SESS, update: { sessionUpdate: "available_commands_update", availableCommands: ["cmd1", "cmd2"], }, }, }); const modeEvent = makeEvent(3, "acp_read", "2026-06-18T00:01:01Z", { method: "session/update", params: { sessionId: SESS, update: { sessionUpdate: "current_mode_update", currentModeId: "code" }, }, }); // session/prompt has the lowest seq — it was published first but arrived last const promptEvent = makeEvent(1, "acp_write", "2026-06-18T00:01:00Z", { method: "session/prompt", params: { sessionId: SESS, prompt: [ { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${EVENT_HEX.toUpperCase()}\nFrom: Alice (hex: ${AUTHOR_HEX})\nContent: please help`, }, { type: "text", text: "[Context]\nScope: thread" }, ], }, }); // Deliver events out of order: status first, then prompt const items = buildTranscript([commandsEvent, modeEvent, promptEvent]); const userMsg = items.find((i) => i.type === "message" && i.role === "user"); assert.ok( userMsg, "user message item must be present even when session/prompt arrives after status events", ); assert.equal( userMsg.text, "please help", "user message text extracted from session/prompt Content: line", ); const blocks = buildTranscriptDisplayBlocks(items); const turnBlock = blocks.find((b) => b.kind === "turn"); assert.ok(turnBlock, "expected a turn block"); const promptSegment = turnBlock.segments.find((s) => s.kind === "prompt"); assert.ok( promptSegment, "prompt segment must be present when session/prompt arrives out-of-order", ); assert.equal( promptSegment.user.text, "please help", "prompt segment carries the correct user text", ); }); // --- session-boundary ordering: restart scenario end-to-end ───────────────── test("buildTranscript restart sequence: both sessions retain their own system-prompt card", () => { // Full two-session restart sequence routed through processTranscriptEvent. // Each session/new event is keyed by (seq, timestamp) — the same dedup pair // used by observerRelayStore — producing distinct system-prompt items for // sess-1 and sess-2. Both must be present in the final transcript and placed // in the correct run: // sess-1: system-prompt → sess-1 activity (before boundary) // sess-2: boundary → system-prompt → user-prompt → sess-2 activity const CH = "33333333-3333-3333-3333-333333333333"; const AUTHOR_HEX = "c".repeat(64); const USER_EVENT_HEX = "e".repeat(64); const sess1Events = [ // sess-1 turn_started { seq: 1, timestamp: "2026-07-01T10:00:00.000Z", kind: "turn_started", agentIndex: 0, channelId: CH, sessionId: null, turnId: "turn-1", payload: { source: "channel", triggeringEventIds: [] }, }, // sess-1 session/new (first fire — pushes system-prompt to the stream) { seq: 2, timestamp: "2026-07-01T10:00:00.100Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: null, turnId: "turn-1", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nYou are a helpful assistant.\n\n[System]\nObserver.", }, }, }, // sess-1 resolves { seq: 3, timestamp: "2026-07-01T10:00:00.200Z", kind: "session_resolved", agentIndex: 0, channelId: CH, sessionId: "sess-1", turnId: "turn-1", payload: { sessionId: "sess-1", isNewSession: true }, }, // sess-1 activity { seq: 4, timestamp: "2026-07-01T10:00:01.000Z", kind: "acp_read", agentIndex: 0, channelId: CH, sessionId: "sess-1", turnId: "turn-1", payload: { method: "session/update", params: { sessionId: "sess-1", update: { sessionUpdate: "tool_call", toolCallId: "call-1", status: "completed", title: "shell", kind: "shell", rawInput: { command: "echo hello" }, content: { type: "text", text: "hello" }, }, }, }, }, ]; const restartEvents = [ // Restart: turn_started with null sessionId { seq: 5, timestamp: "2026-07-01T11:00:00.000Z", kind: "turn_started", agentIndex: 0, channelId: CH, sessionId: null, turnId: "turn-2", payload: { source: "channel", triggeringEventIds: [] }, }, // sess-2 session/new: distinct (seq, timestamp) produces a separate item // (system-prompt:${CH}:6:2026-07-01T11:00:00.100Z) that lands naturally // at the stream tail and is re-anchored to run sess-2. { seq: 6, timestamp: "2026-07-01T11:00:00.100Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: null, turnId: "turn-2", payload: { jsonrpc: "2.0", id: 2, method: "session/new", params: { systemPrompt: "[Base]\nYou are a helpful assistant.\n\n[System]\nObserver.", }, }, }, // New session resolves { seq: 7, timestamp: "2026-07-01T11:00:00.200Z", kind: "session_resolved", agentIndex: 0, channelId: CH, sessionId: "sess-2", turnId: "turn-2", payload: { sessionId: "sess-2", isNewSession: true }, }, // sess-2 user @mention prompt (production shape: a restart is triggered by // a user message; this is what surfaced the original bug in the screenshot). { seq: 8, timestamp: "2026-07-01T11:00:00.300Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: "sess-2", turnId: "turn-2", payload: { jsonrpc: "2.0", id: 3, method: "session/prompt", params: { sessionId: "sess-2", prompt: [ { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${USER_EVENT_HEX.toUpperCase()}\nFrom: Will (hex: ${AUTHOR_HEX})\nContent: @Paul status check? I had to restart`, }, ], }, }, }, // sess-2 activity { seq: 9, timestamp: "2026-07-01T11:00:01.000Z", kind: "acp_read", agentIndex: 0, channelId: CH, sessionId: "sess-2", turnId: "turn-2", payload: { method: "session/update", params: { sessionId: "sess-2", update: { sessionUpdate: "tool_call", toolCallId: "call-2", status: "completed", title: "shell", kind: "shell", rawInput: { command: "echo world" }, content: { type: "text", text: "world" }, }, }, }, }, ]; const items = buildTranscript([...sess1Events, ...restartEvents]); const blocks = buildTranscriptDisplayBlocks(items, "sess-2"); // (a) Exactly one session-boundary block between the two sessions. const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 1, "exactly one session-boundary block for a two-session restart", ); const boundaryIdx = blocks.indexOf(boundaryBlocks[0]); // (b) Exactly two system-prompt standalone blocks — one per session. const systemPromptBlocks = blocks.filter( (b) => b.kind === "single" && b.item?.acpSource === "session/new", ); assert.equal( systemPromptBlocks.length, 2, "must be exactly two system-prompt standalone blocks — one per session", ); const sess1PromptIdx = blocks.indexOf(systemPromptBlocks[0]); const sess2PromptIdx = blocks.indexOf(systemPromptBlocks[1]); // sess-1 system-prompt appears BEFORE the boundary (in run sess-1). assert.ok( sess1PromptIdx < boundaryIdx, `sess-1 system-prompt (idx ${sess1PromptIdx}) must precede boundary (idx ${boundaryIdx})`, ); // sess-2 system-prompt appears AFTER the boundary (in run sess-2). assert.ok( boundaryIdx < sess2PromptIdx, `boundary (idx ${boundaryIdx}) must precede sess-2 system-prompt (idx ${sess2PromptIdx})`, ); // (c) sess-2 activity must appear AFTER both boundary AND sess-2 system-prompt. // Required order: boundary → sess-2 system-prompt → sess-2 user-prompt/activity. // Note: the system-prompt item retains sessionId=null (it was created from a // null-session event); we locate it in the flat array by its item id, not sessionId. const flat = flattenDisplayBlocks(blocks); const sess2SpItemId = systemPromptBlocks[1].item?.id; const flatSess2PromptIdx = flat.findIndex((i) => i.id === sess2SpItemId); const flatSess2ActivityIdx = flat.findIndex( (i) => i.type === "tool" && i.sessionId === "sess-2", ); assert.ok( flatSess2PromptIdx !== -1, "sess-2 system-prompt must appear in flattened output", ); assert.ok( flatSess2ActivityIdx !== -1, "sess-2 tool activity must be present in flattened output", ); assert.ok( flatSess2PromptIdx < flatSess2ActivityIdx, `sess-2 system-prompt (flat idx ${flatSess2PromptIdx}) must precede sess-2 activity (flat idx ${flatSess2ActivityIdx})`, ); }); // --- same-seq different-timestamp: both system-prompt cards survive (archive collision) ── test("buildTranscript same-seq different-timestamp session/new events both produce distinct system-prompt cards", () => { // Archive rebuild scenario: two ObserverHandle processes both start at seq=1 // for the same channel. The (seq, timestamp) key pair must distinguish them // so neither card is lost. If keyed by seq alone, the second would silently // replace the first. const CH = "55555555-5555-5555-5555-555555555555"; const events = [ // Process A: seq=1, timestamp T1 { seq: 1, timestamp: "2026-07-01T10:00:00.000Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: "sess-a", turnId: "turn-a", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nProcess A.", }, }, }, // Process B: seq=1 (same!), timestamp T2 (different) { seq: 1, timestamp: "2026-07-01T11:00:00.000Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: "sess-b", turnId: "turn-b", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nProcess B.", }, }, }, ]; const rawItems = buildTranscript(events); const systemPromptItems = rawItems.filter( (i) => i.type === "metadata" && i.acpSource === "session/new", ); assert.equal( systemPromptItems.length, 2, "two same-seq different-timestamp session/new events must produce two distinct system-prompt items — not one (key collision guard)", ); const bodies = systemPromptItems.map((i) => (i.sections ?? []).map((s) => s.body).join("|"), ); assert.ok( bodies.some((b) => b.includes("Process A")), "Process A system-prompt card must be present", ); assert.ok( bodies.some((b) => b.includes("Process B")), "Process B system-prompt card must be present", ); }); test("buildTranscript five-section system prompt card is standalone with all sections; CheckCheck context contains only Buzz/thread context", () => { // Production scenario: team-pack agent harness emits // [Base]/[System (with team delimiter)]/[Agent Memory — core]/[Channel Canvas] // in systemPrompt. The display layer must: // (a) Render it as a standalone single block (acpSource "session/new"), // NOT inside any turn's prompt bundle. // (b) The standalone item must carry all five sections in order: // Base → System → Team Instructions → Core Memory → Channel Canvas. // (c) The prompt segment's context (CheckCheck dialog) must contain only // the session/prompt:context sections (Buzz event + Thread context), // never the system-prompt sections. const CH = "44444444-4444-4444-4444-444444444444"; const events = [ { seq: 1, timestamp: "2026-07-01T10:00:00.000Z", kind: "turn_started", agentIndex: 0, channelId: CH, sessionId: null, turnId: "turn-1", payload: { source: "channel", triggeringEventIds: [] }, }, { seq: 2, timestamp: "2026-07-01T10:00:00.100Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: null, turnId: "turn-1", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: [ "[Base]", "You are a helpful assistant.", "", "[System]", "Custom persona.", "", "---", "# Team Instructions", "Always tag on handoff.", "", "[Agent Memory — core]", "I am Duncan.", "", "[Channel Canvas]", "Canvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "Last modified: 2026-07-01T10:00:00Z", "Fetch current content with: buzz canvas get --channel 44444444-4444-4444-4444-444444444444", ].join("\n"), }, }, }, { seq: 3, timestamp: "2026-07-01T10:00:00.200Z", kind: "session_resolved", agentIndex: 0, channelId: CH, sessionId: "sess-e2e", turnId: "turn-1", payload: { sessionId: "sess-e2e", isNewSession: true }, }, { seq: 4, timestamp: "2026-07-01T10:00:01.000Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: "sess-e2e", turnId: "turn-1", payload: { jsonrpc: "2.0", id: 2, method: "session/prompt", params: { sessionId: "sess-e2e", prompt: [ { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${"a".repeat(64)}\nFrom: x (hex: ${"b".repeat(64)})\nContent: hello`, }, { type: "text", text: "[Thread context]\nPrior messages here.", }, ], }, }, }, ]; const rawItems = buildTranscript(events); const blocks = buildTranscriptDisplayBlocks(rawItems); const flat = flattenDisplayBlocks(blocks); // (a) Exactly one standalone system-prompt single block. const systemPromptBlocks = blocks.filter( (b) => b.kind === "single" && b.item?.acpSource === "session/new", ); assert.equal( systemPromptBlocks.length, 1, "exactly one standalone system-prompt single block", ); // (b) The standalone item carries all five sections in order. const spItem = systemPromptBlocks[0].item; assert.ok(spItem, "system-prompt block must have an item"); const titles = (spItem.sections ?? []).map((s) => s.title); assert.deepEqual( titles, ["Base", "System", "Team Instructions", "Core Memory", "Channel Canvas"], "system-prompt standalone card must carry Base → System → Team Instructions → Core Memory → Channel Canvas in order", ); // (c) The system-prompt item must NOT be inside any turn group. // Verify by checking that no turn block contains it. const systemPromptInTurnBlock = blocks.some( (b) => b.kind === "turn" && b.segments.some((seg) => seg.kind === "prompt" ? seg.user.acpSource === "session/new" || seg.context?.acpSource === "session/new" : seg.kind === "item" ? seg.item.acpSource === "session/new" : false, ), ); assert.ok( !systemPromptInTurnBlock, "system-prompt must not appear inside any turn block", ); // (d) CheckCheck context (prompt segment's context field) must contain only // the session/prompt:context item — Buzz/thread context only, no system-prompt sections. const promptContextItem = flat.find( (i) => i.acpSource === "session/prompt:context", ); assert.ok( promptContextItem, "prompt context item (acpSource session/prompt:context) must be present", ); const contextSectionTitles = (promptContextItem.sections ?? []).map( (s) => s.title, ); // Must have Buzz event and Thread context sections, NOT Base/System/Team Instructions/Core Memory/Channel Canvas. assert.ok( contextSectionTitles.some((t) => t.toLowerCase().includes("buzz")), "prompt context must contain a Buzz event section", ); assert.ok( !contextSectionTitles.some( (t) => t === "Base" || t === "System" || t === "Team Instructions" || t === "Core Memory" || t === "Channel Canvas", ), "prompt context must NOT contain system-prompt sections (Base/System/Team Instructions/Core Memory/Channel Canvas)", ); }); // --- claude-agent-acp _meta.systemPrompt.append transport --- test("buildTranscript session/new via _meta.systemPrompt.append produces identical standalone card as bare systemPrompt field", () => { // claude-agent-acp delivers the system prompt at _meta.systemPrompt.append // instead of the bare systemPrompt field. The observer must extract it and // build the identical standalone card (same five sections, same acpSource, // same turnId: null, same placement before the first turn). const CH = "55555555-5555-5555-5555-555555555555"; const SYSTEM_PROMPT = [ "[Base]", "You are a helpful assistant.", "", "[System]", "Custom persona.", "", "---", "# Team Instructions", "Always tag on handoff.", "", "[Agent Memory \u2014 core]", "I am Duncan.", "", "[Channel Canvas]", "Canvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "Last modified: 2026-07-01T10:00:00Z", "Fetch current content with: buzz canvas get --channel 55555555-5555-5555-5555-555555555555", ].join("\n"); const makeEvents = (params) => [ { seq: 1, timestamp: "2026-07-01T10:00:00.000Z", kind: "turn_started", agentIndex: 0, channelId: CH, sessionId: null, turnId: "turn-1", payload: { source: "channel", triggeringEventIds: [] }, }, { seq: 2, timestamp: "2026-07-01T10:00:00.100Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: null, turnId: "turn-1", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params }, }, { seq: 3, timestamp: "2026-07-01T10:00:00.200Z", kind: "session_resolved", agentIndex: 0, channelId: CH, sessionId: "sess-cc", turnId: "turn-1", payload: { sessionId: "sess-cc", isNewSession: true }, }, { seq: 4, timestamp: "2026-07-01T10:00:01.000Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: "sess-cc", turnId: "turn-1", payload: { jsonrpc: "2.0", id: 2, method: "session/prompt", params: { sessionId: "sess-cc", prompt: [ { type: "text", text: `[Buzz event: @mention]\nEvent ID: ${"a".repeat(64)}\nFrom: x (hex: ${"b".repeat(64)})\nContent: hello`, }, { type: "text", text: "[Thread context]\nPrior messages here." }, ], }, }, }, ]; // Build transcript from the claude-agent-acp _meta transport. const metaEvents = makeEvents({ _meta: { systemPrompt: { append: SYSTEM_PROMPT } }, }); const metaRaw = buildTranscript(metaEvents); const metaBlocks = buildTranscriptDisplayBlocks(metaRaw); const metaFlat = flattenDisplayBlocks(metaBlocks); // Also build from the standard bare-field transport for comparison. const fieldEvents = makeEvents({ systemPrompt: SYSTEM_PROMPT }); const fieldRaw = buildTranscript(fieldEvents); const fieldBlocks = buildTranscriptDisplayBlocks(fieldRaw); // (a) Both produce exactly one standalone system-prompt single block. const metaSPBlocks = metaBlocks.filter( (b) => b.kind === "single" && b.item?.acpSource === "session/new", ); const fieldSPBlocks = fieldBlocks.filter( (b) => b.kind === "single" && b.item?.acpSource === "session/new", ); assert.equal( metaSPBlocks.length, 1, "_meta: exactly one standalone system-prompt block", ); assert.equal( fieldSPBlocks.length, 1, "field: exactly one standalone system-prompt block", ); // (b) Both carry the same five ordered sections. const EXPECTED_TITLES = [ "Base", "System", "Team Instructions", "Core Memory", "Channel Canvas", ]; const metaTitles = (metaSPBlocks[0].item?.sections ?? []).map((s) => s.title); const fieldTitles = (fieldSPBlocks[0].item?.sections ?? []).map( (s) => s.title, ); assert.deepEqual( metaTitles, EXPECTED_TITLES, "_meta: five sections in order", ); assert.deepEqual( fieldTitles, EXPECTED_TITLES, "field: five sections in order", ); // (c) System prompt appears before Prompt context in both display orders. const metaSPIdx = metaFlat.findIndex((i) => i.title === "System prompt"); const metaPCIdx = metaFlat.findIndex((i) => i.title === "Prompt context"); assert.ok(metaSPIdx !== -1, "_meta: System prompt item present"); assert.ok(metaPCIdx !== -1, "_meta: Prompt context item present"); assert.ok( metaSPIdx < metaPCIdx, `_meta: System prompt (${metaSPIdx}) must precede Prompt context (${metaPCIdx})`, ); // (d) The _meta item has turnId: null (standalone, not in a turn bucket). const metaSPRawIdx = metaRaw.findIndex((i) => i.title === "System prompt"); assert.equal( metaRaw[metaSPRawIdx]?.turnId ?? null, null, "_meta: system-prompt item must have turnId=null", ); }); test("buildTranscript session/new bare systemPrompt field takes precedence over _meta.systemPrompt.append", () => { // When both transports are present (non-standard but must not regress), // the standard bare field must win — a reversed ?? would silently use the // wrong text and the card body would differ from the wire source of truth. const CH = "66666666-6666-6666-6666-666666666666"; const events = [ { seq: 1, timestamp: "2026-07-01T10:00:00.000Z", kind: "acp_write", agentIndex: 0, channelId: CH, sessionId: "sess-both", turnId: "turn-1", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nWinner.", _meta: { systemPrompt: { append: "[Base]\nLoser." } }, }, }, }, ]; const rawItems = buildTranscript(events); const spItem = rawItems.find((i) => i.title === "System prompt"); assert.ok(spItem, "System prompt item must be present"); const bodies = (spItem.sections ?? []).map((s) => s.body).join("|"); assert.ok( bodies.includes("Winner"), "bare systemPrompt must win over _meta.systemPrompt.append", ); assert.ok( !bodies.includes("Loser"), "_meta.systemPrompt.append must not appear when bare field is present", ); });