fix(install): surface aborted gateway restart during hermes update (#78574) - #78590
fix(install): surface aborted gateway restart during hermes update (#78574)#78590PRATHAMESH75 wants to merge 2 commits into
Conversation
monerostar
left a comment
There was a problem hiding this comment.
Ubuntu 26.04 linux-5800x (kernel 7.0.0-28-generic) here.
Live check on PR head 901b4e8:
- pytest tests/hermes_cli/test_update_gateway_restart_aborted.py: 5 passed in 0.43s
- Helpers import cleanly via hermes_cli.main
- Exception path now sets gateway_fleet_restart_incomplete, prints hermes gateway restart, and sys.exit(1) — matches the existing partial-fleet-failure contract
This is a better fit for the reported log (earlier updates had drain lines; failing run had none and still said Update complete) than a pure WSL detection tweak alone. Swallowing the whole restart phase at debug-only is enough to leave a stale gateway against new source.
Residual gap (not blocking this PR): if supports_systemd_services() is false on WSL and the restart phase does not raise, you can still skip unit discovery without hitting this handler. Separate from what this PR fixes. I am good with landing this first for #78574.
Looks good from Linux.
|
suggesting changes
An empty post-failure PID set cannot distinguish “nothing was running before the phase” from “a running gateway was stopped and did not come back,” which is the exact failure contract this PR is intended to close. Please record pre-restart gateway state or mark when any drain/stop/restart side effect begins, and fail the update on an escaping exception whenever a pre-existing gateway was touched unless its replacement was positively verified. Add an Security evidence:
Signed: GPT-5.6-sol-xhigh in Codex |
901b4e8 to
62e15d5
Compare
|
Thanks @egilewski — you're right, the The gap, addressed as you described. The handler now records pre-restart gateway state: right before any stop/drain, On the requested
The helper keeps the One note for transparency: in the newly-covered "stopped and gone" case the recovery warning still uses the generic wording ("any gateway still running… restart manually"); the actionable advice ( |
|
suggesting changes The restart-safety check can still treat an inconclusive gateway-discovery result as proof that no gateway remains. A gateway that keeps serving pre-update code may therefore be missed when a restart does not complete, while the update is reported successful. Treat an inconclusive discovery as unknown and fail closed unless authoritative pre- and post-restart observations prove that no gateway remains; add a regression for that case. Security evidence:
Not checked:
Signed: GPT-5.6-luna-max in Codex |
|
Thanks for the careful re-read. I agree with the principle — inconclusive discovery must fail closed — and I want to be precise about where this PR already does that versus where the residual concern actually lives, because I think the remaining gap is out of scope for #78574 and belongs in a separate change. What this PR's handler already does.
The only path that reports success is Where the residual gap really is. The "inconclusive can surface as try:
from gateway.status import get_running_pid
_append_unique_pid(pids, get_running_pid(), _exclude)
except Exception:
pass
...
try:
include_restart_managers = not supports_systemd_services()
except Exception:
include_restart_managers = FalseA partial scan can therefore return Why I'm not folding that into this PR. Making discovery distinguish inconclusive from authoritatively-empty means changing Happy to open a follow-up that hardens |
monerostar
left a comment
There was a problem hiding this comment.
Native Win11 here (10.0.26200, Python 3.11.15, Hermes 0.20.0). Earlier monerostar note was Linux on 901b4e8. This is the new tip 768f0f966.
Main on this box has no _restart_phase_failure_is_incomplete and no test_update_gateway_restart_aborted.py. Import from the install tree raises ImportError.
PR worktree:
- pytest tests/hermes_cli/test_update_gateway_restart_aborted.py: 10 passed in 1.18s
- Live import of the helper from the PR tree, same 5 decision cases as the new class:
| surviving | pre_restart | incomplete? |
|---|---|---|
| [4321] | [4321] | True |
| None | [] | True |
| [] | [4321] | True |
| [] | None | True |
| [] | [] | False |
That last row is the only clean path. Empty-after-stop is no longer treated as success. Did not run a live hermes update on this daily driver (multi-profile gateways up).
Residual I am not claiming closed: if find_gateway_pids can return [] when the scan itself was inconclusive, that still looks like the clean row. The helper is honest about None vs empty. Maintainer note after this tip still applies.
Looks good for the empty-survivor fail-open that landed today.
The gateway auto-restart phase in `hermes update` was wrapped in a blanket `except Exception` that only logged at debug level. When the phase raised early — e.g. importing `hermes_cli.gateway` from the freshly pulled checkout inside a process that already loaded pre-update modules — every drain and restart line vanished from the update output, the update printed "Update complete!" and exited 0, and the still-running gateway kept serving pre-update modules against replaced source files. The next Telegram turn died with `ImportError: cannot import name 'is_trivial_prompt'`. The handler now probes for surviving gateway processes and, unless it can positively prove none are running, prints the cause plus a manual recovery command and marks the fleet restart incomplete — which exits nonzero and writes the gateway-mode exit-code marker, matching the existing failed-or-stale-unit path. Fixes NousResearch#78574
…ivor probe Review follow-up (NousResearch#78574): the aborted-restart handler only flagged the fleet stale when the post-failure survivor probe was None or non-empty. A positive empty probe was treated as proof-of-safety — but `[]` is only safe when nothing was running before the phase. If a gateway was discovered, stopped (SIGTERM/drain), and its replacement never came back, the probe is empty at exactly that unsafe moment and the update reported success — the fail-open contract this fix exists to close. Snapshot the pre-restart gateway PIDs before any stop/drain and route the handler decision through a pure _restart_phase_failure_is_incomplete() helper that fails closed on an empty survivor set whenever a gateway existed pre-restart (or the pre-state could not be read). Add decision-level regression tests covering the stopped-without-replacement gap, unknown pre-state, and the truly-no-gateway positive control.
768f0f9 to
b005b22
Compare
|
Merged via PR #86687 (consolidated update-honesty train). Both your commits — surfacing the aborted restart phase and the fail-closed empty-survivor decision helper — were cherry-picked onto current main with your authorship preserved in git log. Thanks! |
What does this PR do?
hermes updatecan finish with a clean✓ Update complete!and exit 0 while leaving a running gateway on pre-update modules — the next turn then dies withImportError: cannot import name 'is_trivial_prompt' from 'agent.memory_provider'.The cause is the blanket handler around the entire gateway auto-restart phase in
hermes_cli/update_cmd.py:If anything in that phase raises — most plausibly the
from hermes_cli.gateway import (...)at the top of the block, which pulls the freshly pulled module into a process that already loaded the pre-update ones — the whole phase is skipped. Every→ draining …/✓ Restarted hermes-gatewayline vanishes from the update log, and the failure is invisible at default log level. That matches the reported log exactly: earlier updates show drain/restart lines, the failing run shows none, and exit status is 0.Upstream already has the right machinery for the partial failure case (
failed_or_stale_units→_warn_incomplete_gateway_fleet_restart→gateway_fleet_restart_incomplete→ nonzero exit +.update_exit_codemarker). This PR routes the total failure case into the same contract instead of swallowing it.On exception the handler now:
hermes_cli.gatewayis itself one of the ways we get here);hermes gateway restart;gateway_fleet_restart_incomplete, so the update exits nonzero and — under--gateway— writes1to.update_exit_code, exactly like the existing failed-unit path.None(undeterminable) and a non-empty PID list are both treated as "assume stale"; only a positive empty result stays quiet, so a host with no gateway running does not start failing its updates.This is deliberately a reporting fix, not an attempt to guess why the phase died: the issue asks for "exit nonzero or print a clear warning and manual recovery command", and any root cause behind that blanket
exceptproduces the same silent-stale outcome today.Related Issue
Fixes #78574
Type of Change
Changes Made
hermes_cli/update_cmd.py_surviving_gateway_pids_after_failed_restart()— best-effort probe for gateways still running after the restart phase aborted; returnsNonewhen undeterminable so callers can distinguish "nothing running" from "cannot tell"._warn_gateway_restart_phase_aborted()— prints the cause, the surviving PIDs, and thehermes gateway restart/hermes gateway statusrecovery commands. Mirrors the wording/shape of the neighbouring_warn_incomplete_gateway_fleet_restart().except Exceptionnow calls both, setsgateway_fleet_restart_incomplete = True, and writes the gateway-mode.update_exit_codemarker.hermes_cli/main.py— re-export the two new helpers alongside the existing update-path names.tests/hermes_cli/test_update_gateway_restart_aborted.py— new regression tests.How to Test
Result:
2 files, 9 tests passed, 0 failed.Full update-path suite (22 files, includes every
tests/hermes_cli/test_update*.pyandtest_cmd_update*.py):Result:
22 files, 167 tests passed, 0 failed.The tests cover the invariants from the issue: the probe never raises when
hermes_cli.gatewayis broken, an empty probe result stays distinguishable from an undeterminable one, and the warning names both the underlying cause and the manual recovery command.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
Before (reported in #78574 — no drain/restart step, exit 0):
After, when the restart phase aborts:
…and the update process exits nonzero.