mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-10 03:22:05 +00:00
fix(windows): browser tool + spurious SIGINT from subprocess spawning
Three related Windows-only fixes that together make the browser toolset
actually usable on Windows. Symptom chain: user invokes browser_navigate
-> tool returns {"success": false, "error": "Daemon process exited
during startup with no error output"} and the CLI exits mid-turn with
the session summary.
Root cause (3 layers):
1. tools/browser_tool.py::_find_agent_browser() resolved
node_modules/.bin/agent-browser to the extensionless POSIX shell
shim via Path.exists(). On Windows, CreateProcessW cannot execute
that script (WinError 193 "not a valid Win32 application"). Fix:
delegate to shutil.which with path=node_modules/.bin so PATHEXT
picks up agent-browser.CMD on Windows and the extensionless shim
stays correct on POSIX.
2. Windows Terminal / Win32 delivers a spurious CTRL_C_EVENT to the
parent hermes.exe whenever a background thread spawns a .cmd
subprocess. Python 3.11's default SIGINT handler raises
KeyboardInterrupt in MainThread, which unwinds prompt_toolkit's
app.run() -> cli.py::run()'s finally block calls _run_cleanup()
-> _emergency_cleanup_all_sessions -> spawns a concurrent
_run_browser_command("close", ...) on the same session the agent
thread just opened. Two agent-browser processes race on the same
--session name, the daemon startup loses, and the tool returns
the "Daemon process exited during startup" error. Fix: install a
Windows-only SIGINT handler that absorbs the signal silently.
Real user Ctrl+C still routes through prompt_toolkit's own c-c
keybinding at the TUI layer, which is how Claude Code handles the
same quirk (driving cancellation via the TUI key handler, not
signals).
3. In tools/browser_tool.py, both Popen sites now pass
creationflags=CREATE_NO_WINDOW | STARTF_USESTDHANDLES with
close_fds=True on Windows. CREATE_NO_WINDOW suppresses the .cmd
console flash; STARTF_USESTDHANDLES + close_fds ensures the child
inherits only our three chosen handles (DEVNULL stdin, temp-file
stdout/stderr) and no leaked parent console handles that could
confuse agent-browser's native daemon spawn. Notably we do NOT
add CREATE_NEW_PROCESS_GROUP - on Python 3.11 Windows the flag
interacts badly with asyncio's ProactorEventLoop and makes things
worse.
Verified end-to-end on Windows 10 / Windows Terminal / PowerShell:
browser_navigate to https://example.com returns
{"success": true, "title": "Example Domain"} and the CLI stays alive
for follow-up tool calls and assistant turns.
Refs: earlier Windows quirks commits 1cebb3bad (Ctrl+Enter newline),
26f5af52a (environment hints), aefd1a37f (Playwright Chromium).
This commit is contained in:
parent
62b4ebb7db
commit
0ba1e12abc
2 changed files with 100 additions and 6 deletions
31
cli.py
31
cli.py
|
|
@ -678,6 +678,7 @@ def _run_cleanup():
|
|||
if _cleanup_done:
|
||||
return
|
||||
_cleanup_done = True
|
||||
|
||||
try:
|
||||
_cleanup_all_terminals()
|
||||
except Exception:
|
||||
|
|
@ -12347,6 +12348,36 @@ class HermesCLI:
|
|||
_signal.signal(_signal.SIGTERM, _signal_handler)
|
||||
if hasattr(_signal, 'SIGHUP'):
|
||||
_signal.signal(_signal.SIGHUP, _signal_handler)
|
||||
|
||||
# Windows: install a SIGINT handler that absorbs the signal
|
||||
# instead of letting Python's default handler raise
|
||||
# KeyboardInterrupt in MainThread. Windows Terminal / Win32
|
||||
# delivers spurious CTRL_C_EVENT to the hermes process when
|
||||
# child processes are spawned from background threads (agent
|
||||
# subprocess Popen path). The default Python SIGINT handler
|
||||
# would then unwind prompt_toolkit's app.run(), trigger
|
||||
# _run_cleanup mid-turn, and close browser sessions mid-open
|
||||
# — causing "Daemon process exited during startup" errors.
|
||||
#
|
||||
# The handler is a silent no-op. Real user Ctrl+C still works
|
||||
# because prompt_toolkit binds c-c at the TUI layer and never
|
||||
# reaches this OS-signal path. This matches how Claude Code
|
||||
# handles the same Windows quirk (cancellation is driven by
|
||||
# the TUI key handler, not by OS signals).
|
||||
#
|
||||
# POSIX: leave the default SIGINT handler alone. prompt_toolkit
|
||||
# installs its own handler there and it works as expected.
|
||||
if sys.platform == "win32":
|
||||
def _sigint_absorb(signum, frame):
|
||||
# Absorb silently. Do NOT call agent.interrupt() here:
|
||||
# Windows fires spurious CTRL_C_EVENT whenever a
|
||||
# background thread spawns a .cmd subprocess, and
|
||||
# interrupt() would inject a fake user message each
|
||||
# time. Real user Ctrl+C routes through prompt_toolkit's
|
||||
# own c-c key binding at the TUI layer (same pattern as
|
||||
# Claude Code's Windows handling).
|
||||
return
|
||||
_signal.signal(_signal.SIGINT, _sigint_absorb)
|
||||
except Exception:
|
||||
pass # Signal handlers may fail in restricted environments
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue