fix(pi): prevent oversized subagent result crashes
This commit is contained in:
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user