Skip to content

fix(probe): re-exec through a child on Windows so spaced interpreter paths work - #57

Merged
Adolanium merged 5 commits into
Adolanium:mainfrom
Iversondwx:fix/windows-reexec-space-in-path
Nov 5, 2026
Merged

Adolanium merged 5 commits into
Adolanium:mainfrom
Iversondwx:fix/windows-reexec-space-in-path

Conversation

@Iversondwx

Copy link
Copy Markdown

Problem

_use_gateway_python() hands the probe over to a backend interpreter with:

os.execv(executable, [executable, str(Path(__file__).absolute()), *sys.argv[1:]])

On Windows, CreateProcess splits an unquoted argv[0] at the first space. An interpreter installed under D:\Program Files\... is launched as D:\Program, and the rest of the path is then read as the script name:

D:\Program: can't open file 'D:\Program Files\...\python.exe'

The child exits 0 with an empty stdout, so Desktop's pinnedPage() runs JSON.parse('') and renders "probe returned non-JSON" with no quota windows at all.

It only fires when the launch path differs from the candidate list — for example when probe.py is started through a hermes-agent/.venv junction whose real location is installs/<id>/environments/<hash>/venv. Starting from the real path short-circuits at os.path.normcase(executable) == os.path.normcase(sys.executable) and never reaches os.execv, which is why running the probe from a shell on the real path looks fine.

Fix

On Windows, re-exec as a child (subprocess.run([executable, *argv])) and forward its exit status with os._exit. POSIX keeps os.execv.

Verification (Windows 11, Hermes installed under D:\Program Files\Hermes)

  • Before: python probe.py --gateway-runtime → empty stdout, exit 0, Desktop shows "probe returned non-JSON".
  • After: the same command prints the full provider JSON array (ollama / deepseek / openrouter / copilot / …), and the remaining probe flags plus the backend environment survive the handoff.
  • python -m py_compile probe.py passes.
  • Diff is 10 lines added, 1 removed, in probe.py only.

…paths work

`_use_gateway_python()` hands the probe over to a backend interpreter with
`os.execv(executable, [executable, <script>, *argv])`. On Windows, CreateProcess
splits an unquoted argv[0] at the first space, so an interpreter installed under
"Program Files" is launched as "D:\Program" and the rest is read as the script
name:

    D:\Program: can't open file 'D:\Program Files\...\python.exe'

The child exits 0 with an empty stdout, so Desktop's pinnedPage() runs
JSON.parse('') and shows "probe returned non-JSON" with no quota windows at all.
It only fires when the launch path differs from the candidate list - e.g. when
probe.py is started through a hermes-agent/.venv junction whose real location is
installs/<id>/environments/<hash>/venv - which is why command-line tests on the
real path look fine.

Windows now re-execs as a child (subprocess.run) and forwards its exit status;
POSIX keeps os.execv.

Verified on Windows 11 with Hermes under D:\Program Files\Hermes: before the
change --gateway-runtime printed nothing and exit 0; after it prints the full
provider JSON array, and the remaining probe flags/environment survive the
handoff.
@Iversondwx
Iversondwx force-pushed the fix/windows-reexec-space-in-path branch from 3c75db5 to da30e68 Compare November 5, 2026 19:00
sys.platform == "win32" matches the probe's other Windows checks and lets tests pin the platform without patching os.name, which pathlib and the venv path logic also read.
A spaced interpreter path must reach subprocess.run intact and the child's exit status must be forwarded. The existing os.execv tests now pin a POSIX platform so the suite gives the same result on Windows hosts.
Picks up the sys.platform check so build_catalog.py --check passes.
@Adolanium
Adolanium merged commit 2fd4a4a into Adolanium:main Nov 5, 2026
1 check passed
@Adolanium

Copy link
Copy Markdown
Owner

Merged, thank you @Iversondwx, and welcome!

Tracing the empty stdout back to the .venv junction made this easy to accept. It explains why the probe looked fine from a shell on the real path and still failed in Desktop. Moving to subprocess.run is the right fix too: os.execv on Windows doesn't quote its arguments, and subprocess quotes both the interpreter path and the script path.

I added a few commits on top of yours before merging:

  • The branch now checks sys.platform == "win32" instead of os.name == "nt". That matches the probe's other Windows checks. It also lets tests pin the platform without patching os.name, which pathlib and the venv path logic read too.
  • A new test in test_probe_runtime.py checks that a D:\Program Files\... interpreter reaches subprocess.run intact and that the child's exit status is forwarded. It fails against the old code for both the standalone and the catalog copy. The three existing os.execv tests now pin a POSIX platform. On a Windows host they had started calling the real os._exit, which ended the test run.
  • I regenerated catalog/desktop/probe.py after the platform change and added a thank-you line to the README.

All the workflow steps pass in CI and locally. I didn't have an install under Program Files to test the real handoff, so I'm relying on your Windows 11 run for that. If you can, please try main and confirm the cards still load.

Catalog installs are pinned to 0.2.27, so they get this once a new release is pinned upstream. Until then, you can copy probe.py from main.

One small thing: your commits are authored as Commander <commander@tianji.local>, so they won't show on your GitHub profile. If you commit with an email that's on your GitHub account next time, they'll link up.

@Iversondwx

Copy link
Copy Markdown
Author

Confirmed on main — thanks for the fast merge and for tightening the platform check.

Tested on Windows 11 with Hermes installed under D:\Program Files\Hermes (the real spaced-path case):

  • Copied probe.py from main (2fd4a4a) over both local copies — plugins/hermes-resetwatch/desktop/probe.py and desktop-plugins/hermes-resetwatch/probe.py — sha256 identical (84962f87…).
  • python probe.py --gateway-runtime now returns the full provider array from both locations: 5 providers, ollama → plan Pro with both 5h and Weekly windows populated. The cards load.
  • On the shipped 0.2.26 the same command exited 0 with empty stdout, so the handoff is what made the difference.

One data point for test_probe_runtime.py, in case it's useful: on this Windows host python -m unittest test_probe_runtime.py gives 14/15. test_windows_reexec_runs_a_child_so_spaced_interpreter_paths_survive passes. The only failure is test_vendor_wait_ends_before_the_gateway_shell_limit — expects anthropic, gets the resetwatch error snapshot. It reproduces against both probe.py and catalog/desktop/probe.py, so it looks unrelated to the re-exec change (probably a real-host timing difference rather than the mocked path CI takes).

Until a catalog release pins this, I'll keep those two copies on main; a small local watchdog only re-applies the old patch if hermes plugins update rolls the tree back to 0.2.26.

@Adolanium

Copy link
Copy Markdown
Owner

Thanks @Iversondwx. Checking both copies by hash and running the probe from both locations settles it. The Windows handoff works on a real Program Files install.

The test failure you saw was real, and it came from the missing httpx, not from timing. _collect_cli imports httpx before it starts any vendor. When that import fails, it returns the probe cannot import httpx snapshot, which is the resetwatch card the test got instead of anthropic. CI installs httpx before running the tests, so it never saw this, even though the README says those tests need no packages. I reproduced it by blocking httpx, and #58 gives the test a stub module, since the vendor fetcher is faked anyway. With that merged, test_probe_runtime.py passes with or without httpx.

The catalog release still has to be cut and pinned upstream before hermes plugins update brings this in, so keeping your two copies on main until then makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants