From 6bfd3e8bfa00b4eec68ae60c9a969886ee621457 Mon Sep 17 00:00:00 2001 From: Alin Jiang Date: Fri, 23 Oct 2026 11:33:39 +0200 Subject: [PATCH] fix(claude): replay large child histories without spread Streaming removes whole-transcript duplication, but folding many visible child events still passed the complete aggregate as arguments to Array.push. Large histories exceed the V8 argument limit, the loader catches a RangeError, and history remains empty. Append replay observations iteratively and cover 160,000 aggregate child events. Adapted from #7511, independently reproduced with the public resumeSession and streamHistory interfaces. Co-authored-by: Alin Jiang --- .../claude/agent.sub-agent-replay.test.ts | 40 +++++++++++++++++++ .../server/agent/providers/claude/agent.ts | 24 +++++------ .../claude/subagents/replay-source.ts | 14 +++++-- .../subagents/workflow-replay-source.ts | 18 ++++----- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude/agent.sub-agent-replay.test.ts b/packages/server/src/server/agent/providers/claude/agent.sub-agent-replay.test.ts index bc32c3944e..7cce1ccdac 100644 --- a/packages/server/src/server/agent/providers/claude/agent.sub-agent-replay.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.sub-agent-replay.test.ts @@ -466,6 +466,46 @@ describe("ClaudeAgentSession persisted subagent replay", () => { ); }); + test("replays more child events than V8 permits in one spread call", async () => { + const childCount = 16; + const entriesPerChild = 10_000; + const children = Array.from({ length: childCount }, (_, index) => ({ + agentId: `large-child-${index}`, + toolUseId: `large-task-${index}`, + })); + const parentLines = children.flatMap(({ agentId, toolUseId }) => [ + parentEntry([{ type: "tool_use", id: toolUseId, name: "Task", input: {} }]), + JSON.stringify({ + type: "user", + message: { + content: [ + { + type: "tool_result", + tool_use_id: toolUseId, + content: `agentId: ${agentId}\ndone`, + }, + ], + }, + }), + ]); + const subagentDir = writeParentSession(parentLines); + for (const { agentId, toolUseId } of children) { + writeSubagent({ + subagentDir, + agentId, + meta: JSON.stringify({ toolUseId }), + sidechainLines: Array.from({ length: entriesPerChild }, () => sidechainEntry({ agentId })), + }); + } + + const events = await replayDescriptors(); + const timelineCount = events.filter((event) => event.event.type === "timeline").length; + expect(timelineCount).toBe(childCount * entriesPerChild); + expect(upserts(events).filter((event) => event.status === "completed")).toHaveLength( + childCount, + ); + }, 120_000); + test("limits retained child shell output to the timeline's existing content limit", async () => { const output = "x".repeat(128 * 1024); writeSession({ diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index ad35e1e3f5..1fd4fa9bb0 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -5261,15 +5261,13 @@ class ClaudeAgentSession implements AgentSession { }; if (observations.length === 0) return replay; - this.persistedProviderSubagentEvents.push( - ...foldSubagentObservations(observations).map( - (event): Extract => ({ - type: "provider_subagent", - provider: "claude", - event, - }), - ), - ); + for (const event of foldSubagentObservations(observations)) { + this.persistedProviderSubagentEvents.push({ + type: "provider_subagent", + provider: "claude", + event, + }); + } this.historyPending = true; return replay; } @@ -5327,12 +5325,12 @@ class ClaudeAgentSession implements AgentSession { } if (items.length > 0) { - timeline.push( - ...items.map((item) => ({ + for (const item of items) { + timeline.push({ item, timestamp: historyTimestamp ?? undefined, - })), - ); + }); + } } } diff --git a/packages/server/src/server/agent/providers/claude/subagents/replay-source.ts b/packages/server/src/server/agent/providers/claude/subagents/replay-source.ts index ff1f91ddb7..23ba35c0fe 100644 --- a/packages/server/src/server/agent/providers/claude/subagents/replay-source.ts +++ b/packages/server/src/server/agent/providers/claude/subagents/replay-source.ts @@ -295,7 +295,7 @@ function observeSubagent( lastTimestamp, ); if (subtitle) observations.push(subtitle); - observations.push(...timeline); + for (const observation of timeline) observations.push(observation); const terminalStatus = link.status ?? childTerminalStatus; if (terminalStatus) { observations.push({ @@ -346,9 +346,15 @@ export function observeReplaySubagents(input: { const { ownerId, parent, link } = resolved; // Only proven descendants may own notifications; ambient sidecars cannot claim them. - observations.push( - ...observeSubagent(subagent, parent, input.convertEntry, toolOwners, ownerId), - ); + for (const observation of observeSubagent( + subagent, + parent, + input.convertEntry, + toolOwners, + ownerId, + )) { + observations.push(observation); + } if (subagent.parentFacts) resolvedParents.set(link.id, subagent.parentFacts); unresolved.splice(index, 1); madeProgress = true; diff --git a/packages/server/src/server/agent/providers/claude/subagents/workflow-replay-source.ts b/packages/server/src/server/agent/providers/claude/subagents/workflow-replay-source.ts index 0faa24059a..1d7ff2b1a8 100644 --- a/packages/server/src/server/agent/providers/claude/subagents/workflow-replay-source.ts +++ b/packages/server/src/server/agent/providers/claude/subagents/workflow-replay-source.ts @@ -79,15 +79,15 @@ export function observeReplayWorkflows(input: { }); } - observations.push( - ...observeWorkflowTimeline({ - id, - result: workflow.result, - entries: input.entriesByRunId?.get(workflow.runId) ?? [], - convertEntry: input.convertEntry, - finishedAt, - }), - ); + for (const observation of observeWorkflowTimeline({ + id, + result: workflow.result, + entries: input.entriesByRunId?.get(workflow.runId) ?? [], + convertEntry: input.convertEntry, + finishedAt, + })) { + observations.push(observation); + } const subtitle = buildClaudeSubagentSubtitle({ title: "Workflow",