From 4c350bc4c8c62bff519840376fe536375cac4f3b Mon Sep 17 00:00:00 2001 From: Kyle Chen Date: Thu, 12 Feb 2026 22:01:33 +0800 Subject: [PATCH] Fix: Prevent file descriptor leaks in child process cleanup (#13565) * fix: prevent FD leaks in child process cleanup - Destroy stdio streams (stdin/stdout/stderr) after process exit - Remove event listeners to prevent memory leaks - Clean up child process reference in moveToFinished() - Also fixes model override handling in agent.ts Fixes EBADF errors caused by accumulating file descriptors from sub-agent spawns. * Fix: allow stdin destroy in process registry cleanup --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- src/agents/bash-process-registry.ts | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/agents/bash-process-registry.ts b/src/agents/bash-process-registry.ts index 5d48da89ce5..171b5f4527f 100644 --- a/src/agents/bash-process-registry.ts +++ b/src/agents/bash-process-registry.ts @@ -20,6 +20,8 @@ export type ProcessStatus = "running" | "completed" | "failed" | "killed"; export type SessionStdin = { write: (data: string, cb?: (err?: Error | null) => void) => void; end: () => void; + // When backed by a real Node stream (child.stdin), this exists; for PTY wrappers it may not. + destroy?: () => void; destroyed?: boolean; }; @@ -157,6 +159,38 @@ export function markBackgrounded(session: ProcessSession) { function moveToFinished(session: ProcessSession, status: ProcessStatus) { runningSessions.delete(session.id); + + // Clean up child process stdio streams to prevent FD leaks + if (session.child) { + // Destroy stdio streams to release file descriptors + session.child.stdin?.destroy?.(); + session.child.stdout?.destroy?.(); + session.child.stderr?.destroy?.(); + + // Remove all event listeners to prevent memory leaks + session.child.removeAllListeners(); + + // Clear the reference + delete session.child; + } + + // Clean up stdin wrapper - call destroy if available, otherwise just remove reference + if (session.stdin) { + // Try to call destroy/end method if exists + if (typeof session.stdin.destroy === "function") { + session.stdin.destroy(); + } else if (typeof session.stdin.end === "function") { + session.stdin.end(); + } + // Only set flag if writable + try { + (session.stdin as { destroyed?: boolean }).destroyed = true; + } catch { + // Ignore if read-only + } + delete session.stdin; + } + if (!session.backgrounded) { return; }