Repository navigation
v2: interrupting the shell tool while command creation is pending can orphan the process manager #48838
Description
Description
Found while porting a V1 plugin to the V2 plugin API. My plugin wraps shell execution in a sandbox and needs session interruption to reach the wrapped process. Tracing V2's shell path, I found an ownership gap during Shell.create's ready wait. The analysis is against v2.0.2 (commit ea5ae232); the probe under Steps to reproduce is written to confirm or falsify the finding on a live install.
The chain, at commit ea5ae232:
Shell.createcaptures the service context and buildsrunForkWithfrom it (packages/core/src/shell.ts:129-130). In the pinnedeffect@4.0.0-rc.112,runForkWithcreates a fresh fiber with no link to the calling fiber or any scope unlessoptions.signalis passed (packages/effect/src/internal/effect.ts:5413-5437). Shell passes none.createforks the scoped process manager withrunForkand discards the returned handle (shell.ts:286-305). The caller then awaits a readiness deferred on its own (shell.ts:406-416).- Interrupting the caller removes the readiness waiter only (
packages/effect/src/Deferred.ts:173-186); the producer keeps running. The manager holds the process and stays alive awaitingcommand.done. - Cleanup does exist: pending-launch kill and acquired-process release in
packages/util/src/cross-spawn-spawner.ts:270-296and:419-452. Those finalizers belong to the manager's scope, and at this commit caller interruption has no path into it. Also noteacquireReleasemasks interruption during acquisition by default (internal/effect.ts:3971-3987) and the spawner keeps that default. - The command timeout is installed after spawn, registration, and the output-file open wait (
shell.ts:338-397), so it bounds the run but leaves the readiness interval open. Service teardown interrupts registered commands'donedeferreds (shell.ts:139-149) and reaps them at shutdown; cancellation needs its own path.
Net effect: a session interrupted while the shell tool is between spawn and shell ID can leave the manager and its process running, with no handle any cleanup path knows about. The window's practical duration and frequency still need a runtime measurement.
A fix would keep create's ownership cancellable until successful handoff, for example by passing an interruption signal into runForkWith or holding the acquisition inside the caller's scope until the shell ID exists, while bounding pending creation and cleanup and leaving intentional post-handoff background execution untouched.
Related: #40829 (abort waits for bash to finish naturally), #47350 (stdio held by a background process hangs the tool), and PR #42756 (exit/stdio completion). Those cover behavior after a shell ID exists or during a normal run. The gap above is the interval before the caller receives one.
Plugins
oc-arkive (local V1 plugin in development)
OpenCode version
v2.0.2 source (commit ea5ae23)
Steps to reproduce
The probe is designed so a negative result falsifies the finding:
- Start a shell command whose output-file open is blocked (or stub the spawner so readiness stays pending), with the timeout disabled.
- Once the process exists but before
shell.createreturns, interrupt only the calling tool/session. - Exclude natural exit, the command timeout, and service teardown as causes.
- Observe the manager fiber and the spawned process separately: if the gap is real, neither is terminated by the caller's interruption.
- Compare with interruption after the shell ID is returned, where job-based cancellation works.
Operating System
Linux (Ubuntu)
Terminal
n/a
Activity
HermanGrondahl commentedon Sep 30, 2026
Revalidated this on current v2: the pre-handoff ownership gap still reproduces deterministically after process registration and before Shell.create returns. I opened #52386 with the rollback fix and regression coverage, including the finish/remove publication race.