perf(interactive): cap rendered chat components + kill orphan descendants

Chat component cap: After 100 rendered components, oldest are removed
from the container (session transcript persists on disk via
SessionManager). Prevents unbounded memory growth in long sessions
where thousands of tool calls accumulate DOM-like component trees.

Orphan process prevention: On shutdown, listDescendants(process.pid)
finds ALL child processes (including those spawned by the Bash tool
that bg-shell doesn't track) and kills them with SIGTERM + 500ms
grace + SIGKILL. Prevents orphaned dev servers, build processes, etc.
from persisting after session exit.
This commit is contained in:
deseltrus 2026-04-06 09:52:20 +02:00
parent c5227f7570
commit 0b40d39b0e

View file

@ -7,6 +7,7 @@ import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { listDescendants } from "@gsd/native";
import type { AgentMessage } from "@gsd/pi-agent-core";
import type { AssistantMessage, ImageContent, Message, Model, OAuthProviderId } from "@gsd/pi-ai";
import type {
@ -157,6 +158,10 @@ export interface InteractiveModeOptions {
}
export class InteractiveMode {
// Cap rendered chat components to prevent unbounded memory/CPU growth.
// Only render-components are removed — session transcript stays on disk.
private static readonly MAX_CHAT_COMPONENTS = 100;
private session: AgentSession;
private ui: TUI;
private chatContainer: Container;
@ -2138,6 +2143,18 @@ export class InteractiveMode {
const _exhaustive: never = message;
}
}
this.trimChatHistory();
}
/**
* Remove oldest components when chat exceeds MAX_CHAT_COMPONENTS.
* Only render-components are removed session data stays in SessionManager.
*/
private trimChatHistory(): void {
while (this.chatContainer.children.length > InteractiveMode.MAX_CHAT_COMPONENTS) {
const oldest = this.chatContainer.children[0];
this.chatContainer.removeChild(oldest);
}
}
/**
@ -2232,6 +2249,7 @@ export class InteractiveMode {
}
this.pendingTools.clear();
this.trimChatHistory();
this.ui.requestRender();
}
@ -2325,6 +2343,21 @@ export class InteractiveMode {
if (shutdownBehavior === "stop_ui") {
return;
}
// Kill ALL descendant processes to prevent orphans (next-server, pnpm dev, etc.)
try {
const descendants = listDescendants(process.pid);
for (const childPid of descendants) {
try { process.kill(childPid, "SIGTERM"); } catch {}
}
if (descendants.length > 0) {
await new Promise(resolve => setTimeout(resolve, 500));
for (const childPid of descendants) {
try { process.kill(childPid, "SIGKILL"); } catch {}
}
}
} catch {}
process.exit(0);
}