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 <zonglinj@amazon.com>
This commit is contained in:
Alin Jiang
2026-10-23 11:33:39 +02:00
committed by paseo-bot[bot]
parent 8440cb2743
commit 6bfd3e8bfa
4 changed files with 70 additions and 26 deletions
@@ -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({
@@ -5261,15 +5261,13 @@ class ClaudeAgentSession implements AgentSession {
};
if (observations.length === 0) return replay;
this.persistedProviderSubagentEvents.push(
...foldSubagentObservations(observations).map(
(event): Extract<AgentStreamEvent, { type: "provider_subagent" }> => ({
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,
})),
);
});
}
}
}
@@ -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;
@@ -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",