Skip to content

fix(core): resolve spawn on exit not close to avoid bg-child hang#29108

Open
divitkashyap wants to merge 2 commits into
anomalyco:devfrom
divitkashyap:fix/bash-background-hang-20902
Open

fix(core): resolve spawn on exit not close to avoid bg-child hang#29108
divitkashyap wants to merge 2 commits into
anomalyco:devfrom
divitkashyap:fix/bash-background-hang-20902

Conversation

@divitkashyap
Copy link
Copy Markdown

@divitkashyap divitkashyap commented May 24, 2026

Issue for this PR

Closes #20902
Closes #29294

Type of change

  • Bug fix

What does this PR do?

The cross-spawn spawner (packages/core/src/cross-spawn-spawner.ts) only settled its exit Deferred on the Node close event. close waits for all stdio descriptors to drain — so if the spawned command forks a background grandchild that inherits stdout/stderr (e.g. cmd &, daemonized servers, the Gradle daemon, long-lived stdio MCP servers), close never fires and the shell tool hangs until the configured timeout (~2 minutes).

The fix is to settle on whichever of exit or close fires first. exit fires when the direct child exits regardless of inherited-fd lifetime — which is what callers of handle.exitCode actually want. close is kept as a fallback in case exit never fires for some reason. The exit-code/signal payload comes from whichever event ran first, which matches what Node passes to both.

How did you verify your code works?

  • Added a regression test in packages/opencode/test/tool/shell.test.ts that runs sleep 30 & disown; echo backgrounded through the shell tool and asserts it returns in well under the timeout. Without the fix it hangs the full 15s test timeout; with the fix it returns in ~1s.
  • Ran the full packages/opencode/test/tool/shell.test.ts suite (24 tests pass) and packages/core/test/effect/cross-spawn-spawner.test.ts (24 tests pass) locally.
  • Did not run the full repo typecheck — that pipeline is slow and the change is isolated to one well-typed function.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@divitkashyap
Copy link
Copy Markdown
Author

@thdxr would appreciate a look when you have a moment — small, contained fix with a regression test. Happy to iterate on the approach if you'd prefer something different.

@divitkashyap
Copy link
Copy Markdown
Author

Thanks @YOMXXX 🙏 — appreciate you running the suite and stepping aside. Good call on the core-level test; the shell-tool test exercises the right end-to-end behavior but a cross-spawn-spawner test that asserts handle.exitCode resolves while an inherited stdout pipe stays open would pin the contract at the layer where the fix actually lives. I'll add one.

Also adding Closes #29294 to the description so both issues track here.

The cross-spawn spawner only resolved its exit Deferred on the Node
`close` event, which waits for all stdio descriptors to drain. When a
spawned command forks a background grandchild that inherits those
descriptors (e.g. `cmd &`, daemonized servers, gradle daemon, MCP
stdio servers), `close` never fires and the shell tool hangs until
the configured timeout (~2 minutes).

Settle the exit Deferred on whichever of `exit` / `close` fires first.
`exit` fires when the direct child process exits regardless of
inherited-fd lifetime, which is what callers actually care about.

Closes anomalyco#20902
Pins the contract at the layer where the fix lives: bash backgrounds
a long-lived sleep that inherits stdout, and we assert handle.exitCode
resolves in <2s. Per @YOMXXX review on anomalyco#29108. Verified the test times
out (>10s) when the spawner is reverted to settling only on close.
@divitkashyap divitkashyap force-pushed the fix/bash-background-hang-20902 branch from 805ce1a to 416df5d Compare May 27, 2026 00:55
@divitkashyap
Copy link
Copy Markdown
Author

Added a core-level regression test per @YOMXXX's suggestion: 416df5d. It asserts handle.exitCode resolves in <2s while a backgrounded sleep 30 keeps the inherited stdout fd open. Verified it times out (>10s) when the spawner is reverted to settling only on close.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant