Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(core): stop the bash tool from hanging forever when a child holds the pipe - #349

Open
raymondginger2018-sudo wants to merge 1 commit into
lessweb:mainfrom
raymondginger2018-sudo:pr/bash-close-event-hang
Open

fix(core): stop the bash tool from hanging forever when a child holds the pipe#349
raymondginger2018-sudo wants to merge 1 commit into
lessweb:mainfrom
raymondginger2018-sudo:pr/bash-close-event-hang

Conversation

@raymondginger2018-sudo

Copy link
Copy Markdown

Problem

executeShellCommand only settled its promise inside the child's 'close' event. 'close' fires when the process and its stdio pipes are done, so a backgrounded descendant keeps it from ever firing:

  • cmd &, nohup ... & — the & backgrounds the whole && list, and the detached subshell inherits this tool call's stdout/stderr pipes.
  • The shell exits, the descendant keeps the pipes open, 'close' never arrives.
  • The timeout path called killProcessTree(pid) only, which is a no-op once the shell pid is gone (taskkill /PID <pid> /T /F exits 128), and it did not settle the promise either.

Net effect: resolve() never ran, so the tool call never returned and the whole CLI session wedged permanently. Observed 2026-09-13 23:44; the machine had to be hard-powered-off.

A safe-looking command is enough to trigger it:

cd repo && rm -f out.txt && nohup sh run.sh >/dev/null 2>&1 & sleep 25; echo progress

Fix

  • Settle unconditionally 2s after the timeout kill, and 2s after the child's 'exit' event (a new 'exit' handler), destroying the stdout/stderr pipes on the way out. 'exit' fires when the process is gone, before/without 'close'.
  • Ignore a late 'close' once settled, and keep the timeout status instead of letting a late 'exit' report exitCode: 0 for a command that was actually killed.
  • Append a note to the output telling the model to use run_in_background: true for detached work, so the next call does not repeat the pattern.

Detached work is unaffected: only the tool call settles early; the background process keeps running.

Tests

packages/core/src/tests/tool-handlers.test.ts — new case "Bash settles when a background descendant keeps the output pipe open":

sleep 5 & printf 'hi\n'

sleep 5 & inherits the pipes and outlives the shell. With a 60s configured timeout the call must still return in a few seconds, report ok, include the note, and emit exactly one onProcessExit.

node --import tsx --test --test-concurrency=1 packages/core/src/tests/tool-handlers.test.ts
→ 4 tests / 4 pass
(held-pipe case settles in ~2.1s; existing timeout + background cases unchanged)

tsc --noEmit is clean.

… the pipe

executeShellCommand only resolved the tool call inside the child's 'close'
event. A backgrounded descendant (`cmd &`, `nohup ... &`) inherits the tool
call's stdout/stderr pipes and keeps them open after the shell itself exits,
so 'close' never fires; the timeout path called killProcessTree(pid) only,
which is a no-op once that pid is gone (taskkill /PID <pid> /T /F exits
128) and did not settle the promise either. The promise never settled, so
the tool call never returned and the whole CLI session hung permanently
(observed 2026-09-13 23:44; the machine had to be hard-powered-off).

- settle unconditionally 2s after the timeout kill, and 2s after the
  child's 'exit' event, destroying the pipes on the way out
- ignore a late 'close' once settled, and keep the timeout status instead
  of letting 'exit' report exitCode 0 for a killed command
- append a note telling the model to use run_in_background for detached work
- also validate the stored session cwd (fs.statSync().isDirectory()): a
  poisoned cwd (git-bash /tmp/x stored as \tmp\x) made every later spawn
  fail with ENOENT, killing the bash tool for the rest of the session

Regression test: "Bash settles when a background descendant keeps the
output pipe open".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant