Repository navigation
Conversation
… gateway; prefer the committed PM environment On Windows installs where a pre-PM in-tree venv (built for a different CPython) is present, _ensure_windows_gateway_venv_imports() injected its site-packages into a process running on the PM-managed interpreter. The partial injection then broke every LATER import of a compiled package: the gateway's hosted_room_worker died 5x in rapid succession at startup with ModuleNotFoundError: No module named 'pydantic_core._pydantic_core' (NousResearch#122183, dups NousResearch#122556 NousResearch#122736 NousResearch#122400). - read pyvenv.cfg and never inject a venv whose version does not match the running interpreter (unknown layouts keep legacy behavior) - prefer the committed PM environment (pm.environments.committed_venv) over ambient VIRTUAL_ENV / the in-tree venv when one is committed - add tests/gateway/test_windows_venv_import_guard.py: red on upstream HEAD, green with the fix, skipped off-Windows, and hermetic against home_io_guard (addsitedir stubbed: the unit under test is candidate selection, not stdlib site processing)
|
🤖 Hermes Agent automated review: PR reviewed. Diff analyzed (196 lines changed). Check CI status and manual review recommended. |
SummaryReady to merge. Well-scoped fix for a real Windows gateway crash (pydantic_core ABI mismatch), with a comprehensive test suite. Positives
Suggestions
Questions
|
ehz0ah
left a comment
There was a problem hiding this comment.
Verdict: REQUEST_CHANGES. Two P1 blockers remain at this exact head. The production guard does not parse the format written by uv, and the new regression file is absent from the Windows CI selector. No native Windows validation is claimed.
| try: | ||
| for line in (venv_dir / "pyvenv.cfg").read_text(encoding="utf-8", errors="replace").splitlines(): | ||
| key, sep, value = line.partition("=") | ||
| if sep and key.strip().lower() == "version": |
There was a problem hiding this comment.
P1 — Parse the uv version_info key before accepting this environment. Hermes PM creates these environments with uv venv, and real uv pyvenv.cfg files use version_info = X.Y.Z. This parser accepts only version, so it returns None for the normal PM format. The check below treats None as an unknown layout and injects the environment anyway. A mismatched uv environment can therefore recreate the compiled-extension ABI crash this PR is intended to stop. Please use the shared parser or accept both keys, read with utf-8-sig, and add a regression that writes the real uv format.
|
|
||
| import gateway.run as gateway_run | ||
|
|
||
| pytestmark = pytest.mark.skipif( |
There was a problem hiding this comment.
P1 — Use pytest.mark.platforms("windows") so these tests run in CI. The native Windows jobs build their file list with scripts/ci/list_os_marked_tests.py, which selects only files containing platforms(...). This bare skipif file is omitted from both Windows jobs, while Linux and macOS skip all three tests. I confirmed the selector does not list this file at the exact PR head.
andrexibiza
left a comment
There was a problem hiding this comment.
Preferring the committed dependency environment over a stale in-tree venv addresses the reported Windows failure. The selection-error path currently undoes that protection, and the late import adjustment needs to preserve the process's already-activated generation.
Reviewed 6c4ae6da656079ba86a162263b07393740644999.
P1 — a broken committed selection is converted into permission to use legacy dependencies
The new except Exception branch turns every committed_venv() failure into pm_env=None, then considers VIRTUAL_ENV and the in-tree venv. But pm.environments._recorded_venv() deliberately distinguishes no record from an unreadable, malformed, missing-tree or out-of-install committed selection. Those latter states raise rather than silently selecting an older dependency set.
A malformed facts.json, a permission error, or a committed generation whose tree vanished therefore reaches the ambient fallback here. If that old venv has the same major/minor Python version, the new version guard accepts it. An error reading the canonical owner has become authority to load a different dependency generation—the exact precedence this PR is trying to prevent.
Allow the legacy path only when canonical selection explicitly reports that no PM/payload environment exists. Preserve selection failures as failures before site.addsitedir, sys.path or child environment mutation. Do not repair this by catching the same error under a narrower log message.
Add separate no-selection, malformed-record, unreadable-record, vanished-selected-tree and outside-install-selection cases. For every invalid committed selection, assert that no legacy site directory is added and neither VIRTUAL_ENV nor PYTHONPATH is rewritten. Retain the allowed legacy-only and valid-PM-precedence controls.
P2 — Python-version compatibility does not prove the active dependency generation
The helper runs after gateway module imports, then prepends the selected site's packages and rewrites the child environment. Equal Python major/minor versions can still describe two dependency trees with different package versions. Modules already imported from A stay in sys.modules while late imports can resolve under B.
This matters if the process activated A at boot and the install's committed selection changed to B before this helper reads it. The existing activate_dependencies contract explicitly makes activation a pre-import boot decision and says already-running processes are not switched after a dependency install. The new PM preference must not become a second, later activation decision.
Use the existing activation/environment identity for the running process, or perform the canonical selection before third-party imports. A same-version comparison remains useful as a rejection check; it is not a replacement for generation identity. Add a same-ABI A/B selection-change case with an early module from A and a late dependency, and assert that one process never composes the two generations. This is a source-level lifetime concern, not a claim that changing only the Python patch number is itself an ABI bug.
Existing review, tests and acceptance
ehz0ah's existing review separately identifies version_info parsing and Windows CI selection. Those are already reported and are not duplicated as new inline findings here. The current source still checks only version, and the test file still uses a bare Windows skipif. Coordinate those corrections with this selection/error fix rather than introducing another venv parser.
This audit re-read the complete two-file diff, full helper, canonical PM selection and activation code, test fixtures and discussion. I did not rerun the three tests, process real .pth files, exercise native Windows startup or perform independent 5×2×3 verification. The tests deliberately stub site.addsitedir; their candidate-selection results are not proof of real import compatibility or compiled-package realization.
Exact-head CI 36196076193, Docker 36196076020, and Nix 36196076146 are action_required. No every-commit-green or merge acceptance is claimed. I cannot authorize workflows. The earlier facade-size concern remains a repository acceptance item; this pass did not independently rerun that scanner.
Keep robbyslmt's implementation and the #122183/#122556/#122736/#122400 incident lineage. The needed correction belongs in the existing PM activation/selection seam, not another fallback owner in gateway/run.py. The PR is open, non-draft and reported mergeable at the unchanged head, with recorded base 6d70abc87134a6f1243b90512530e201f08b02c2.
|
|
||
| pm_env = committed_venv(project_root) | ||
| except Exception: | ||
| logger.debug("PM committed-venv lookup failed; falling back to legacy venv candidates", exc_info=True) |
There was a problem hiding this comment.
P1 — do not turn a rejected committed environment into legacy fallback. committed_venv() returns None for no selection but raises for unreadable/invalid facts or a missing/out-of-install generation. This catch collapses those errors into None, allowing same-version ambient/in-tree packages to be injected. Preserve the canonical refusal before path/env mutation; legacy fallback should require an explicit no-selection result. Test corrupted/unreadable facts and a vanished selected tree with a valid-looking legacy venv present.
| # Never inject site-packages built for a different CPython ABI: the native extensions | ||
| # cannot load, and the partial injection then breaks every LATER import of a compiled | ||
| # package (pydantic_core took down the hosted room worker 5x at startup, 2026-09-26). | ||
| # Unknown layouts (no readable pyvenv.cfg) keep the legacy inject behavior. |
There was a problem hiding this comment.
P2 — retain the boot-activated generation, not just its Python version. The helper runs after imports and later prepends site-packages. A booted process on generation A can see a newly committed same-ABI B here, retaining early modules from A and resolving later ones from B. pm.environments.activate_dependencies explicitly prohibits switching an already-running process after install. Bind this adjustment to the running activation or move selection before third-party imports; add a same-ABI A→B selection-change test. Patch-version differences alone are not the finding.
|
Thanks @robbyslmt. The gateway side lands through #123996. Your committed-generation preference is the core of it, and you're credited as co-author ( How the open review points were handled:
Closing in favour of #123996. I'll post the merge SHA here when it lands. |
What does this PR do?
Fixes a P1 Windows gateway startup crash (#122183): on installs where a pre-PM in-tree
venv/was built by a different CPython version than the PM-managed runtime,_ensure_windows_gateway_venv_imports()injected that venv'ssite-packagesinto the gateway process anyway. The partial injection then broke every later import of a compiled package — thehosted_room_workerdied 5× in rapid succession at every boot with:(the gateway core survives because its own imports finish before the injection;
tui_gateway.serverin the worker importspydantic_coreafter it).Two changes to
_ensure_windows_gateway_venv_imports():pyvenv.cfgand never inject a venv whose Python version differs from the running interpreter. An ABI mismatch there is the crash; unknown layouts (no readablepyvenv.cfg) keep legacy behavior so nothing that worked before stops working.pm.environments.committed_venv), it is the sole candidate, replacing ambientVIRTUAL_ENV/ the in-tree venv. The committed generation is what PM selected for this install; the pre-PM venv is a stale artifact that loses to it.Related Issue
Fixes #122183 (duplicates: #122556, #122736, #122400)
Type of Change
Changes Made
gateway/run.py—_ensure_windows_gateway_venv_imports(): version guard (_pyvenv_version()helper) + committed-PM-first candidate selection, withlogger.debugon every skipped/failed path. Legacy fallback preserved when no PM environment is committed.tests/gateway/test_windows_venv_import_guard.py— new: 3 tests covering (a) mismatched venv never injected, (b) matching venv still injected (legacy installs), (c) PM environment wins over a mismatched ambient venv. Red onmain, green with this patch, skipped off-Windows.How to Test
main(Windows, PM-managed runtime + an in-tree venv from an older Python): gateway boots, thenSupervised task hosted_room_worker died: ModuleNotFoundError("No module named 'pydantic_core._pydantic_core'")× 5 ingateway.log, thengiving up restarts.hermes gateway restart: worker stays up; log shows no pydantic_core errors.pytest tests/gateway/test_windows_venv_import_guard.py -q→ 3 passed (Windows; skipped elsewhere).Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass (targeted suites: this file +tests/hermes_cli/test_gateway_windows.pygreen; 4 unrelated pre-existing environmental failures on this machine are listed in the PR notes below)Documentation & Housekeeping
_ensure_windows_gateway_venv_importsearly-returns elsewhere); tests skip off-Windows; no POSIX behavior changeNotes for reviewers
home_io_guardrefuses file I/O under the real Hermes home, andsite.addsitedirstats everysys.pathentry — on default installs the interpreter's own stdlib lives under the home, so the tests stubaddsitedir(the unit under test is candidate selection, not stdlib site processing). This makes the tests hermetic on any layout.test_exec_schtasks_round_trips_non_ascii_task_argument_live(schtasks needs elevation),test_load_enabled_toolsets_*/test_load_cfg_raw_sees_replacement_with_pinned_mtime_and_size(toolset-catalog test-data drift + mtime race).