fix(bg-shell): prevent signal handler accumulation + cap alert queue

Signal handlers (SIGTERM, SIGINT, beforeExit) were registered on every
session_start but never removed. Over multiple sessions within the same
process, handlers accumulated — each adding another cleanupAll() call
and descendant kill sweep on exit.

Fix: session_shutdown now calls process.off() for each handler before
cleanupAll(), preventing accumulation.

Also: signalCleanup now kills ALL descendant processes (not just those
tracked by bg-shell) to catch bash-tool spawned children.

Alert queue: pendingAlerts is capped at 50 entries to prevent unbounded
growth when background processes generate rapid alerts faster than the
agent consumes them.

pushAlert signature updated to accept null bg parameter for system-level
alerts that don't originate from a tracked process.
This commit is contained in:
deseltrus 2026-04-06 09:52:36 +02:00
parent 0b40d39b0e
commit 886c5837ff
2 changed files with 27 additions and 9 deletions

View file

@ -16,6 +16,7 @@ import {
import {
processes,
pendingAlerts,
pushAlert,
cleanupAll,
cleanupSessionProcesses,
persistManifest,
@ -37,19 +38,30 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
}
}
// Clean up on session shutdown
pi.on("session_shutdown", async () => {
cleanupAll();
});
// Register signal handlers to clean up bg processes on unexpected exit (fixes #428)
const signalCleanup = () => {
cleanupAll();
// Also kill bash-tool spawned children that bg-shell doesn't track
try {
const { listDescendants } = require("@gsd/native") as typeof import("@gsd/native");
const descendants = listDescendants(process.pid);
for (const childPid of descendants) {
try { process.kill(childPid, "SIGKILL"); } catch {}
}
} catch {}
};
process.on("SIGTERM", signalCleanup);
process.on("SIGINT", signalCleanup);
process.on("beforeExit", signalCleanup);
// Clean up on session shutdown — remove signal handlers to prevent accumulation
pi.on("session_shutdown", async () => {
process.off("SIGTERM", signalCleanup);
process.off("SIGINT", signalCleanup);
process.off("beforeExit", signalCleanup);
cleanupAll();
});
// ── Compaction Awareness: Survive Context Resets ───────────────
/** Build a compact state summary of all alive processes for context re-injection */
@ -65,7 +77,7 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
return ` - id:${p.id} "${p.label}" [${p.processType}] status:${p.status} uptime:${formatUptime(Date.now() - p.startedAt)}${portInfo}${urlInfo}${errInfo}${groupInfo}`;
}).join("\n");
pendingAlerts.push(
pushAlert(null,
`${reason} ${alive.length} background process(es) are still running:\n${processSummaries}\nUse bg_shell digest/output/kill with these IDs.`
);
}
@ -150,7 +162,7 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
` - ${s.id}: ${s.label} (pid ${s.pid}, type: ${s.processType}${s.group ? `, group: ${s.group}` : ""})`
).join("\n");
pendingAlerts.push(
pushAlert(null,
`${surviving.length} background process(es) from previous session still running:\n${summary}\n Note: These processes are outside bg_shell's control. Kill them manually if needed.`
);
}

View file

@ -33,6 +33,8 @@ export const processes = new Map<string, BgProcess>();
/** Pending alerts to inject into the next agent context */
export let pendingAlerts: string[] = [];
const MAX_PENDING_ALERTS = 50;
/** Replace the pendingAlerts array (used by the extension entry point) */
export function setPendingAlerts(alerts: string[]): void {
pendingAlerts = alerts;
@ -58,8 +60,12 @@ export function addEvent(bg: BgProcess, event: Omit<ProcessEvent, "timestamp">):
}
}
export function pushAlert(bg: BgProcess, message: string): void {
pendingAlerts.push(`[bg:${bg.id} ${bg.label}] ${message}`);
export function pushAlert(bg: BgProcess | null, message: string): void {
const prefix = bg ? `[bg:${bg.id} ${bg.label}] ` : "";
pendingAlerts.push(`${prefix}${message}`);
if (pendingAlerts.length > MAX_PENDING_ALERTS) {
pendingAlerts.splice(0, pendingAlerts.length - MAX_PENDING_ALERTS);
}
}
export function getInfo(p: BgProcess): BgProcessInfo {