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);
});
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 });

View File

@@ -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<string, RunningChild>();
@@ -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;
}

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
--- 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 {