From da85814940a42ac79a3d67bf711d7b75565492d1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 2 May 2026 14:24:45 +0700 Subject: [PATCH] fix(server): apply get_agent_activity limit to projected entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP get_agent_activity tool sliced the raw timeline before passing to the curator, so `limit: 5` could land mid-message and return a trailing fragment of streamed assistant text. Now goes through the existing projection pipeline so the limit counts whole projected entries — assistant deltas merged, tool calls collapsed by callId, reasoning chunks merged. Same bug fixed in the wait_for_agent timeout's "Recent activity" preview, which used the same raw slice. Consolidates the merge logic: activity-curator no longer ships its own collapseTimeline, it delegates to projectTimelineRows. One source of truth for "what is a complete message". Drops the meaningless `provider` parameter that was threaded through the projection module — merge logic never branched on it. The wire schema keeps `provider` per entry; session stamps it once at the emit boundary. Adds `reasoning_merge` to the projection kind enum (additive wire change). --- .../src/server/agent/activity-curator.ts | 137 ++---------------- .../src/server/agent/mcp-server.test.ts | 64 ++++++++ .../server/src/server/agent/mcp-server.ts | 24 +-- .../server/src/server/agent/mcp-shared.ts | 8 +- .../claude-agent.sub-agent-sidechain.test.ts | 2 +- .../server/agent/timeline-projection.test.ts | 38 +++-- .../src/server/agent/timeline-projection.ts | 109 +++++++++++--- packages/server/src/server/session.ts | 20 ++- packages/server/src/shared/messages.ts | 2 +- 9 files changed, 228 insertions(+), 176 deletions(-) diff --git a/packages/server/src/server/agent/activity-curator.ts b/packages/server/src/server/agent/activity-curator.ts index 376128900..ba48a3877 100644 --- a/packages/server/src/server/agent/activity-curator.ts +++ b/packages/server/src/server/agent/activity-curator.ts @@ -1,6 +1,7 @@ -import type { AgentTimelineItem, ToolCallDetail } from "./agent-sdk-types.js"; +import type { AgentTimelineItem } from "./agent-sdk-types.js"; import { isLikelyExternalToolName } from "./tool-name-normalization.js"; import { buildToolCallDisplayModel } from "../../shared/tool-call-display.js"; +import { projectTimelineRows } from "./timeline-projection.js"; const DEFAULT_MAX_ITEMS = 0; const MAX_TOOL_INPUT_CHARS = 400; @@ -60,130 +61,19 @@ function formatToolSummary(summary: string | undefined): string | null { return `${normalized.slice(0, MAX_TOOL_SUMMARY_CHARS - 3)}...`; } -function hasNonEmptyObject(value: unknown): boolean { - return ( - typeof value === "object" && - value !== null && - !Array.isArray(value) && - Object.keys(value).length > 0 - ); -} - -function mergeUnknownValue(existing: unknown | null, incoming: unknown | null): unknown | null { - if (incoming === null) { - return existing; - } - - if (!hasNonEmptyObject(incoming) && hasNonEmptyObject(existing)) { - return existing; - } - - return incoming; -} - -function mergeToolDetail(existing: ToolCallDetail, incoming: ToolCallDetail): ToolCallDetail { - if (existing.type === "unknown" && incoming.type !== "unknown") { - return incoming; - } - if (incoming.type === "unknown" && existing.type !== "unknown") { - return existing; - } - if (existing.type === "unknown" && incoming.type === "unknown") { - return { - type: "unknown", - input: mergeUnknownValue(existing.input, incoming.input), - output: mergeUnknownValue(existing.output, incoming.output), - }; - } - if (existing.type === incoming.type) { - return { ...existing, ...incoming } as ToolCallDetail; - } - return incoming; -} - -function inputFromUnknownDetail(detail: ToolCallDetail): unknown { +function inputFromUnknownDetail( + detail: Extract["detail"], +): unknown { return detail.type === "unknown" ? detail.input : null; } -/** - * Collapse timeline items: - * - Dedupe tool calls by callId (pending/completed -> single) - * - Merge consecutive assistant_message/reasoning into single items - */ -function collapseTimeline(items: AgentTimelineItem[]): AgentTimelineItem[] { - const result: AgentTimelineItem[] = []; - const toolCallMap = new Map(); - let assistantBuffer = ""; - let reasoningBuffer = ""; - - function flushAssistant() { - if (assistantBuffer) { - result.push({ type: "assistant_message", text: assistantBuffer }); - assistantBuffer = ""; - } - } - - function flushReasoning() { - if (reasoningBuffer) { - result.push({ type: "reasoning", text: reasoningBuffer }); - reasoningBuffer = ""; - } - } - - function flushToolCalls() { - for (const toolItem of toolCallMap.values()) { - result.push(toolItem); - } - toolCallMap.clear(); - } - - for (const item of items) { - if (item.type === "assistant_message") { - flushReasoning(); - flushToolCalls(); - assistantBuffer += item.text; - } else if (item.type === "reasoning") { - flushAssistant(); - flushToolCalls(); - reasoningBuffer += item.text; - } else if (item.type === "tool_call") { - flushAssistant(); - flushReasoning(); - const existing = toolCallMap.get(item.callId); - if (existing && existing.type === "tool_call") { - if (item.status === "failed") { - toolCallMap.set(item.callId, { - ...existing, - ...item, - detail: mergeToolDetail(existing.detail, item.detail), - error: item.error, - metadata: item.metadata, - }); - } else { - toolCallMap.set(item.callId, { - ...existing, - ...item, - detail: mergeToolDetail(existing.detail, item.detail), - error: null, - metadata: item.metadata, - }); - } - } else { - toolCallMap.set(item.callId, item); - } - } else { - flushAssistant(); - flushReasoning(); - flushToolCalls(); - result.push(item); - } - } - - flushAssistant(); - flushReasoning(); - flushToolCalls(); - - return result; +function projectForCuration(items: readonly AgentTimelineItem[]): AgentTimelineItem[] { + const rows = items.map((item, index) => ({ + seq: index + 1, + timestamp: "", + item, + })); + return projectTimelineRows({ rows, mode: "projected" }).map((entry) => entry.item); } /** @@ -197,8 +87,7 @@ export function curateAgentActivity( return "No activity to display."; } - // Collapse timeline: dedupe tool calls, merge consecutive messages - const collapsed = collapseTimeline(timeline); + const collapsed = projectForCuration(timeline); const maxItems = options?.maxItems ?? DEFAULT_MAX_ITEMS; const recentItems = diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index 7b3246584..1841f9cad 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -2009,4 +2009,68 @@ describe("agent snapshot MCP serialization", () => { "archived-activity-agent", ); }); + + it("get_agent_activity limit counts projected messages, not raw deltas", async () => { + const { agentManager, agentStorage, spies } = createTestDeps(); + const snapshot = createManagedAgent({ id: "live-activity-agent", currentModeId: "default" }); + spies.agentManager.getAgent.mockReturnValue(snapshot); + spies.agentManager.getTimeline.mockReturnValue([ + { type: "user_message", text: "Say hi" }, + { type: "assistant_message", text: "Hello " }, + { type: "assistant_message", text: "world" }, + { type: "assistant_message", text: "." }, + { type: "assistant_message", text: " How" }, + { type: "assistant_message", text: " are" }, + { type: "assistant_message", text: " you?" }, + ]); + + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + logger: createTestLogger(), + providerRegistry: { + claude: createProviderDefinition({}), + } as unknown as Record, + }); + const tool = registeredTool(server, "get_agent_activity"); + const response = await tool.callback({ agentId: "live-activity-agent", limit: 1 }); + + const content = String(response.structuredContent.content); + expect(content).toContain("Hello world. How are you?"); + }); + + it("get_agent_activity limit=2 returns the last two projected entries whole", async () => { + const { agentManager, agentStorage, spies } = createTestDeps(); + const snapshot = createManagedAgent({ id: "live-activity-agent-2", currentModeId: "default" }); + spies.agentManager.getAgent.mockReturnValue(snapshot); + spies.agentManager.getTimeline.mockReturnValue([ + { type: "user_message", text: "u1" }, + { type: "assistant_message", text: "first " }, + { type: "assistant_message", text: "answer" }, + { type: "user_message", text: "u2" }, + { type: "assistant_message", text: "second " }, + { type: "assistant_message", text: "answer" }, + { type: "user_message", text: "u3" }, + { type: "assistant_message", text: "third " }, + { type: "assistant_message", text: "answer" }, + ]); + + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + logger: createTestLogger(), + providerRegistry: { + claude: createProviderDefinition({}), + } as unknown as Record, + }); + const tool = registeredTool(server, "get_agent_activity"); + const response = await tool.callback({ agentId: "live-activity-agent-2", limit: 2 }); + + const content = String(response.structuredContent.content); + expect(content).toContain("[User] u3"); + expect(content).toContain("third answer"); + expect(content).not.toContain("[User] u2"); + expect(content).not.toContain("second answer"); + expect(content).not.toContain("first answer"); + }); }); diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index cfe2d066c..58fbb4647 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -20,6 +20,7 @@ import { toAgentPayload, } from "./agent-projections.js"; import { curateAgentActivity } from "./activity-curator.js"; +import { selectItemsByProjectedLimit } from "./timeline-projection.js"; import type { AgentStorage } from "./agent-storage.js"; import { ensureAgentLoaded } from "./agent-loading.js"; import { isStoredAgentProviderAvailable } from "../persistence-hooks.js"; @@ -1935,18 +1936,19 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom const timeline = agentManager.getTimeline(agentId); const snapshot = agentManager.getAgent(agentId); - const activitiesToCurate = limit ? timeline.slice(-limit) : timeline; + const selection = selectItemsByProjectedLimit({ + items: timeline, + direction: "tail", + limit: limit ?? 0, + }); + const curatedContent = curateAgentActivity(selection.items); + const { totalProjected, shownProjected } = selection; - const curatedContent = curateAgentActivity(activitiesToCurate); - const totalCount = timeline.length; - const shownCount = activitiesToCurate.length; - - let countHeader: string; - if (limit && shownCount < totalCount) { - countHeader = `Showing ${shownCount} of ${totalCount} ${totalCount === 1 ? "activity" : "activities"} (limited to ${limit})`; - } else { - countHeader = `Showing all ${totalCount} ${totalCount === 1 ? "activity" : "activities"}`; - } + const noun = totalProjected === 1 ? "activity" : "activities"; + const countHeader = + limit && shownProjected < totalProjected + ? `Showing ${shownProjected} of ${totalProjected} ${noun} (limited to ${limit})` + : `Showing all ${totalProjected} ${noun}`; const contentWithCount = `${countHeader}\n\n${curatedContent}`; diff --git a/packages/server/src/server/agent/mcp-shared.ts b/packages/server/src/server/agent/mcp-shared.ts index db2f75bf8..286fc526a 100644 --- a/packages/server/src/server/agent/mcp-shared.ts +++ b/packages/server/src/server/agent/mcp-shared.ts @@ -8,6 +8,7 @@ import type { } from "./agent-sdk-types.js"; import type { AgentManager, ManagedAgent, WaitForAgentResult } from "./agent-manager.js"; import { curateAgentActivity } from "./activity-curator.js"; +import { selectItemsByProjectedLimit } from "./timeline-projection.js"; import type { AgentStorage } from "./agent-storage.js"; import { ensureAgentLoaded } from "./agent-loading.js"; import { serializeAgentSnapshot } from "../messages.js"; @@ -147,7 +148,12 @@ export async function waitForAgentWithTimeout( if (error instanceof Error && error.message === "wait timeout") { const snapshot = agentManager.getAgent(agentId); const timeline = agentManager.getTimeline(agentId); - const recentActivity = curateAgentActivity(timeline.slice(-5)); + const recent = selectItemsByProjectedLimit({ + items: timeline, + direction: "tail", + limit: 5, + }); + const recentActivity = curateAgentActivity(recent.items); const waitedSeconds = Math.round(AGENT_WAIT_TIMEOUT_MS / 1000); const message = `Awaiting the agent timed out after ${waitedSeconds}s. This does not mean the agent failed - call wait_for_agent again to continue waiting.\n\nRecent activity:\n${recentActivity}`; return { diff --git a/packages/server/src/server/agent/providers/claude-agent.sub-agent-sidechain.test.ts b/packages/server/src/server/agent/providers/claude-agent.sub-agent-sidechain.test.ts index 977defec0..d95654040 100644 --- a/packages/server/src/server/agent/providers/claude-agent.sub-agent-sidechain.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.sub-agent-sidechain.test.ts @@ -301,7 +301,7 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => { timestamp: `2026-02-01T00:00:0${index}.000Z`, item, })); - const projected = projectTimelineRows(rows, "claude", "projected"); + const projected = projectTimelineRows({ rows, mode: "projected" }); const projectedTaskCalls = projected.filter( (entry) => entry.item.type === "tool_call" && entry.item.callId === "task-call-1", ); diff --git a/packages/server/src/server/agent/timeline-projection.test.ts b/packages/server/src/server/agent/timeline-projection.test.ts index ae5e03efe..aa98a7c16 100644 --- a/packages/server/src/server/agent/timeline-projection.test.ts +++ b/packages/server/src/server/agent/timeline-projection.test.ts @@ -26,7 +26,7 @@ describe("projectTimelineRows", () => { }, ]; - const projected = projectTimelineRows(rows, "codex", "projected"); + const projected = projectTimelineRows({ rows, mode: "projected" }); expect(projected).toHaveLength(2); expect(projected[0]?.item).toEqual({ @@ -39,6 +39,32 @@ describe("projectTimelineRows", () => { expect(projected[0]?.collapsed).toContain("assistant_merge"); }); + test("merges adjacent reasoning chunks in projected mode", () => { + const rows: AgentTimelineRow[] = [ + { + seq: 1, + timestamp: "2026-02-13T00:00:00.000Z", + item: { type: "reasoning", text: "Step " }, + }, + { + seq: 2, + timestamp: "2026-02-13T00:00:00.100Z", + item: { type: "reasoning", text: "by step" }, + }, + { + seq: 3, + timestamp: "2026-02-13T00:00:00.200Z", + item: { type: "assistant_message", text: "done" }, + }, + ]; + + const projected = projectTimelineRows({ rows, mode: "projected" }); + + expect(projected).toHaveLength(2); + expect(projected[0]?.item).toEqual({ type: "reasoning", text: "Step by step" }); + expect(projected[0]?.collapsed).toContain("reasoning_merge"); + }); + test("collapses tool lifecycle by callId and reports exact source seq ranges", () => { const rows: AgentTimelineRow[] = [ { @@ -80,7 +106,7 @@ describe("projectTimelineRows", () => { }, ]; - const projected = projectTimelineRows(rows, "codex", "projected"); + const projected = projectTimelineRows({ rows, mode: "projected" }); expect(projected).toHaveLength(2); const tool = projected[0]; @@ -110,7 +136,7 @@ describe("projectTimelineRows", () => { }, ]; - const projected = projectTimelineRows(rows, "codex", "canonical"); + const projected = projectTimelineRows({ rows, mode: "canonical" }); expect(projected).toHaveLength(2); expect(projected[0]?.item).toEqual(rows[0]?.item); @@ -152,7 +178,6 @@ describe("selectTimelineWindowByProjectedLimit", () => { const selected = selectTimelineWindowByProjectedLimit({ rows, - provider: "codex", direction: "tail", limit: 1, }); @@ -193,7 +218,6 @@ describe("selectTimelineWindowByProjectedLimit", () => { const selected = selectTimelineWindowByProjectedLimit({ rows, - provider: "claude", direction: "after", limit: 2, }); @@ -247,7 +271,6 @@ describe("selectTimelineWindowByProjectedLimit", () => { const selected = selectTimelineWindowByProjectedLimit({ rows, - provider: "codex", direction: "tail", limit: 2, }); @@ -306,7 +329,6 @@ describe("selectTimelineWindowByProjectedLimit", () => { const selected = selectTimelineWindowByProjectedLimit({ rows, - provider: "codex", direction: "after", limit: 1, }); @@ -347,7 +369,6 @@ describe("selectTimelineWindowByProjectedLimit", () => { const selected = selectTimelineWindowByProjectedLimit({ rows, - provider: "claude", direction: "before", limit: 1, }); @@ -405,7 +426,6 @@ describe("selectTimelineWindowByProjectedLimit", () => { const selected = selectTimelineWindowByProjectedLimit({ rows, - provider: "codex", direction: "tail", limit: 1, collapseToolLifecycle: false, diff --git a/packages/server/src/server/agent/timeline-projection.ts b/packages/server/src/server/agent/timeline-projection.ts index a99e574b8..3d8f55ca0 100644 --- a/packages/server/src/server/agent/timeline-projection.ts +++ b/packages/server/src/server/agent/timeline-projection.ts @@ -1,4 +1,4 @@ -import type { AgentProvider, AgentTimelineItem, ToolCallDetail } from "./agent-sdk-types.js"; +import type { AgentTimelineItem, ToolCallDetail } from "./agent-sdk-types.js"; import type { AgentTimelineRow } from "./agent-manager.js"; export type TimelineProjectionMode = "canonical" | "projected"; @@ -8,11 +8,10 @@ export interface TimelineSeqRange { endSeq: number; } -export type TimelineProjectionKind = "assistant_merge" | "tool_lifecycle"; +export type TimelineProjectionKind = "assistant_merge" | "reasoning_merge" | "tool_lifecycle"; export type TimelineLimitDirection = "tail" | "before" | "after"; export interface TimelineProjectionEntry { - provider: AgentProvider; item: AgentTimelineItem; timestamp: string; seqStart: number; @@ -100,12 +99,8 @@ function mergeToolCallItems( return merged; } -function makeCanonicalEntries( - rows: readonly AgentTimelineRow[], - provider: AgentProvider, -): WorkingEntry[] { +function makeCanonicalEntries(rows: readonly AgentTimelineRow[]): WorkingEntry[] { return rows.map((row) => ({ - provider, item: row.item, timestamp: row.timestamp, seqStart: row.seq, @@ -156,6 +151,45 @@ function collapseToolLifecycle(entries: readonly WorkingEntry[]): WorkingEntry[] return output; } +function mergeReasoningChunks(entries: readonly WorkingEntry[]): WorkingEntry[] { + const output: WorkingEntry[] = []; + + for (const entry of entries) { + const previous = output[output.length - 1]; + const shouldMerge = + previous && + previous.item.type === "reasoning" && + entry.item.type === "reasoning" && + previous.seqEnd + 1 === entry.seqStart; + + if (!shouldMerge || !previous) { + output.push(entry); + continue; + } + const previousReasoning = previous.item as Extract; + const entryReasoning = entry.item as Extract; + + const collapsedKinds = new Set([ + ...previous.collapsed, + ...entry.collapsed, + "reasoning_merge", + ]); + + output[output.length - 1] = { + ...previous, + item: { + type: "reasoning", + text: `${previousReasoning.text}${entryReasoning.text}`, + }, + seqEnd: entry.seqEnd, + sourceSeqRanges: mergeSeqRanges(previous.sourceSeqRanges, entry.sourceSeqRanges), + collapsed: Array.from(collapsedKinds), + }; + } + + return output; +} + function mergeAssistantChunks(entries: readonly WorkingEntry[]): WorkingEntry[] { const output: WorkingEntry[] = []; @@ -198,18 +232,18 @@ function mergeAssistantChunks(entries: readonly WorkingEntry[]): WorkingEntry[] return output; } -export function projectTimelineRows( - rows: readonly AgentTimelineRow[], - provider: AgentProvider, - mode: TimelineProjectionMode, -): TimelineProjectionEntry[] { - const canonical = makeCanonicalEntries(rows, provider); - if (mode === "canonical") { +export function projectTimelineRows(input: { + rows: readonly AgentTimelineRow[]; + mode: TimelineProjectionMode; +}): TimelineProjectionEntry[] { + const canonical = makeCanonicalEntries(input.rows); + if (input.mode === "canonical") { return canonical; } const toolCollapsed = collapseToolLifecycle(canonical); - return mergeAssistantChunks(toolCollapsed); + const assistantMerged = mergeAssistantChunks(toolCollapsed); + return mergeReasoningChunks(assistantMerged); } /** @@ -219,17 +253,16 @@ export function projectTimelineRows( */ export function selectTimelineWindowByProjectedLimit(input: { rows: readonly AgentTimelineRow[]; - provider: AgentProvider; direction: TimelineLimitDirection; limit: number; collapseToolLifecycle?: boolean; }): ProjectedWindowSelection { - const { rows, provider, direction } = input; + const { rows, direction } = input; const limit = Math.max(0, Math.floor(input.limit)); const collapseTools = input.collapseToolLifecycle ?? true; - const canonical = makeCanonicalEntries(rows, provider); - const projectedAll = mergeAssistantChunks( - collapseTools ? collapseToolLifecycle(canonical) : canonical, + const canonical = makeCanonicalEntries(rows); + const projectedAll = mergeReasoningChunks( + mergeAssistantChunks(collapseTools ? collapseToolLifecycle(canonical) : canonical), ); if (projectedAll.length === 0) { @@ -308,3 +341,37 @@ export function selectTimelineWindowByProjectedLimit(input: { maxSeq: Number.isFinite(maxSeq) ? maxSeq : null, }; } + +/** + * Apply a projected-count limit to a flat AgentTimelineItem[] without seq metadata. + * Used by callers that only have items in hand (e.g. MCP tools reading + * `agentManager.getTimeline`). Index position is treated as canonical seq. + */ +export interface ProjectedItemSelection { + items: AgentTimelineItem[]; + totalProjected: number; + shownProjected: number; +} + +export function selectItemsByProjectedLimit(input: { + items: readonly AgentTimelineItem[]; + direction: TimelineLimitDirection; + limit: number; +}): ProjectedItemSelection { + const rows: AgentTimelineRow[] = input.items.map((item, index) => ({ + seq: index + 1, + timestamp: "", + item, + })); + const projectedAll = projectTimelineRows({ rows, mode: "projected" }); + const window = selectTimelineWindowByProjectedLimit({ + rows, + direction: input.direction, + limit: input.limit, + }); + return { + items: window.selectedRows.map((row) => row.item), + totalProjected: projectedAll.length, + shownProjected: window.projectedEntries.length, + }; +} diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 99ed5c7c6..1f71864ba 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -7313,7 +7313,6 @@ export class Session { direction: AgentTimelineFetchDirection; cursor: AgentTimelineCursor | undefined; requestedLimit: number; - provider: AgentSnapshotPayload["provider"]; timeline: ReturnType; }): { timeline: ReturnType; @@ -7321,13 +7320,12 @@ export class Session { minSeq: number | null; maxSeq: number | null; } { - const { agentId, direction, cursor, requestedLimit, provider } = params; + const { agentId, direction, cursor, requestedLimit } = params; let timeline = params.timeline; const projectedLimit = Math.max(1, Math.floor(requestedLimit)); let fetchLimit = projectedLimit; let projectedWindow = selectTimelineWindowByProjectedLimit({ rows: timeline.rows, - provider, direction, limit: projectedLimit, collapseToolLifecycle: false, @@ -7362,7 +7360,6 @@ export class Session { }); projectedWindow = selectTimelineWindowByProjectedLimit({ rows: timeline.rows, - provider, direction, limit: projectedLimit, collapseToolLifecycle: false, @@ -7424,11 +7421,10 @@ export class Session { direction, cursor, requestedLimit, - provider: snapshot.provider, timeline, }); timeline = projectedResult.timeline; - entries = projectTimelineRows(projectedResult.selectedRows, snapshot.provider, projection); + entries = projectTimelineRows({ rows: projectedResult.selectedRows, mode: projection }); if (projectedResult.minSeq !== null && projectedResult.maxSeq !== null) { startCursor = { epoch: timeline.epoch, seq: projectedResult.minSeq }; endCursor = { epoch: timeline.epoch, seq: projectedResult.maxSeq }; @@ -7440,7 +7436,7 @@ export class Session { const lastRow = timeline.rows[timeline.rows.length - 1]; startCursor = firstRow ? { epoch: timeline.epoch, seq: firstRow.seq } : null; endCursor = lastRow ? { epoch: timeline.epoch, seq: lastRow.seq } : null; - entries = projectTimelineRows(timeline.rows, snapshot.provider, projection); + entries = projectTimelineRows({ rows: timeline.rows, mode: projection }); } this.emit({ @@ -7460,7 +7456,15 @@ export class Session { endCursor, hasOlder, hasNewer, - entries, + entries: entries.map((entry) => ({ + provider: snapshot.provider, + item: entry.item, + timestamp: entry.timestamp, + seqStart: entry.seqStart, + seqEnd: entry.seqEnd, + sourceSeqRanges: entry.sourceSeqRanges, + collapsed: entry.collapsed, + })), error: null, }, }); diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 7f21cb832..33db0d39f 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -2390,7 +2390,7 @@ export const AgentTimelineEntryPayloadSchema = z.object({ seqStart: z.number().int().nonnegative(), seqEnd: z.number().int().nonnegative(), sourceSeqRanges: z.array(AgentTimelineSeqRangeSchema), - collapsed: z.array(z.enum(["assistant_merge", "tool_lifecycle"])), + collapsed: z.array(z.enum(["assistant_merge", "reasoning_merge", "tool_lifecycle"])), }); export const FetchAgentTimelineResponseMessageSchema = z.object({