From cb26a044d3c1d3ce2848f4b8a9273969b477d44d Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 2 Aug 2026 07:24:52 -0400 Subject: [PATCH] fix(pi): prevent oversized subagent result crashes --- .../extensions/subagents/supervisor.test.ts | 32 +++++++++++++++++++ .../pi/extensions/subagents/supervisor.ts | 11 +++++-- .../agents/pi/patches/pi-flex-spacer.patch | 14 ++++---- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/modules/agents/pi/extensions/subagents/supervisor.test.ts b/modules/agents/pi/extensions/subagents/supervisor.test.ts index f1503ba..97b5b3c 100644 --- a/modules/agents/pi/extensions/subagents/supervisor.test.ts +++ b/modules/agents/pi/extensions/subagents/supervisor.test.ts @@ -140,6 +140,22 @@ test("activity exposes ordered transcript events while status and list keep only assert.doesNotMatch(JSON.stringify(listed), /private transcript body|secret file contents/u); }); +test("status activity history keeps only the 100 most recent summaries", async () => { + const runner = new FakeRunner(); + const supervisor = new Supervisor(runner, "/tmp"); + const accepted = await spawnStarted(supervisor); + + for (let index = 0; index < 150; index += 1) { + runner.starts[0].events.running(`tick ${index}`); + } + + const history = supervisor.status(accepted.id).activityHistory; + + assert.equal(history.length, 100); + assert.equal(history[0].summary, "tick 50"); + assert.equal(history[99].summary, "tick 149"); +}); + test("process failure reaches failed with diagnostics", async () => { const runner = new FakeRunner(); const supervisor = new Supervisor(runner, "/tmp"); @@ -303,6 +319,22 @@ test("maxConcurrent preserves queued records", async () => { assert.equal(runner.starts.length, 2); }); +test("clearTerminal returns only removed terminal ids", async () => { + const runner = new FakeRunner(); + const supervisor = new Supervisor(runner, "/tmp"); + const first = await spawnStarted(supervisor, "one"); + const second = await spawnStarted(supervisor, "two"); + const running = await spawnStarted(supervisor, "three"); + + runner.starts[0].events.completed("one done", "agent_settled"); + runner.starts[1].events.completed("two done", "agent_settled"); + + assert.deepEqual(supervisor.clearTerminal(), [first.id, second.id]); + assert.throws(() => supervisor.status(first.id), /unknown subagent id/); + assert.throws(() => supervisor.status(second.id), /unknown subagent id/); + assert.equal(supervisor.status(running.id).state, "running"); +}); + test("terminal records stay listed past ttl and remain retrievable until cleared", async () => { const runner = new FakeRunner(); const supervisor = new Supervisor(runner, "/tmp", { recentTerminalTtlMs: 5 }); diff --git a/modules/agents/pi/extensions/subagents/supervisor.ts b/modules/agents/pi/extensions/subagents/supervisor.ts index 9bcf5a9..75963a8 100644 --- a/modules/agents/pi/extensions/subagents/supervisor.ts +++ b/modules/agents/pi/extensions/subagents/supervisor.ts @@ -44,6 +44,8 @@ const DEFAULT_TIMEOUTS = { runMs: 0, }; +const MAX_ACTIVITY_HISTORY = 100; + export class Supervisor { private nextChild = 0; private readonly children = new Map(); @@ -90,14 +92,14 @@ export class Supervisor { return cloneResult(this.require(id).record); } - clearTerminal(ids?: string[]): SubagentStatus[] { + clearTerminal(ids?: string[]): string[] { const selectedIds = ids ? [...new Set(ids.map((id) => id.trim()).filter(Boolean))] : undefined; if (selectedIds) for (const id of selectedIds) this.require(id); - const cleared: SubagentStatus[] = []; + const cleared: string[] = []; for (const [id, child] of this.children) { if (selectedIds && !selectedIds.includes(id)) continue; if (!isTerminal(child.record.status.state)) continue; - cleared.push(cloneStatus(child.record.status)); + cleared.push(id); this.children.delete(id); } if (cleared.length > 0) this.emitChange(); @@ -344,6 +346,9 @@ export class Supervisor { const summary = summarizeActivity(activity); record.status.currentActivity = summary; record.status.activityHistory.push(summary); + if (record.status.activityHistory.length > MAX_ACTIVITY_HISTORY) { + record.status.activityHistory.splice(0, record.status.activityHistory.length - MAX_ACTIVITY_HISTORY); + } return activity; } diff --git a/modules/agents/pi/patches/pi-flex-spacer.patch b/modules/agents/pi/patches/pi-flex-spacer.patch index 1c2d733..9ed2086 100644 --- a/modules/agents/pi/patches/pi-flex-spacer.patch +++ b/modules/agents/pi/patches/pi-flex-spacer.patch @@ -32,7 +32,7 @@ diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/codin diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts 2026-08-01 18:41:36.963495957 -0400 +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts 2026-08-01 18:43:04.876341236 -0400 -@@ -210,6 +210,45 @@ +@@ -210,6 +210,47 @@ return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code); } @@ -56,7 +56,9 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p + private renderGroup(children: Component[], width: number): string[] { + const lines: string[] = []; + for (const child of children) { -+ lines.push(...child.render(width)); ++ for (const line of child.render(width)) { ++ lines.push(line); ++ } + } + return lines; + } @@ -78,7 +80,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage. Disable this warning in /settings."; -@@ -335,6 +374,7 @@ +@@ -335,6 +376,7 @@ private fdPath: string | undefined; private editorContainer: Container; private footer: FooterComponent; @@ -86,7 +88,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p private footerDataProvider: FooterDataProvider; // Stored so the same manager can be injected into custom editors, selectors, and extension UI. private keybindings: KeybindingsManager; -@@ -477,7 +517,9 @@ +@@ -477,7 +519,9 @@ this.editorContainer = new Container(); this.editorContainer.addChild(this.editor as Component); this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd()); @@ -96,7 +98,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled); // Load hide thinking block setting -@@ -704,19 +746,25 @@ +@@ -704,19 +748,25 @@ console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`)); } @@ -134,7 +136,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p this.ui.setFocus(this.editor); this.setupKeyHandlers(); -@@ -2033,25 +2081,25 @@ +@@ -2033,25 +2083,25 @@ | ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void }) | undefined, ): void {