Skip to content

kill() on a child whose spawn failed SIGKILLs senpi's own process group on Node < 24.22 / < 26.10 #3706

Closed

Description

@code-yeongyu

Summary

On Node releases before v24.22.0 / v26.10.0 (and every Node 22.x), calling ChildProcess.kill() on a child whose spawn failed (ENOENT, EACCES, EMFILE, a missing cwd; child.pid === undefined) sends kill(0, sig): it signals the caller's own process group. Upstream: nodejs/node#65052, fixed by nodejs/node#65054 (v26.10.0, v24.22.0; not backported to 22). @code-yeongyu/senpi declares engines.node >= 24.0.0, so every 24.0-24.21 and 26.0-26.9 install is exposed: a timeout/abort/cleanup path that fires for a command that could not start SIGKILLs senpi itself and everything else in the terminal's foreground job.

Reproduction (Node 26.7.0, macOS; same on Linux)

const { spawn } = require("node:child_process");
const c = spawn("/nonexistent/probe", []);
c.on("error", () => {});
c.kill("SIGKILL");            // pid is undefined
setTimeout(() => console.log("SURVIVED"), 500);

Run inside its own process group with a sibling sleep 60: under Node 26.7.0 the whole group dies with signal 9 (the sibling too); under Node 24.22.0 and Bun 1.4.3 it prints SURVIVED.

It surfaced in senpi#3590: a tool-child start test that spawns a missing binary SIGKILLed its own vitest runner on a Node 26.7 host.

Expected

No senpi code path ever calls ChildProcess.kill() on a child without a pid, on any supported Node. A child that never started has nothing to stop: the call is skipped (and reports "not signalled").

Actual

Production call sites that can run with pid === undefined (audited at origin/main 9d2ef73):

  • Kill because pid is undefined (worst): coding-agent/.../cursor-cli-oauth/transport.ts:149, senpi-codemode/src/environments/py-installer.ts:232, senpi-codemode/src/kernels/shared/subprocess-process.ts:173,184 (fallback), pty/src/pipe-fallback.ts:76 (fallback), senpi-codemode/src/kernels/py/process.ts:150, senpi-codemode/src/environments/js-installer.ts:192.
  • Timeout/abort/stop paths with no pid guard: agent/src/harness/pico3/bash.ts:18, coding-agent/src/core/exec.ts:55,59, .../anthropic-subscription/availability.ts:113, .../cursor-cli-oauth/models-probe.ts:55, core/package-manager.ts:2672, core/resolve-config-command.ts:61,70, core/share/share-gh.ts:22, core/tools/find.ts:243, core/tools/grep/rg/process.ts:75, experimental/mini/server/run.ts:86, experimental/session-worker-manager.ts:437,753, modes/interactive/interactive-mode.ts:2010, modes/rpc/rpc-client.ts:396,401, utils/clipboard-command.ts:25, tui/src/autocomplete.ts:215.
    (exitCode === null / !child.killed checks do not help: a failed spawn reports neither before its error tick.)

Scope + acceptance criteria

  • One helper in coding-agent (signalSpawnedChild(child, signal): returns false and sends nothing when pid is undefined) used by every coding-agent site; the same guard inline in agent, pty, tui and senpi-codemode (they do not depend on coding-agent).
  • Each site keeps its existing behavior for a child that did start.
  • Tests: a failed-spawn child is never signalled (spy on ChildProcess.prototype.kill, mocked, so a RED run cannot kill its runner) for the helper and the worst-case sites.
  • QA scripts / examples / tests are out of scope (dev-only, run on pinned Node 24.22+).

Related

senpi#3590 (where it surfaced; fixed there for tool-child/client.ts), nodejs/node#65052, nodejs/node#65054.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions