fix(acp_adapter): windows bash hang - #69083
Conversation
|
Independent Windows 10 integration proof confirms this failure class in a real ACP host. A disposable Buzz PR #2773 managed agent launched installed The matching Git Bash probe child ran So #69083 matches both the observed stack and process-lifecycle root cause. I have not claimed this PR's exact implementation was exercised locally; this is independent reproduction evidence supporting its diagnosis and bounded tree-kill approach. |
|
@teknium1 @OutThisLife please approve Actions so CI can run |
monerostar
left a comment
There was a problem hiding this comment.
monerostar native Windows live-verify
Environment
- Windows 11 native (Build 26200) · Git Bash at
%LOCALAPPDATA%\hermes\git\bin\bash.exe - Control install:
_bash_startsstill usessubprocess.run(..., timeout=15)(unbounded post-killcommunicate()class) - PR HEAD:
b21dcec87— worktreepr-69083 - CI: required checks pass
- Related symptom class: ACP / local shell hang on Windows (also see open issue #73403)
Root cause (still present on main)
tools/environments/local.py _bash_starts probes Git Bash with external MSYS programs (true/cat). On timeout, stock subprocess.run cleanup can hang forever after kill when MSYS children keep pipes open. PR switches the probe to shared bounded_captured_run (tree-kill + bounded 1s drain) already used for git probes — helper exists on main in hermes_cli/_subprocess_compat.py; call site was the gap.
Live probe (healthy path)
| main | this PR | |
|---|---|---|
_bash_starts(bash.exe) |
True ~0.078s | True ~0.076s |
| cached second call | — | True ~0.000s |
| implementation | subprocess.run |
bounded_captured_run |
Did not force a hang repro (would need a stuck MSYS child); the control/path difference is the important part and matches the documented deadlock class.
Tests (PR tree)
pytest tests/test_windows_subprocess_no_window_flags.py -k "bounded or bash_starts or tree_kill or communicate" -q -o addopts=
→ 13 passed, 29 deselected
pytest tests/tools/test_find_shell.py -k "windows or bash_starts or bounded" -q -o addopts=
→ 4 passed, 1 skipped, 14 deselected
Broader test_find_shell.py + full no_window file: 55 passed, 3 failed on this host for reasons orthogonal to the hang fix:
- Two
TestFindShellPrefersUserShellcases — real Git Bash on PATH wins over temp fakeSHELL/bashstubs (environment / resolution order). test_suppress_platform_ver_console_posix_noop— asserts emptywin32_verunder a posix-noop path; on real Win11win32_veris populated ('10', '10.0....').
Not treating those as regressions of this PR’s bash-probe change.
Assessment
- Right fix, reuses existing bounded helper — prefer this over a third one-off timeout wrapper
- Good match for ACP adapter Windows hang reports
- CI green
Verdict: LGTM direction for the _bash_starts deadlock class.
Formal Approve: blocked for external collaborator — comment review only.
|
Thanks for targeting the remaining Git Bash probe path. The premise still holds on current Suggested changes
Automated hermes-sweeper review. |
Replace _bash_starts subprocess.run(timeout=) with shared bounded_captured_run so post-kill pipe drains cannot hang tool startup.
b21dcec to
7d72a64
Compare
Return a short stderr diagnostic from bounded_captured_run on timeout so _bash_starts can cache it, and cover retained-stdin plus git-probe fail-open.
|
Thanks for the salvage notes. Rebased onto current Also followed up with timeout stderr diagnostics so |
|
Native Windows follow-up found one process-tree cleanup ordering gap that should be addressed before merge.
Controlled proof on Windows 10:
The focused correction is two files:
Verification on current
I recommend landing this correction before maintainer approval/merge. I have the exact local two-file diff available if useful. |
Kill the process tree while the launcher PID is still discoverable; proc.kill() stays as fallback. Assert cleanup order taskkill -> kill -> bounded drain.
|
Thanks @OpenSpek - landed the cleanup ordering correction in e651925. _kill_process_tree() now runs taskkill /T /F before proc.kill() on Windows so descendants stay discoverable; proc.kill() remains the fallback. The timeout unit test asserts taskkill -> kill -> bounded drain. Focused tests: 7 passed for bounded/tree-kill/timeout paths. |
Independent validation — Windows 10 + Git Bash + ACP (multica) ✓We independently validated this fix on a real Windows ACP deployment, with confirmation from two separate agent instances. Environment:
Before (current v0.20.0): Daemon log showed "Creating new local environment for task default..." with NO further stderr output. Agent PID stayed alive, produced zero messages, killed by idle watchdog after 45 min (status=idle_watchdog, tool_in_flight=false). After (this PR's fix backported to v0.20.0):
Results:
The root cause matches the diagnosis: Confirmed working on Windows 10 + real ACP host (multica). No regressions in non-ACP CLI usage. Thanks for the fix! |
…ws-bash-hang # Conflicts: # hermes_cli/_subprocess_compat.py
|
Thank you, @kaverjody, for the thorough review. I have addressed the feedback, and the merge conflicts are now resolved. @teknium1, could you please review and merge this PR when you have a chance? This is a serious Windows bug: a timed-out Git Bash startup probe can hang indefinitely while draining inherited pipes, blocking terminal and tool startup. |
|
This is the canonical fix for the bash probe hang — thanks for the thorough approach with
Both are rebased onto current |
Resolve test_find_shell conflict: keep bounded_captured_run mocks and sys/Path imports; take main's windows_only markers.
What does this PR do?
Symptom: On Windows, driving Hermes through an ACP client and asking it to write a file hung — the ACP session never progressed past the first tool path that needs a local shell.
Cause: That path runs
tools/environments/local._bash_starts, which probed Git Bash withsubprocess.run(..., timeout=15). After timeout,run()'s post-kill cleanup does an unboundedcommunicate(). If a suspended MSYS child (true/cat) still holds the captured pipe handles, the reader-thread join never returns, so the ACP write-file / tool turn looks wedged forever.Same deadlock class as the shared git-probe fix (#68997). This PR extracts
bounded_captured_run(Popen + tree-kill + bounded 1s drain,stdin=DEVNULL) and routes_bash_startsthrough it.bounded_git_probebecomes a thin fail-open wrapper on the same helper.Related Issue
Fixes #73403
Type of Change
Changes Made
hermes_cli/_subprocess_compat.py— addbounded_captured_run; refactorbounded_git_probeonto it; generalize process-tree kill helpertools/environments/local.py—_bash_starts()usesbounded_captured_runinstead ofsubprocess.run(timeout=...)tests/test_windows_subprocess_no_window_flags.py— spawn contract + Windows tree-kill coverage forbounded_captured_runtests/tools/test_find_shell.py—_bash_startsuses bounded helper; timeout fails open; retained-stdin regression (POSIX)How to Test
scripts/run_tests.sh tests/tools/test_find_shell.py tests/test_windows_subprocess_no_window_flags.py -q(44 passed)Popen/bounded_captured_runand assert timeout path tree-kills + returnsreturncode=-1without hangingChecklist
Code
scripts/run_tests.sh tests/tools/test_find_shell.py tests/test_windows_subprocess_no_window_flags.py(44 passed, 0 failed); reproduced hang via ACP client write-file before the fixDocumentation & Housekeeping
cli-config.yaml.example- N/ACONTRIBUTING.md/AGENTS.md- N/AScreenshots / Logs
N/A — hang is absence of progress after ACP asks Hermes to write a file; covered by bounded cleanup unit tests.