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:
parent
c5227f7570
commit
0b40d39b0e
1 changed files with 33 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue