diff --git a/packages/host-service/src/daemon/DaemonSupervisor.ts b/packages/host-service/src/daemon/DaemonSupervisor.ts index d139fc25f6e..780fac08fda 100644 --- a/packages/host-service/src/daemon/DaemonSupervisor.ts +++ b/packages/host-service/src/daemon/DaemonSupervisor.ts @@ -942,16 +942,28 @@ export class DaemonSupervisor { // Prod: detached so PTYs survive host-service restarts via socket // adoption. Dev: attached as defense-in-depth in case serve.ts's // dev shutdown doesn't fire (e.g. host-service crash). - child = childProcess.spawn( - process.execPath, - [this.opts.scriptPath, `--socket=${socketPath}`], - { - detached: !isDev, - stdio, - env: childEnv, - windowsHide: true, - }, - ); + // Raise RLIMIT_NOFILE before exec: macOS's 256 soft default starves a + // daemon hosting many worktrees' PTYs and surfaces as node-pty + // "posix_spawnp failed" (EMFILE). The raised limit is inherited by + // handoff successors the daemon spawns from itself. + const isWindows = process.platform === "win32"; + const command = isWindows ? process.execPath : "/bin/sh"; + const commandArgs = isWindows + ? [this.opts.scriptPath, `--socket=${socketPath}`] + : [ + "-c", + 'ulimit -n 1048576 2>/dev/null || ulimit -n "$(ulimit -Hn)" 2>/dev/null || true; exec "$@"', + "sh", + process.execPath, + this.opts.scriptPath, + `--socket=${socketPath}`, + ]; + child = childProcess.spawn(command, commandArgs, { + detached: !isDev, + stdio, + env: childEnv, + windowsHide: true, + }); } finally { if (logFd >= 0) { try { diff --git a/packages/pty-daemon/src/Pty/Pty.ts b/packages/pty-daemon/src/Pty/Pty.ts index 054b09c6172..6bf8a83d4a7 100644 --- a/packages/pty-daemon/src/Pty/Pty.ts +++ b/packages/pty-daemon/src/Pty/Pty.ts @@ -142,6 +142,21 @@ function validateDims(cols: number, rows: number): void { } } +function reprobeErrno(meta: SessionMeta): string { + try { + const probe = childProcess.spawnSync(meta.shell, ["-c", ":"], { + cwd: meta.cwd, + timeout: 1000, + stdio: "ignore", + }); + if (!probe.error) return "ok"; + const e = probe.error as NodeJS.ErrnoException; + return e.code ?? e.message; + } catch (e) { + return `reprobe-failed:${(e as Error).message}`; + } +} + export function spawn({ meta }: SpawnOptions): Pty { validateDims(meta.cols, meta.rows); // Pre-flight: node-pty's "posix_spawnp failed" message swallows errno @@ -178,9 +193,11 @@ export function spawn({ meta }: SpawnOptions): Pty { encoding: null, }); } catch (err) { - // Annotate with shell + cwd so the wire-error reply is actionable. + // node-pty's native "posix_spawnp failed." drops the errno, so re-probe + // the same shell+cwd with spawnSync to surface the real code (e.g. + // EMFILE/EAGAIN/ENOENT). throw new Error( - `spawn failed (shell=${meta.shell} cwd=${meta.cwd ?? "(none)"}): ${(err as Error).message}`, + `spawn failed (shell=${meta.shell} cwd=${meta.cwd ?? "(none)"} errno=${reprobeErrno(meta)}): ${(err as Error).message}`, ); } const adapter = new NodePtyAdapter(term, meta);