Repository navigation
Description
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.killedchecks 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 whenpidis 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.
Activity
fix: a failed-spawn worker connection never signals on close; LOW gua…
fix: never signal a child whose spawn failed (code-yeongyu#3706)
docs: changes.md trackers and changelog entries for code-yeongyu#3706