Skip to content

fix(windows): make updated gateway discoverable and use committed env - #123062

Closed
yazeeddbiani-tech wants to merge 1 commit into
NousResearch:mainfrom
yazeeddbiani-tech:fix/windows-gateway-update-20260925
Closed

yazeeddbiani-tech wants to merge 1 commit into
NousResearch:mainfrom
yazeeddbiani-tech:fix/windows-gateway-update-20260925

Conversation

@yazeeddbiani-tech

Copy link
Copy Markdown

Problem

On a Windows source install, hermes update completed the code and desktop builds but exited 1 while verifying the gateway relaunch. The gateway did start, but its post-update command came from runtime_command() as python -I -c ... gateway run --replace. The strict process matcher intentionally rejects inline -c commands, so the liveness poll could not see this gateway. A later status probe removed its PID file while the runtime lock was still active, causing subsequent updates to fail before fetching with active gateway lock has no PID metadata.

The relaunched Python 3.14 gateway also loaded pydantic_core from the old in-tree Python 3.11 venv, causing the hosted-room worker to fail at import time.

Change

  • On Windows, build the post-update profile relaunch as python -m hermes_cli.main ... gateway run --replace. The existing restart watcher supplies PYTHONPATH, VIRTUAL_ENV, and HERMES_HOME. The strict process matcher already recognizes this command; no identity checks are loosened. Other platforms retain their existing launcher.
  • Prefer PM's committed environment for detached Windows gateway imports, falling back to the legacy venv only when no committed environment exists.

Verification

  • Both new regression test files were red against the original code, then passed after the fix: 3 tests.
  • Existing gateway command-line matcher, Windows relaunch verification, and update restart fallback suites passed: 90 tests.
  • scripts/check-windows-footguns.py --diff main passed on all four changed files.
  • A live Windows install recovered after manually starting its Scheduled Task; this PR's patched update flow has not yet been exercised end-to-end on a second update.

Related work: #115495 addresses Scheduled Task fallback after a Job Object teardown. #122144 and #106491 address venv interpreter rebinding. This change addresses the inline-source discovery mismatch and the committed-versus-legacy environment choice observed in the live failure.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery platform/windows area/install-update Installer, updater, packaging, wheels, doctor sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Sep 25, 2026
@denohk

denohk commented Sep 26, 2026

Copy link
Copy Markdown

Independent live reproduction on a Windows source install (git install, store Python 3.14), plus one gap this PR does not cover.

The symptom, reproduced: hermes update aborted on a healthy, serving gateway with

RuntimeError: Could not map Windows gateway PIDs to profiles:
  Could not inspect gateway PID for profile default
  <- RuntimeError: active gateway lock has no PID metadata

The live gateway at the time had argv [<store>\python.exe, -I, -c, "<bootstrap>", gateway, run, --replace] — i.e. exactly the runtime_command() shape you describe. hermes gateway status still reported it as running (via the runtime-status fallback), which is what made the failure look arbitrary.

Confirms your diagnosis end to end: the post-update relaunch comes from hermes_cli/gateway.py::_gateway_run_args_for_profile → runtime_command(PROJECT_ROOT, [...]), so the relaunched gateway is inline--c and the strict matcher cannot see it. After your change that path emits -m hermes_cli.main, which the existing matcher already recognises — agreed that this is the smaller-footprint fix for that producer.

Gap — two other producers still emit an inline--c gateway:

  1. hermes_cli/web_server_gateway.py::_spawn_hermes_action spawns runtime_command(PROJECT_ROOT, ["gateway", "restart"]) for the dashboard/desktop gateway controls, so a gateway restarted from there is invisible again (and then loses its gateway.pid, see fix(gateway): identify a live gateway from its lock when gateway.pid is gone #122188 / [Bug]: status cleanup can unlink a live gateway identity during startup #96582).
  2. The published source-install launcher …\.hermes\bin\hermes.cmd / hermes.exe is itself an inline -c shim — python.exe -I -c "import base64; exec(base64.b64decode('…'))" %* — a different inline shape from runtime_command's literal bootstrap. A gateway started through the launcher is invisible to the strict matcher on current main, and this PR's -m change does not touch it. Same class as fix(gateway): ABI-guard _ensure_windows_gateway_venv_imports against cross-minor venv adoption #123191's "gateway lost pydantic_core after update" thread on the same launcher.

So switching _gateway_run_args_for_profile fixes the updater's own restart (the loudest symptom) but not "a Windows gateway started by the install's launcher". If the intent is "no identity checks loosened", every Windows gateway producer needs the -m form: _gateway_run_args_for_profile, web_server_gateway._spawn_hermes_action, and the published launcher for gateway run.

Implementation detail, if identity is ever preferred over -m: the matcher consumes _read_process_cmdline(), which is " ".join(psutil.Process(pid).cmdline()) — the join merges the -c operand, so inline_source_flag_index() sees only the first word of the source and no string rule can recover the boundary. The argv LIST keeps it:

['<store>\\python.exe', '-I', '-c', "import os, sys, runpy; … import hermes_bootstrap; runpy.run_module('hermes_cli.main', run_name='__main__', alter_sys=True)", 'gateway', 'run', '--replace']

With the list in hand, a rule keyed on the first -c operand recognises the launcher bootstrap (import hermes_bootstrap + runpy.run_module('hermes_cli.main') and reads the trailing tokens as the process's own argv. Wrappers that merely carry a gateway command for a later spawn (the detached restart watcher in #107002) have neither marker in their first operand and stay rejected — I replayed tests/gateway/test_gateway_command_line_matcher.py's ACCEPT / REJECT / INLINE_SOURCE_REJECT / INTERPRETER_OPTION_ACCEPT lists against that rule, including the nested case python -I -c <watcher> … -c <bootstrap> gateway run --replace (must stay False), and every one holds.

Verification of the whole path on my install (with the equivalent local change in place): find_profile_gateway_processes(strict=True) → ProfileGatewayProcess(profile='default', pid=…, create_time=…); hermes update --plan → gateway [default] pid … — manual; full hermes update → → 1 gateway(s) ACKed socket pause → ✓ Paused gateway profile(s): default → 20 commits pulled → ✓ Restarting Windows gateway profile(s): default → Fleet version check: ✓ default (pid …) @ <sha> — up to date. That is the stage that previously aborted.

@mydosu

mydosu commented Sep 26, 2026

Copy link
Copy Markdown

Independent Windows reproduction at 9fc7f17906e supports both failures this PR targets: the Desktop update completed code/builds but exited 1 while an inline--c gateway was alive, and the relaunched Python 3.14 gateway loaded the old Python 3.11 pydantic_core package. After a local fix and gateway restart, gateway_state=running and the hosted-room import error did not recur; 101 targeted tests passed. I have not exercised a second full Desktop update.

Two review details from the live investigation:

  1. gateway.status._read_process_cmdline() joins psutil argv without quoting the -c source. A regression test using only subprocess.list2cmdline() can pass while the real process remains invisible. " ".join(runtime_command(root, ["gateway", "run", "--replace"])) is the input that exposed my first false green.
  2. For the PM path, hermes_bootstrap has already pinned the dependency generation in this process's sys.path. Re-reading committed_venv() inside _ensure_windows_gateway_venv_imports() and adding that tree can mix generations if PM publishes another generation between bootstrap and gateway startup. My local fix leaves the activated path alone when a committed PM environment exists, then retains the legacy fallback only for pre-PM installs.

The -m relaunch change fixes this updater producer. Other inline-bootstrap gateway producers remain covered by the more general identity work in #123331 / #123109. I would avoid a duplicate PR and coordinate these changes with those threads.

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

With the watcher's own overlay python -m hermes_cli.main gateway run --help reaches the CLI and both matchers accept the cmdline. Two problems remain, both on the gateway/run.py half.

1. committed_venv() can raise and the call site is unguarded — gateway/run.py:432, called at gateway/run.py:5899. _recorded_venv raises when facts.json names a generation that is gone or outside the install (pm/environments.py:190). Reproduced against a real install-state dir: facts.json → environments/gen-deadbeef (no pyvenv.cfg) gives RuntimeError: dependency environment is missing or outside this install. flush_before_selecting notes that on Windows "NTFS journals metadata, not file contents, so power loss right after a publish can still leave the selected tree with incomplete files" (pm/environments.py:140-144), so this is reachable. The two lines above the new call are wrapped in _best_effort; this one is not, and it runs inside start_gateway after the PID-file claim, so the gateway dies having already claimed the home. Base never called committed_venv here — a new fail-hard path.

2. The -m form drops the isolated-mode launch contract — hermes_cli/gateway.py:920. runtime_command is "safe to persist across dependency GC" and its bootstrap opens with -I plus os.environ.pop(...) for PYTHONPATH/PYTHONHOME/VIRTUAL_ENV (hermes_cli/_launchers.py:38-52); the Windows branch keeps only runtime_command(...)[0], the interpreter. Measured on the store interpreter, same env, cwd HERMES_HOME: old gives isolated=1, ignore_environment=1 with no user site-packages; new gives isolated=0, ignore_environment=0 with AppData\Roaming\Python\Python314\site-packages (plus win32, pythonwin) on sys.path. For a relaunch that must "run inside the venv this process is using", inheriting the user's per-user site-packages is backwards. Keep -m, but use [python, "-I", "-m", "hermes_cli.main", *args] plus the env scrub — inline_source_flag_index returns None for -I, so -m is still recognised.

Minor. Both cases in test_windows_update_relaunch_command.py:12,19 patch is_windows to True and nothing asserts the POSIX form: replacing the guard with if True: leaves the file green (verified) — the 123028 mutation gap. And test_gateway_committed_env.py never patches committed_venv to a raiser, so a forced raise passes (verified).

@ethernet8023

Copy link
Copy Markdown
Collaborator

Thanks for the detailed write-up and the live Windows repro, it pinned down the real mechanism.

Closing because current main no longer has either half of this problem:

  • Inline--c gateway invisible to the matcher: gateway.status.looks_like_gateway_command_line now accepts the Hermes inline bootstrap (fix(gateway): recognise the Windows launcher's inline-bootstrap gateway form, then fix(gateway): read every Hermes inline bootstrap's argv as its identity, on all OSes). I ran main's real runtime_command(root, ["gateway", "run", "--replace"]), joined it the way _read_process_cmdline does (" ".join(argv)), and the matcher returns True. So the updater's liveness poll can see that gateway without switching the relaunch to -m. That also keeps the -I isolated launch contract that the -m form drops.
  • Old in-tree venv overriding the committed PM env: _ensure_windows_gateway_venv_imports already returns early when committed_venv(project_root) exists ([Bug]: Windows gateway on the PM runtime prepends the pre-PM venv and crashes hosted_room_worker (No module named 'pydantic_core._pydantic_core') #122183), which is the same preference this PR adds.

Not verified on a live Windows install, only against the real producer and matcher on Linux. If the exit-1 after hermes update still reproduces on a current build, please open an issue with the gateway's argv list and we'll look at it again.

@alt-glitch alt-glitch added os/windows Native Windows behaviour or breakage: PowerShell install, Windows paths, cmd.exe, WSL-vs-native diff and removed platform/windows labels Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery os/windows Native Windows behaviour or breakage: PowerShell install, Windows paths, cmd.exe, WSL-vs-native diff P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants