fix(pi): prevent oversized subagent result crashes

This commit is contained in:
2026-08-02 07:24:52 -04:00
parent af643c452d
commit cb26a044d3
3 changed files with 48 additions and 9 deletions

View File

@@ -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); 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 () => { test("process failure reaches failed with diagnostics", async () => {
const runner = new FakeRunner(); const runner = new FakeRunner();
const supervisor = new Supervisor(runner, "/tmp"); const supervisor = new Supervisor(runner, "/tmp");
@@ -303,6 +319,22 @@ test("maxConcurrent preserves queued records", async () => {
assert.equal(runner.starts.length, 2); 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 () => { test("terminal records stay listed past ttl and remain retrievable until cleared", async () => {
const runner = new FakeRunner(); const runner = new FakeRunner();
const supervisor = new Supervisor(runner, "/tmp", { recentTerminalTtlMs: 5 }); const supervisor = new Supervisor(runner, "/tmp", { recentTerminalTtlMs: 5 });

View File

@@ -44,6 +44,8 @@ const DEFAULT_TIMEOUTS = {
runMs: 0, runMs: 0,
}; };
const MAX_ACTIVITY_HISTORY = 100;
export class Supervisor { export class Supervisor {
private nextChild = 0; private nextChild = 0;
private readonly children = new Map<string, RunningChild>(); private readonly children = new Map<string, RunningChild>();
@@ -90,14 +92,14 @@ export class Supervisor {
return cloneResult(this.require(id).record); 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; const selectedIds = ids ? [...new Set(ids.map((id) => id.trim()).filter(Boolean))] : undefined;
if (selectedIds) for (const id of selectedIds) this.require(id); if (selectedIds) for (const id of selectedIds) this.require(id);
const cleared: SubagentStatus[] = []; const cleared: string[] = [];
for (const [id, child] of this.children) { for (const [id, child] of this.children) {
if (selectedIds && !selectedIds.includes(id)) continue; if (selectedIds && !selectedIds.includes(id)) continue;
if (!isTerminal(child.record.status.state)) continue; if (!isTerminal(child.record.status.state)) continue;
cleared.push(cloneStatus(child.record.status)); cleared.push(id);
this.children.delete(id); this.children.delete(id);
} }
if (cleared.length > 0) this.emitChange(); if (cleared.length > 0) this.emitChange();
@@ -344,6 +346,9 @@ export class Supervisor {
const summary = summarizeActivity(activity); const summary = summarizeActivity(activity);
record.status.currentActivity = summary; record.status.currentActivity = summary;
record.status.activityHistory.push(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; return activity;
} }

View File

@@ -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 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 --- 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 +++ 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); 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[] { + private renderGroup(children: Component[], width: number): string[] {
+ const lines: string[] = []; + const lines: string[] = [];
+ for (const child of children) { + for (const child of children) {
+ lines.push(...child.render(width)); + for (const line of child.render(width)) {
+ lines.push(line);
+ }
+ } + }
+ return lines; + 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 = 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."; "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 fdPath: string | undefined;
private editorContainer: Container; private editorContainer: Container;
private footer: FooterComponent; private footer: FooterComponent;
@@ -86,7 +88,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p
private footerDataProvider: FooterDataProvider; private footerDataProvider: FooterDataProvider;
// Stored so the same manager can be injected into custom editors, selectors, and extension UI. // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
private keybindings: KeybindingsManager; private keybindings: KeybindingsManager;
@@ -477,7 +517,9 @@ @@ -477,7 +519,9 @@
this.editorContainer = new Container(); this.editorContainer = new Container();
this.editorContainer.addChild(this.editor as Component); this.editorContainer.addChild(this.editor as Component);
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd()); 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); this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
// Load hide thinking block setting // Load hide thinking block setting
@@ -704,19 +746,25 @@ @@ -704,19 +748,25 @@
console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`)); 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.ui.setFocus(this.editor);
this.setupKeyHandlers(); this.setupKeyHandlers();
@@ -2033,25 +2081,25 @@ @@ -2033,25 +2083,25 @@
| ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void }) | ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })
| undefined, | undefined,
): void { ): void {