Repository navigation
Conversation
…running
On Windows, `hermes update` aborts with exit code 2 whenever any other
process whose `.exe` is the venv `hermes.exe` or `hermes-gateway.exe`
shim is running — including the gateway itself, which the post-update
kill+restart block is going to reap anyway. The user is forced to
`taskkill /F /IM hermes.exe` every running instance and retry, even
though the update would have worked fine: the post-update sweep
matches gateway PIDs by command line ("hermes gateway" / "hermes.exe
gateway" / etc.), not by the venv shim path, and it correctly excludes
the calling process and its ancestor chain.
The fix classifies each concurrent-instance match by reading its
cmdline through psutil and comparing it to the same gateway-marker
substrings the post-update sweep uses. If every concurrent match is a
gateway, the update proceeds; the gateway PID is reaped by the
post-update block a few seconds later. Unrelated concurrent instances
(TUI shell, Hermes Desktop backend child, a plain REPL) still trigger
the existing abort message, since those aren't covered by the
post-update sweep.
Files changed:
- hermes_cli/main.py: add _classify_concurrent_instance() and
_filter_non_gateway_concurrent_instances(); gate the pre-update
concurrent check on the non-gateway subset only.
- tests/hermes_cli/test_update_concurrent_quarantine.py: add
classification, filter, and end-to-end cmd_update tests covering
both the gateway-only (skip-abort) and mixed (still-abort) paths.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds logic to relax the Windows pre-update concurrent-instance gate so hermes update can proceed when the only other running Hermes process is the gateway (which is already handled by the post-update kill+restart flow).
Changes:
- Introduces
_classify_concurrent_instanceand_filter_non_gateway_concurrent_instancesto identify and ignore gateway-only concurrent processes. - Updates the Windows concurrent-instance abort gate in
_cmd_update_implto consider only non-gateway processes. - Adds unit/integration-style tests covering classification, filtering, and the updated gate behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
hermes_cli/main.py |
Adds gateway-aware classification/filtering and updates the Windows concurrent-instance abort gate accordingly. |
tests/hermes_cli/test_update_concurrent_quarantine.py |
Adds tests for the new helper functions and for the relaxed/update gate behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| class _BrokenProc: | ||
| def cmdline(self): # noqa: D401 — psutil Process duck type | ||
| raise psutil.AccessDenied(pid=1234) | ||
|
|
||
| fake = types.SimpleNamespace(Process=lambda pid: _BrokenProc()) | ||
| import psutil as _real_psutil | ||
| with patch.dict(sys.modules, {"psutil": _real_psutil}): | ||
| result = _classify_concurrent_instance(1234) | ||
| assert result == "unknown" |
| gateway_markers = ( | ||
| "hermes_cli.main gateway", | ||
| "hermes_cli/main.py gateway", | ||
| "hermes gateway", | ||
| "hermes.exe gateway", | ||
| "hermes-gateway.exe", | ||
| "gateway/run.py", | ||
| ) |
|
Thanks for isolating the Windows update gate; the underlying issue remains on current main. Problems
Suggested changes
Automated hermes-sweeper review. |
… update (#37039) On Windows, the pre-update concurrent-instance gate aborted with exit 2 whenever ANY other process held the venv hermes.exe shim — including the gateway itself, which _pause_windows_gateways_for_update() stops moments later and the post-update restart phase brings back. Users with a running gateway were forced into a manual taskkill dance before every update. The gate now filters gateway runtimes out of the abort list and proceeds when nothing else is concurrent. Classification delegates to _is_pausable_gateway -> gateway.status.looks_like_gateway_command_line (the canonical shlex-tokenized, profile-selector-aware matcher shared by the Desktop preflight exemption and the venv-holder guard fallback), so the gate's exemption and the pause machinery cannot drift apart. Anything not positively identified as a gateway — REPLs, dashboard, Desktop backend children, gateway MANAGEMENT commands like 'gateway status', unreadable cmdlines — still aborts exactly as before, and the abort message now lists only the PIDs that are actually the user's problem. Surgical reapply of PR #37039 by @damadorPL onto current main (the gate moved from hermes_cli/main.py to hermes_cli/update_cmd.py in the main.py decomposition); his substring classifier was replaced with the canonical matcher, which also fixes the 'hermes gateway status' misclassification flagged in review. Co-authored-by: Hermes <hermes@nousresearch.com>
|
Merged via #95059 (rebase merge, commit 36f1423) — your commit cherry-picked onto current main with your authorship preserved in git log. The gate had moved from One change from your original: the classifier delegates to the canonical Verified live on windows-latest with real spawned processes (11/11), plus an A/B run proving the old refusal fires on merge-base. Thanks for the fix and the thorough root-cause writeup — the post-update-sweep symmetry argument was exactly right. |
…v mutation On Windows installs where the gateway runs as an SCM service (WinSW, NSSM, sc.exe create), the existing pause machinery kills the gateway process directly — and the service wrapper's failure ladder resurrects it within seconds, re-taking the venv file locks mid-update. The update then dies partway through dependency sync with access-denied errors. This extends _pause_windows_gateways_for_update() to detect when a gateway's process tree is owned by a running SCM service, and to stop the SERVICE through sc.exe instead of killing the child: - gateway/status.py: expose service-ownership discovery for gateway runtimes (find_windows_gateway_services maps validated gateway PIDs through process ancestry to running SCM service PIDs, with create-time identity checks against PID reuse). - hermes_cli/update_cmd.py: stop verified services via sc.exe before venv mutation and restart them afterward. Stops wait for a stable SCM 'stopped' state AND for the original descendant processes to exit (service 'Stopped' is not proof the child released its handles). Failure to prove ownership, stop a service, or restart it fails closed; rollback restores attempted services, and rollback failures are surfaced rather than swallowed. - Fail-closed throughout: unreadable identities, ambiguous ancestry, or a service that will not reach a stable state abort the update before any file mutation. Complements #37039 (gateway-only concurrent instances no longer abort): that fix lets the update proceed past the gate; this one makes the pause actually stick when the gateway is service-supervised. Note: tests/gateway/test_status.py::TestReadProcessCmdlinePsFallback:: test_ps_fallback_when_proc_unavailable fails on Windows on current main before this change as well (POSIX ps fallback asserted on a platform without it); all other touched suites pass (155 passed, 5 skipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…v mutation On Windows installs where the gateway runs as an SCM service (WinSW, NSSM, sc.exe create), the existing pause machinery kills the gateway process directly — and the service wrapper's failure ladder resurrects it within seconds, re-taking the venv file locks mid-update. The update then dies partway through dependency sync with access-denied errors. This extends _pause_windows_gateways_for_update() to detect when a gateway's process tree is owned by a running SCM service, and to stop the SERVICE through sc.exe instead of killing the child: - gateway/status.py: expose service-ownership discovery for gateway runtimes (find_windows_gateway_services maps validated gateway PIDs through process ancestry to running SCM service PIDs, with create-time identity checks against PID reuse). - hermes_cli/update_cmd.py: stop verified services via sc.exe before venv mutation and restart them afterward. Stops wait for a stable SCM 'stopped' state AND for the original descendant processes to exit (service 'Stopped' is not proof the child released its handles). Failure to prove ownership, stop a service, or restart it fails closed; rollback restores attempted services, and rollback failures are surfaced rather than swallowed. - Fail-closed throughout: unreadable identities, ambiguous ancestry, or a service that will not reach a stable state abort the update before any file mutation. Complements NousResearch#37039 (gateway-only concurrent instances no longer abort): that fix lets the update proceed past the gate; this one makes the pause actually stick when the gateway is service-supervised. Note: tests/gateway/test_status.py::TestReadProcessCmdlinePsFallback:: test_ps_fallback_when_proc_unavailable fails on Windows on current main before this change as well (POSIX ps fallback asserted on a platform without it); all other touched suites pass (155 passed, 5 skipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… update (NousResearch#37039) On Windows, the pre-update concurrent-instance gate aborted with exit 2 whenever ANY other process held the venv hermes.exe shim — including the gateway itself, which _pause_windows_gateways_for_update() stops moments later and the post-update restart phase brings back. Users with a running gateway were forced into a manual taskkill dance before every update. The gate now filters gateway runtimes out of the abort list and proceeds when nothing else is concurrent. Classification delegates to _is_pausable_gateway -> gateway.status.looks_like_gateway_command_line (the canonical shlex-tokenized, profile-selector-aware matcher shared by the Desktop preflight exemption and the venv-holder guard fallback), so the gate's exemption and the pause machinery cannot drift apart. Anything not positively identified as a gateway — REPLs, dashboard, Desktop backend children, gateway MANAGEMENT commands like 'gateway status', unreadable cmdlines — still aborts exactly as before, and the abort message now lists only the PIDs that are actually the user's problem. Surgical reapply of PR NousResearch#37039 by @damadorPL onto current main (the gate moved from hermes_cli/main.py to hermes_cli/update_cmd.py in the main.py decomposition); his substring classifier was replaced with the canonical matcher, which also fixes the 'hermes gateway status' misclassification flagged in review. Co-authored-by: Hermes <hermes@nousresearch.com>
…v mutation On Windows installs where the gateway runs as an SCM service (WinSW, NSSM, sc.exe create), the existing pause machinery kills the gateway process directly — and the service wrapper's failure ladder resurrects it within seconds, re-taking the venv file locks mid-update. The update then dies partway through dependency sync with access-denied errors. This extends _pause_windows_gateways_for_update() to detect when a gateway's process tree is owned by a running SCM service, and to stop the SERVICE through sc.exe instead of killing the child: - gateway/status.py: expose service-ownership discovery for gateway runtimes (find_windows_gateway_services maps validated gateway PIDs through process ancestry to running SCM service PIDs, with create-time identity checks against PID reuse). - hermes_cli/update_cmd.py: stop verified services via sc.exe before venv mutation and restart them afterward. Stops wait for a stable SCM 'stopped' state AND for the original descendant processes to exit (service 'Stopped' is not proof the child released its handles). Failure to prove ownership, stop a service, or restart it fails closed; rollback restores attempted services, and rollback failures are surfaced rather than swallowed. - Fail-closed throughout: unreadable identities, ambiguous ancestry, or a service that will not reach a stable state abort the update before any file mutation. Complements NousResearch#37039 (gateway-only concurrent instances no longer abort): that fix lets the update proceed past the gate; this one makes the pause actually stick when the gateway is service-supervised. Note: tests/gateway/test_status.py::TestReadProcessCmdlinePsFallback:: test_ps_fallback_when_proc_unavailable fails on Windows on current main before this change as well (POSIX ps fallback asserted on a platform without it); all other touched suites pass (155 passed, 5 skipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dards, update gate, llms.txt URL) MCP profile scope: scope_tool_arguments receives only real dict configs. Test doubles (SimpleNamespace without _config, MagicMock auto-attrs) were crashing or failing closed on a scope policy they never had; production dicts keep the fail-closed malformed-scope rule. Skills standards: rewrite 6 descriptions to <=60 chars with terminal period, fix humanizer tags placement, replace dangling writing-plans related_skills references. Suite now 1202/1202. Update gate: apply the NousResearch#37039 gateway exception to the second Windows concurrent-instance gate too, matching the first gate and the tests. llms.txt: SITE_BASE was repointed to a GitHub Pages URL that 404s (has_pages false); revert to the live serving host the skill documents. Test fixtures: model_tools no-listener test neutralizes first-party observability (stack_monitor defaults enabled, so post_tool_call has a listener); approval_interrupt assertions include the witness key added by 615d214.
… update (NousResearch#37039) On Windows, the pre-update concurrent-instance gate aborted with exit 2 whenever ANY other process held the venv hermes.exe shim — including the gateway itself, which _pause_windows_gateways_for_update() stops moments later and the post-update restart phase brings back. Users with a running gateway were forced into a manual taskkill dance before every update. The gate now filters gateway runtimes out of the abort list and proceeds when nothing else is concurrent. Classification delegates to _is_pausable_gateway -> gateway.status.looks_like_gateway_command_line (the canonical shlex-tokenized, profile-selector-aware matcher shared by the Desktop preflight exemption and the venv-holder guard fallback), so the gate's exemption and the pause machinery cannot drift apart. Anything not positively identified as a gateway — REPLs, dashboard, Desktop backend children, gateway MANAGEMENT commands like 'gateway status', unreadable cmdlines — still aborts exactly as before, and the abort message now lists only the PIDs that are actually the user's problem. Surgical reapply of PR NousResearch#37039 by @damadorPL onto current main (the gate moved from hermes_cli/main.py to hermes_cli/update_cmd.py in the main.py decomposition); his substring classifier was replaced with the canonical matcher, which also fixes the 'hermes gateway status' misclassification flagged in review. Co-authored-by: Hermes <hermes@nousresearch.com>
…v mutation On Windows installs where the gateway runs as an SCM service (WinSW, NSSM, sc.exe create), the existing pause machinery kills the gateway process directly — and the service wrapper's failure ladder resurrects it within seconds, re-taking the venv file locks mid-update. The update then dies partway through dependency sync with access-denied errors. This extends _pause_windows_gateways_for_update() to detect when a gateway's process tree is owned by a running SCM service, and to stop the SERVICE through sc.exe instead of killing the child: - gateway/status.py: expose service-ownership discovery for gateway runtimes (find_windows_gateway_services maps validated gateway PIDs through process ancestry to running SCM service PIDs, with create-time identity checks against PID reuse). - hermes_cli/update_cmd.py: stop verified services via sc.exe before venv mutation and restart them afterward. Stops wait for a stable SCM 'stopped' state AND for the original descendant processes to exit (service 'Stopped' is not proof the child released its handles). Failure to prove ownership, stop a service, or restart it fails closed; rollback restores attempted services, and rollback failures are surfaced rather than swallowed. - Fail-closed throughout: unreadable identities, ambiguous ancestry, or a service that will not reach a stable state abort the update before any file mutation. Complements NousResearch#37039 (gateway-only concurrent instances no longer abort): that fix lets the update proceed past the gate; this one makes the pause actually stick when the gateway is service-supervised. Note: tests/gateway/test_status.py::TestReadProcessCmdlinePsFallback:: test_ps_fallback_when_proc_unavailable fails on Windows on current main before this change as well (POSIX ps fallback asserted on a platform without it); all other touched suites pass (155 passed, 5 skipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…v mutation On Windows installs where the gateway runs as an SCM service (WinSW, NSSM, sc.exe create), the existing pause machinery kills the gateway process directly — and the service wrapper's failure ladder resurrects it within seconds, re-taking the venv file locks mid-update. The update then dies partway through dependency sync with access-denied errors. This extends _pause_windows_gateways_for_update() to detect when a gateway's process tree is owned by a running SCM service, and to stop the SERVICE through sc.exe instead of killing the child: - gateway/status.py: expose service-ownership discovery for gateway runtimes (find_windows_gateway_services maps validated gateway PIDs through process ancestry to running SCM service PIDs, with create-time identity checks against PID reuse). - hermes_cli/update_cmd.py: stop verified services via sc.exe before venv mutation and restart them afterward. Stops wait for a stable SCM 'stopped' state AND for the original descendant processes to exit (service 'Stopped' is not proof the child released its handles). Failure to prove ownership, stop a service, or restart it fails closed; rollback restores attempted services, and rollback failures are surfaced rather than swallowed. - Fail-closed throughout: unreadable identities, ambiguous ancestry, or a service that will not reach a stable state abort the update before any file mutation. Complements NousResearch#37039 (gateway-only concurrent instances no longer abort): that fix lets the update proceed past the gate; this one makes the pause actually stick when the gateway is service-supervised. Note: tests/gateway/test_status.py::TestReadProcessCmdlinePsFallback:: test_ps_fallback_when_proc_unavailable fails on Windows on current main before this change as well (POSIX ps fallback asserted on a platform without it); all other touched suites pass (155 passed, 5 skipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug Description
On Windows,
hermes updateaborts with exit code 2 and a wall of "Anotherhermes.exe is running" guidance whenever any other process whose
.exeisthe venv
hermes.exeorhermes-gateway.exeshim is running — includingthe gateway itself, which the post-update kill+restart block at the bottom
of
cmd_updateis going to reap anyway.This forces every Windows user with a running gateway into a manual
taskkill /F /IM hermes.exedance before everyhermes update, eventhough the post-update sweep already handles the gateway cleanly. The
user's only escape is
--force, which suppresses the message butdoesn't change the underlying redundancy.
Related: the issue #26670 the pre-update gate was added to address
(WinError 32 on the venv shim during
pip install -e .) is NOT agateway problem — the
_quarantine_running_hermes_exeretry /MoveFileEx-reboot-deferred fallback added at the same time handles
that case for the gateway too. So skipping the abort for gateway-only
matches doesn't reintroduce the original lock failure: the quarantine
step still renames the live shim before uv writes a replacement.
Root Cause
hermes_cli/main.py:9300calls_detect_concurrent_hermes_instances,which lists every process whose executable is one of the venv
shims (
hermes.exe,hermes-gateway.exe). It then prints the fulllist and
sys.exit(2)regardless of whether the matches are allgateways (which the post-update sweep will handle) or genuinely
unrelated processes (TUI shell, Hermes Desktop backend, a separate
REPL in another terminal).
The post-update kill+restart block at lines 9994-10500 matches
gateways by command line —
"hermes gateway","hermes.exe gateway","hermes-gateway.exe","hermes_cli.main gateway","gateway/run.py"— and correctly excludes the calling process andits ancestor chain. So the post-update path is already safe; only the
pre-update gate is over-broad.
Fix
Add a small classifier that mirrors the post-update sweep's gateway
patterns, and gate the pre-update abort on the non-gateway
subset of matches. If every concurrent match is a gateway, the
update proceeds and the post-update block reaps the gateway PID. If
anything else is in the list, the existing abort message and
--forceescape hatch remain unchanged.New helpers (in
hermes_cli/main.py):_classify_concurrent_instance(pid)— reads the pid's cmdline viapsutil and returns
"gateway","non-gateway", or"unknown"(cmdline unreadable / access denied / psutil unavailable). The
classifier mirrors the substring markers in
hermes_cli/gateway.py:_scan_gateway_pidsso the two scans matchthe same set of PIDs.
_filter_non_gateway_concurrent_instances(matches)— returns thesubset of concurrent-instance matches whose pid is not a
gateway.
The pre-update gate at line 9300 now calls the filter and aborts only
on the non-gateway subset:
Unknown-classified pids (psutil can't read them) bucket as
non-gateway — the gate errs on the side of the existing abort, so we
never silently kill a process we couldn't positively identify.
How to Verify
hermes gateway run(or start it via the servicemanager, on Linux/macOS).
hermes updatefrom a different terminal.running" listing the gateway PIDs, user has to
taskkillmanually.post-update block and respawned by the existing watcher logic
(
launch_detached_profile_gateway_restart).still aborts and lists only the non-gateway matches.
Test Plan
tests/hermes_cli/test_update_concurrent_quarantine.py:recognition across launcher types, non-gateway cmdline
recognition, psutil error → unknown, no-psutil → unknown,
mixed-list filter splitting, gateway-only → empty filter).
cmd_updateintegration tests: the user's bug(
test_cmd_update_skips_abort_when_only_concurrent_is_gateway)and the unchanged behaviour for non-gateway matches
(
test_cmd_update_still_aborts_on_non_gateway_concurrent).test_update_concurrent_quarantine.pystill pass.
(
test_update_autostash.py,test_update_stale_dashboard.py,test_cmd_update.py::test_update_on_fork_*) are pre-existingand confirmed against
mainbefore this change. They arecaused by Windows-specific wmic/ps quirks, not by this fix.
Risk Assessment
Low — the change is narrowly scoped:
difference is whether the pre-update gate fires for gateway-only
matches.
_scan_gateway_pidsinhermes_cli/gateway.py, so a pid classifiedas
"gateway"here is the same set of PIDs the post-update sweepwill reap. Symmetry by construction.
still trigger the existing abort with no behaviour change.
unknown(psutil can't read cmdline) defaults to non-gateway, sowe never silently kill a process we couldn't positively identify.