Skip to content

fix(tools): stop the bash tool hanging when a detached child holds the pipes#875

Open
abhay-codes07 wants to merge 1 commit into
mistralai:mainfrom
abhay-codes07:fix/bash-detached-child-hang
Open

fix(tools): stop the bash tool hanging when a detached child holds the pipes#875
abhay-codes07 wants to merge 1 commit into
mistralai:mainfrom
abhay-codes07:fix/bash-detached-child-hang

Conversation

@abhay-codes07

Copy link
Copy Markdown

Fixes #831

A tool call that spawns a detached or backgrounded child process left the bash tool stuck until its timeout (300s by default) even though the command itself finished instantly, exactly as the standalone reproducer in #831 shows.

Root cause, two layers deep:

  1. proc.communicate() waits for EOF on stdout/stderr, and a detached child that inherited those pipes keeps them open for as long as it lives.
  2. Swapping in proc.wait() does not help: asyncio's BaseSubprocessTransport only wakes exit waiters in _call_connection_lost, which requires the process to have exited AND every pipe transport to have disconnected (_try_finish in CPython's asyncio/base_subprocess.py). So wait() is pipe-hostage too. I verified this empirically before landing on the approach below.

Change in the bash tool:

  • stdout/stderr are pumped concurrently into buffers bounded by max_output_bytes (reading continues past the cap so a chatty child never blocks on a full pipe; this also stops a flood of output from buffering unbounded in memory, which communicate() did)
  • process exit is detected by polling returncode, which the transport sets as soon as the process exits, with the timeout as deadline
  • after exit, the pipes get a 2 second grace period to reach EOF, then the tool returns with whatever output was captured

kill_async_subprocess had the same pipe dependency through proc.wait() after killing, which could stall the timeout path indefinitely when the surviving pipe holder was outside the killed group; it polls returncode now as well.

Testing:

  • New test spawning a grandchild that holds the inherited pipes for 15s and asserting the tool returns with the parent's output well before that
  • Full tests/tools/test_bash.py and tests/agent_loop/e2e/test_e2e_bash.py suites pass on Linux (50 tests), ruff and pyright clean
  • Manually verified before/after: the reproducer scenario went from blocking the full 15s to returning in about 2s, with normal commands and timeout enforcement unchanged

@michelTho this closes out the fuzzer-style reproducer in #831.

…e pipes

A command that spawns a detached or backgrounded child inheriting
stdout/stderr left the bash tool blocked until its timeout even though
the command itself exited immediately: communicate() waits for EOF on
the pipes, and asyncio's Process.wait() has the same behavior because
BaseSubprocessTransport only wakes exit waiters once every pipe has
disconnected, not when the process exits.

The tool now pumps stdout and stderr concurrently into bounded buffers,
polls returncode (set as soon as the process exits) with a deadline for
the timeout, and gives the pipes a short grace period to reach EOF
before returning with whatever output was captured. Bounding the
buffers also stops a command that floods stdout from buffering
unbounded output in memory; reading continues past the cap so the child
never blocks on a full pipe.

kill_async_subprocess had the same pipe dependency through proc.wait()
after killing, which could stall the timeout path forever; it now polls
returncode as well.

Fixes mistralai#831
@abhay-codes07
abhay-codes07 requested a review from a team as a code owner July 3, 2026 09:35
Copilot AI review requested due to automatic review settings July 3, 2026 09:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

bug: Mistral Vibe hangs after detached child-process tool call

2 participants