Repository navigation
fix(acp_adapter): windows bash hang - #69083
fangliquanflq wants to merge 8 commits into
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.
|
Independent reproduction with before/after measurements — hermes-agent 0.20.5, SymptomEvery Three things made it clear the block is upstream of
That narrows it to Smoking gunAn orphaned probe process, parent already dead, still alive 10+ minutes later: Minimal repro (no Hermes needed)
import subprocess, time
BASH = r"C:\Program Files\Git\bin\bash.exe"
t = time.time()
try:
subprocess.run([BASH, "--noprofile", "--norc", "-c", "sleep 60"],
capture_output=True, text=True, timeout=5,
creationflags=0x08000000)
except subprocess.TimeoutExpired:
print("%.2fs" % (time.time() - t)) # -> 60.12s
Redirecting to a file instead of a pipe returns in 5.02 s, which matches Confirming this PR's approachApplied
Would be good to see this land. The failure mode leaves no log line and no |
|
Thanks for the independent Windows 11 reproduction and measurements. The findings are consistent with this PR's root cause: the Git-for-Windows launcher can exit while an MSYS descendant retains the captured pipe handles, so No additional code change is needed for the notified feedback:
Your 60.12s-to-16.54s timeout measurement and zero-survivor result provide useful independent confirmation of the exact behavior this change targets. I did not rerun tests because this comment required no code change; the evidence above refers to the committed head and its recorded CI results. |
…3403) _bash_starts used subprocess.run(timeout=15). On Windows run()'s post-timeout cleanup is an unbounded communicate(): the probe's MSYS children (true/cat) outlive the killed bash holding the captured pipe write ends, so the reader join never returned and the ACP host wedged for minutes at the first terminal tool (faulthandler dumps in the thread sit in _bash_starts → _communicate). Route the probe through hermes_cli._subprocess_compat.bounded_probe_run — own Popen (stdin=DEVNULL, hidden window), communicate(timeout), process-tree kill and a 1 s bounded drain — so a probe that cannot finish fails fast and _find_bash falls through to its last-resort candidate. A timeout is recorded as the probe detail so the ASLR diagnostics still see it. Supersedes #69083 (@fangliquanflq), whose bounded_captured_run design landed on main as bounded_probe_run for the git probes (#68997); this is the surviving _bash_starts call-site conversion from that PR. Co-authored-by: fangliquanflq <[email protected]>
|
Thanks @fangliquanflq — this landed on |
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.