Skip to content

fix(gateway): avoid Windows uv pythonw launcher console - #41028

Closed
ashu1800 wants to merge 1 commit into
NousResearch:mainfrom
ashu1800:fix/windows-gateway-pythonw-script
Closed

fix(gateway): avoid Windows uv pythonw launcher console#41028
ashu1800 wants to merge 1 commit into
NousResearch:mainfrom
ashu1800:fix/windows-gateway-pythonw-script

Conversation

@ashu1800

@ashu1800 ashu1800 commented Jun 7, 2026

Copy link
Copy Markdown

Summary

Fix the Windows Gateway Scheduled Task wrapper so uv-managed installs do not leave a persistent foreground console/cmd window open.

The generated Hermes_Gateway.cmd now reuses the same detached interpreter resolver as direct Windows starts:

  • detects uv-created venv launchers via pyvenv.cfg
  • uses the base pythonw.exe instead of venv\Scripts\pythonw.exe when the venv launcher is a uv shim
  • preserves imports by setting PYTHONPATH to include the source checkout and venv Lib\site-packages
  • keeps service-managed starts free of start "" and --replace

Why this matters

On Windows uv installs, venv\Scripts\pythonw.exe can behave like a uv shim that starts the base console-subsystem python.exe. When the Gateway is launched from the Scheduled Task through that shim, users can get a persistent blank console/cmd window.

That is not just cosmetic: the window can be the Gateway's lifetime owner. Closing it can terminate the Gateway, so users are left with a bad choice:

  • leave a random black cmd window open forever, or
  • close it and lose the messaging gateway.

Gateway service/start/restart paths should be background/no-window by default; only explicit hermes gateway run should be foreground/debug style.

Related reports / prior art

This addresses the same root cause reported in:

This PR is intentionally focused on the Scheduled Task .cmd generation path and adds a regression test that verifies uv venvs use the base pythonw.exe plus PYTHONPATH.

Test plan

Ran on Windows:

PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run --with pytest python -m pytest tests/hermes_cli/test_gateway_windows.py -q -o addopts=''
35 passed in 5.67s

Also verified the generated local task script uses base pythonw.exe, includes VIRTUAL_ENV/PYTHONPATH, and contains no start "" or --replace for service-managed starts.

@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 labels Jun 7, 2026
@hqy2435662352

Copy link
Copy Markdown

Hi @ashu180 — your PR is still marked CONFLICTING 17 days after you opened it, and I had a near-duplicate open as #49615 (now closed as superseded). I rebased your commit onto current main (6e88f7b6) locally and verified pytest tests/hermes_cli/test_gateway_windows.py is 39/39 green with this patch applied. Offering it here as a salvage patch — please feel free to cherry-pick onto your branch and push, or git am it directly. Author identity is preserved (From: ashu180 <18011166553@163.com>).

Conflict resolution summary:

Two files conflicted during git rebase origin/main:

  1. hermes_cli/gateway_windows.py — two regions in _build_gateway_cmd_script:

    • Region A (lines 371–378, comment block above lines.append(f'set "VIRTUAL_ENV=...")): kept main's more detailed VIRTUAL_ENV comment; the duplicated _resolve_detached_python call line was simplified.
    • Region B (PYTHONPATH block): kept your version. main's set "PYTHONPATH={";".join([..., "%PYTHONPATH%"])} writes a literal %PYTHONPATH% into the PYTHONPATH string when PYTHONPATH is not yet defined in the parent env — cmd.exe does not expand %VAR% inside an already-quoted set value, so the gateway launches with a malformed PYTHONPATH. Your if defined PYTHONPATH (...) else (...) form is the correct cmd.exe idiom and is what the generated wrapper should use.
  2. tests/hermes_cli/test_gateway_windows.py — three regions in test_gateway_cmd_script_uses_uv_safe_base_pythonw (renamed to test_gateway_cmd_script_uses_base_pythonw_for_uv_venv_launcher in your version):

    • Kept your version throughout. The function signature change (_build_gateway_cmd_script(..., project_root=str(project))) is required because your fix added a project_root: str | None = None keyword-only parameter to the function. Tests that pass project_root= explicitly are tighter contracts than tests that rely on _resolve_detached_python to guess it.
    • Test name normalization: uses_uv_safe_base_pythonwuses_base_pythonw_for_uv_venv_launcher (matches the function being tested).
    • pyvenv.cfg simplified from home = ... implementation = CPython uv = 0.11.14 version_info = 3.11.15 to home = ... uv = true (smaller fixture; both shapes trigger the uv-detect path).

Final diff stat after rebase: +33/-16 across 2 files (vs. your original +59/-6 — the smaller delta reflects the fact that _resolve_detached_python was already on main post-#45610, so the helper duplication went away).

Verified on main (commit 6e88f7b6):

$ PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest tests/hermes_cli/test_gateway_windows.py -q -o addopts=''
.......................................                                  [100%]
39 passed in 5.30s

Note that #45610 (fix(windows): harden gateway scheduled task, merged 2026-06-23) already landed the start "" /B + >> gateway-stdio.log 2>&1 portion of this fix on main — your PR was the earlier one for the uv-launcher root cause and remains the canonical path for that fix. The Fixes #38387 keyword on this PR will continue to close that issue once merged.

Patch (apply with git am from a clean branch off main):

From 1128d65bb9eb24d5515e7f67635ec4e2ba18d191 Mon Sep 17 00:00:00 2001
From: ashu180 <18011166553@163.com>
Date: Sun, 7 Jun 2026 13:38:22 +0800
Subject: [PATCH] fix(gateway): avoid Windows uv pythonw launcher console

---
 hermes_cli/gateway_windows.py            | 25 ++++++++++++++++++++----
 tests/hermes_cli/test_gateway_windows.py | 24 +++++++++++------------
 2 files changed, 33 insertions(+), 16 deletions(-)

diff --git a/hermes_cli/gateway_windows.py b/hermes_cli/gateway_windows.py
index 994ab6e1c..72bc98e3e 100644
--- a/hermes_cli/gateway_windows.py
+++ b/hermes_cli/gateway_windows.py
@@ -344,15 +344,21 @@ def _build_gateway_cmd_script(
     working_dir: str,
     hermes_home: str,
     profile_arg: str,
+    project_root: str | None = None,
 ) -> str:
     """Build the ``gateway.cmd`` wrapper content (CRLF-terminated).
 
     The script:
       - cd's into a stable working directory
       - exports HERMES_HOME, PYTHONIOENCODING, VIRTUAL_ENV
-      - invokes ``pythonw -m hermes_cli.main [--profile X] gateway run``
+      - invokes a no-console ``pythonw -m hermes_cli.main [--profile X] gateway run``
         directly so the wrapper cmd.exe exits without a visible gateway console
 
+    uv-created venv launchers need special care: ``venv\\Scripts\\pythonw.exe``
+    can respawn the base interpreter as console ``python.exe``.  Reuse the same
+    resolver as direct detached starts so Scheduled Task launches use the base
+    ``pythonw.exe`` plus PYTHONPATH entries for the repo and venv site-packages.
+
     We intentionally do NOT inline PATH overrides here — cmd.exe inherits
     the per-user PATH the Scheduled Task was created with, and forcibly
     rewriting PATH tends to break Homebrew/nvm-style installations.
@@ -366,8 +372,13 @@ def _build_gateway_cmd_script(
     # VIRTUAL_ENV lets the gateway's own python detection find the venv
     # if someone imports hermes_constants-based logic during startup.
     lines.append(f'set "VIRTUAL_ENV={venv_dir}"')
-    pythonpath_entries = [str(Path(__file__).resolve().parent.parent), *extra_pythonpath]
-    lines.append(f'set "PYTHONPATH={";".join([*pythonpath_entries, "%PYTHONPATH%"])}"')
+    repo_root = project_root or str(Path(python_path).resolve().parent.parent.parent)
+    pythonpath_entries = [repo_root, *extra_pythonpath]
+    if pythonpath_entries:
+        prefix = ";".join(str(Path(entry)) for entry in pythonpath_entries if entry)
+        lines.append(
+            f'if defined PYTHONPATH (set "PYTHONPATH={prefix};%PYTHONPATH%") else (set "PYTHONPATH={prefix}")'
+        )
 
     prog_args = [pythonw_path, "-m", "hermes_cli.main"]
     if profile_arg:
@@ -497,7 +508,13 @@ def _write_task_script() -> Path:
     hermes_home = str(Path(get_hermes_home()).resolve())
     profile_arg = _profile_arg(hermes_home)
 
-    content = _build_gateway_cmd_script(python_path, working_dir, hermes_home, profile_arg)
+    content = _build_gateway_cmd_script(
+        python_path,
+        working_dir,
+        hermes_home,
+        profile_arg,
+        project_root=str(PROJECT_ROOT),
+    )
     script_path = get_task_script_path()
     tmp = script_path.with_suffix(".tmp")
     tmp.write_text(content, encoding="utf-8", newline="")
diff --git a/tests/hermes_cli/test_gateway_windows.py b/tests/hermes_cli/test_gateway_windows.py
index c327039fc..b8d7c8688 100644
--- a/tests/hermes_cli/test_gateway_windows.py
+++ b/tests/hermes_cli/test_gateway_windows.py
@@ -210,17 +210,14 @@ def test_gateway_cmd_script_uses_pythonw_without_replace_or_start_churn(monkeypa
     assert "exit /b 0" in content
 
 
-def test_gateway_cmd_script_uses_uv_safe_base_pythonw(monkeypatch, tmp_path):
-    """Scheduled Task wrapper should share the detached uv-venv workaround."""
+def test_gateway_cmd_script_uses_base_pythonw_for_uv_venv_launcher(tmp_path):
+    """Scheduled Task wrapper must match direct detached starts for uv venvs."""
     project = tmp_path / "project"
     scripts = project / "venv" / "Scripts"
     site_packages = project / "venv" / "Lib" / "site-packages"
-    hermes_home = tmp_path / "hermes-home"
-    base = tmp_path / "uv" / "python" / "cpython-3.11-windows-x86_64-none"
-    scripts.mkdir(parents=True)
-    site_packages.mkdir(parents=True)
-    hermes_home.mkdir()
-    base.mkdir(parents=True)
+    base = tmp_path / "uv-base"
+    for directory in (scripts, site_packages, base):
+        directory.mkdir(parents=True, exist_ok=True)
 
     venv_python = scripts / "python.exe"
     venv_pythonw = scripts / "pythonw.exe"
@@ -228,21 +225,24 @@ def test_gateway_cmd_script_uses_uv_safe_base_pythonw(monkeypatch, tmp_path):
     for exe in (venv_python, venv_pythonw, base_pythonw):
         exe.write_text("", encoding="utf-8")
     (project / "venv" / "pyvenv.cfg").write_text(
-        f"home = {base}\nimplementation = CPython\nuv = 0.11.14\nversion_info = 3.11.15\n",
+        f"home = {base}\nuv = true\n",
         encoding="utf-8",
     )
 
     content = gateway_windows._build_gateway_cmd_script(
         str(venv_python),
-        str(hermes_home),
-        str(hermes_home),
+        str(tmp_path / "hermes-home"),
+        str(tmp_path / "hermes-home"),
         "",
+        project_root=str(project),
     )
 
     assert str(base_pythonw) in content
+    assert str(venv_pythonw) not in content
     assert f'set "VIRTUAL_ENV={project / "venv"}"' in content
+    assert str(project) in content
     assert str(site_packages) in content
-    assert str(venv_pythonw) not in content
+    assert "PYTHONPATH=" in content
 
 
 def test_elevated_gateway_command_uses_pythonw_hidden_console(monkeypatch):
-- 
2.45.1.windows.1

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused Windows gateway investigation. This is an automated hermes-sweeper review; current main already provides the requested behavior.

  • Commit 433db17c0 (fix(windows): harden gateway scheduled task, shipped in v2026.7.1) changed the actual Scheduled Task action to wscript.exe + a generated VBS launcher (hermes_cli/gateway_windows.py:440-500, :575-629), so it no longer starts through the .cmd wrapper.
  • That launcher calls _resolve_detached_python (hermes_cli/gateway_windows.py:463), whose uv branch selects the base pythonw.exe and includes venv site-packages (:734-756).
  • The same guarantee is also covered for the generated CMD compatibility helper in tests/hermes_cli/test_gateway_windows.py:213-245.

The PR's behavioral goal is therefore already implemented on current main.

@teknium1 teknium1 closed this Jul 14, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

4 participants