From 886c5837ffdcee91b16496ad281c60ac85bb277d Mon Sep 17 00:00:00 2001 From: deseltrus Date: Mon, 6 Apr 2026 09:52:36 +0200 Subject: [PATCH] fix(bg-shell): prevent signal handler accumulation + cap alert queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../extensions/bg-shell/bg-shell-lifecycle.ts | 26 ++++++++++++++----- .../extensions/bg-shell/process-manager.ts | 10 +++++-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts b/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts index 688db06c4..32ee56455 100644 --- a/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +++ b/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts @@ -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.` ); } diff --git a/src/resources/extensions/bg-shell/process-manager.ts b/src/resources/extensions/bg-shell/process-manager.ts index db707fb40..659f13e26 100644 --- a/src/resources/extensions/bg-shell/process-manager.ts +++ b/src/resources/extensions/bg-shell/process-manager.ts @@ -33,6 +33,8 @@ export const processes = new Map(); /** 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): } } -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 {