Problem
exec from @effectionx/process@0.8.1 can leave the child process tree running with no owner when the calling scope is halted during acquisition. createPosixProcess spawns the OS process and then suspends several times — two fromReadable resources and four spawns — before registering the ensure that signals the process group and joins stdio EOF. A halt landing on any of those suspension points discards the pending instructions, so the kill-ensure never registers and the child runs to completion as an orphan.
This was one of the two causes of #417: an orphaned deno bundle wrote its output into a scratch directory after the scope's cleanup had already removed it.
Two smaller defects sit in the same teardown:
- The
catch (_e) { /* process is probably already dead */ } around process.kill(-pid, "SIGTERM") also swallows the join: when the kill throws, the all([stdoutDone, stderrDone]) wait is skipped entirely, so teardown can return while the tree is still winding down.
- On macOS,
killpg on a process group whose members are all zombies fails with EPERM, not ESRCH (verified empirically on Darwin 25.5.0). Code distinguishing "already dead" from "not permitted" by errno misclassifies this case.
What the repository does meanwhile
scripts/lib/contained-run.ts (from the #417 fix) registers the terminate-and-join teardown before the process exists and creates the process in the same synchronous continuation, so no halt can separate acquisition from release. The bundler is its only consumer; every other exec call site still has the upstream window.
Acceptance
@effectionx/process registers the child's termination with no suspension point after the process is created, or upstream ships an equivalent guarantee, and the pinned version here carries it.
scripts/lib/contained-run.ts is retired in favor of the upstream primitive, its tests migrated to whatever pins the upstream contract.
Problem
execfrom@effectionx/process@0.8.1can leave the child process tree running with no owner when the calling scope is halted during acquisition.createPosixProcessspawns the OS process and then suspends several times — twofromReadableresources and fourspawns — before registering theensurethat signals the process group and joins stdio EOF. A halt landing on any of those suspension points discards the pending instructions, so the kill-ensurenever registers and the child runs to completion as an orphan.This was one of the two causes of #417: an orphaned
deno bundlewrote its output into a scratch directory after the scope's cleanup had already removed it.Two smaller defects sit in the same teardown:
catch (_e) { /* process is probably already dead */ }aroundprocess.kill(-pid, "SIGTERM")also swallows the join: when the kill throws, theall([stdoutDone, stderrDone])wait is skipped entirely, so teardown can return while the tree is still winding down.killpgon a process group whose members are all zombies fails withEPERM, notESRCH(verified empirically on Darwin 25.5.0). Code distinguishing "already dead" from "not permitted" by errno misclassifies this case.What the repository does meanwhile
scripts/lib/contained-run.ts(from the #417 fix) registers the terminate-and-join teardown before the process exists and creates the process in the same synchronous continuation, so no halt can separate acquisition from release. The bundler is its only consumer; every otherexeccall site still has the upstream window.Acceptance
@effectionx/processregisters the child's termination with no suspension point after the process is created, or upstream ships an equivalent guarantee, and the pinned version here carries it.scripts/lib/contained-run.tsis retired in favor of the upstream primitive, its tests migrated to whatever pins the upstream contract.