diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index b9de81872..e231a0cd2 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -79,11 +79,13 @@ const APPROVAL_SLUG_LENGTH = 8; type PtyExitEvent = { exitCode: number; signal?: number }; type PtyListener = (event: T) => void; +type PtyDisposable = { dispose: () => void }; type PtyHandle = { pid: number; write: (data: string | Buffer) => void; - onData: (listener: PtyListener) => void; - onExit: (listener: PtyListener) => void; + onData: (listener: PtyListener) => PtyDisposable; + onExit: (listener: PtyListener) => PtyDisposable; + kill: (signal?: string) => void; }; type PtySpawn = ( file: string, @@ -361,6 +363,8 @@ async function runExecProcess(opts: { let child: ChildProcessWithoutNullStreams | null = null; let pty: PtyHandle | null = null; let stdin: SessionStdin | undefined; + let ptyDataDisposable: PtyDisposable | null = null; + let ptyExitDisposable: PtyDisposable | null = null; if (opts.sandbox) { const { child: spawned } = await spawnWithFallback({ @@ -601,7 +605,7 @@ async function runExecProcess(opts: { if (pty) { const cursorResponse = buildCursorPositionResponse(); - pty.onData((data) => { + ptyDataDisposable = pty.onData((data) => { const raw = data.toString(); const { cleaned, requests } = stripDsrRequests(raw); if (requests > 0) { @@ -664,10 +668,18 @@ async function runExecProcess(opts: { }; if (pty) { - pty.onExit((event) => { + ptyExitDisposable = pty.onExit((event) => { const rawSignal = event.signal ?? null; const normalizedSignal = rawSignal === 0 ? null : rawSignal; handleExit(event.exitCode ?? null, normalizedSignal); + // Clean up PTY resources to prevent FD leaks + try { + ptyDataDisposable?.dispose(); + ptyExitDisposable?.dispose(); + pty.kill(); + } catch { + // Ignore cleanup errors + } }); } else if (child) { child.once("close", (code, exitSignal) => { diff --git a/src/types/lydell-node-pty.d.ts b/src/types/lydell-node-pty.d.ts index be7c40b76..35d913efe 100644 --- a/src/types/lydell-node-pty.d.ts +++ b/src/types/lydell-node-pty.d.ts @@ -1,11 +1,13 @@ declare module "@lydell/node-pty" { export type PtyExitEvent = { exitCode: number; signal?: number }; export type PtyListener = (event: T) => void; + export type PtyDisposable = { dispose: () => void }; export type PtyHandle = { pid: number; write: (data: string | Buffer) => void; - onData: (listener: PtyListener) => void; - onExit: (listener: PtyListener) => void; + onData: (listener: PtyListener) => PtyDisposable; + onExit: (listener: PtyListener) => PtyDisposable; + kill: (signal?: string) => void; }; export type PtySpawn = (