mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-09 03:11:58 +00:00
fix(entry-points): guard hermes_bootstrap import so partial updates don't brick hermes (#22091)
teknium1 hit ModuleNotFoundError: No module named 'hermes_bootstrap' after a code update, on both his Windows machine AND his Linux workstation. The failure mode is real and affects every user who updates hermes by any path OTHER than a fully-successful ``hermes update``. ## What happens hermes_bootstrap.py is a top-level module registered via pyproject.toml's ``py-modules`` list (added by Brooklyn's Windows UTF-8 stdio work). It must be registered in the venv's editable-install .pth file before Python can find it as a bare ``import hermes_bootstrap``. ``hermes update`` handles this correctly: (1) git reset --hard, (2) clear __pycache__, (3) uv pip install -e . (re-registers the package including the new py-modules list), (4) restart. BUT if any step AFTER (1) fails — network blip during pip install, PEP 668 on a system Python, venv locked, uv not in PATH, a crash mid-update — the user is left with new code that references hermes_bootstrap and a venv that doesn't know about it. Every hermes invocation after that crashes with ModuleNotFoundError, including ``hermes update`` itself. No recovery path without manual `uv pip install -e .`. Also affects users who ``git pull`` the repo directly without running hermes update — relatively common for developers. ## Fix Wrap ``import hermes_bootstrap`` in a try/except ModuleNotFoundError across all 6 entry points (hermes_cli/main, run_agent, gateway/run, acp_adapter/entry, cli, batch_runner). On Windows, missing bootstrap means the UTF-8 stdio setup doesn't run — degraded behavior (Unicode chars may fail to print) but NOT a crash. POSIX is unaffected either way since the bootstrap is a no-op there. Once hermes is running again, the user can ``hermes update`` to fully recover. ## Test update tests/test_hermes_bootstrap.py::test_entry_point_imports_bootstrap scans for the first top-level import in each entry point and asserts it is hermes_bootstrap. Extended the check to accept a Try block whose body is a lone Import of hermes_bootstrap — that's the recovery-friendly form we just introduced. Verified behavior by ``mv hermes_bootstrap.py hermes_bootstrap.py.bak`` and confirming ``python -c "import hermes_cli.main"`` succeeds. 82/82 tests pass (hermes_bootstrap + windows-native + windows-compat).
This commit is contained in:
parent
3299be6bdb
commit
26bac67ef9
7 changed files with 73 additions and 8 deletions
|
|
@ -15,7 +15,14 @@ Usage::
|
|||
|
||||
# IMPORTANT: hermes_bootstrap must be the very first import — UTF-8 stdio
|
||||
# on Windows. No-op on POSIX. See hermes_bootstrap.py for full rationale.
|
||||
import hermes_bootstrap # noqa: F401
|
||||
try:
|
||||
import hermes_bootstrap # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
# Graceful fallback when hermes_bootstrap isn't registered in the venv
|
||||
# yet — happens during partial ``hermes update`` where git-reset landed
|
||||
# new code but ``uv pip install -e .`` didn't finish. Missing bootstrap
|
||||
# means UTF-8 stdio setup is skipped on Windows; POSIX is unaffected.
|
||||
pass
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
|
|
|||
|
|
@ -22,7 +22,14 @@ Usage:
|
|||
|
||||
# IMPORTANT: hermes_bootstrap must be the very first import — UTF-8 stdio
|
||||
# on Windows. No-op on POSIX. See hermes_bootstrap.py for full rationale.
|
||||
import hermes_bootstrap # noqa: F401
|
||||
try:
|
||||
import hermes_bootstrap # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
# Graceful fallback when hermes_bootstrap isn't registered in the venv
|
||||
# yet — happens during partial ``hermes update`` where git-reset landed
|
||||
# new code but ``uv pip install -e .`` didn't finish. Missing bootstrap
|
||||
# means UTF-8 stdio setup is skipped on Windows; POSIX is unaffected.
|
||||
pass
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
|
|
|||
9
cli.py
9
cli.py
|
|
@ -14,7 +14,14 @@ Usage:
|
|||
|
||||
# IMPORTANT: hermes_bootstrap must be the very first import — UTF-8 stdio
|
||||
# on Windows. No-op on POSIX. See hermes_bootstrap.py for full rationale.
|
||||
import hermes_bootstrap # noqa: F401
|
||||
try:
|
||||
import hermes_bootstrap # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
# Graceful fallback when hermes_bootstrap isn't registered in the venv
|
||||
# yet — happens during partial ``hermes update`` where git-reset landed
|
||||
# new code but ``uv pip install -e .`` didn't finish. Missing bootstrap
|
||||
# means UTF-8 stdio setup is skipped on Windows; POSIX is unaffected.
|
||||
pass
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -15,7 +15,14 @@ Usage:
|
|||
|
||||
# IMPORTANT: hermes_bootstrap must be the very first import — UTF-8 stdio
|
||||
# on Windows. No-op on POSIX. See hermes_bootstrap.py for full rationale.
|
||||
import hermes_bootstrap # noqa: F401
|
||||
try:
|
||||
import hermes_bootstrap # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
# Graceful fallback when hermes_bootstrap isn't registered in the venv
|
||||
# yet — happens during partial ``hermes update`` where git-reset landed
|
||||
# new code but ``uv pip install -e .`` didn't finish. Missing bootstrap
|
||||
# means UTF-8 stdio setup is skipped on Windows; POSIX is unaffected.
|
||||
pass
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
|
|
|
|||
|
|
@ -46,7 +46,20 @@ Usage:
|
|||
# IMPORTANT: hermes_bootstrap must be the very first import — it sets up
|
||||
# UTF-8 stdio on Windows so print()/subprocess children don't hit
|
||||
# UnicodeEncodeError with non-ASCII characters. No-op on POSIX.
|
||||
import hermes_bootstrap # noqa: F401
|
||||
#
|
||||
# Guarded against ModuleNotFoundError because ``hermes_bootstrap`` is a
|
||||
# top-level module registered via pyproject.toml's ``py-modules`` list.
|
||||
# When the user upgrades code via ``git pull`` (or ``hermes update``
|
||||
# crashes between ``git reset --hard`` and ``uv pip install -e .``), the
|
||||
# new code references ``hermes_bootstrap`` but the editable install's
|
||||
# ``.pth`` file still points at the old set of top-level modules. Without
|
||||
# this guard, hermes crashes on import and the user can't run
|
||||
# ``hermes update`` to recover. Missing the bootstrap means UTF-8 stdio
|
||||
# setup is skipped on Windows — degraded, not broken. POSIX is unaffected.
|
||||
try:
|
||||
import hermes_bootstrap # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
|
|
|||
|
|
@ -22,7 +22,14 @@ Usage:
|
|||
|
||||
# IMPORTANT: hermes_bootstrap must be the very first import — UTF-8 stdio
|
||||
# on Windows. No-op on POSIX. See hermes_bootstrap.py for full rationale.
|
||||
import hermes_bootstrap # noqa: F401
|
||||
try:
|
||||
import hermes_bootstrap # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
# Graceful fallback when hermes_bootstrap isn't registered in the venv
|
||||
# yet — happens during partial ``hermes update`` where git-reset landed
|
||||
# new code but ``uv pip install -e .`` didn't finish. Missing bootstrap
|
||||
# means UTF-8 stdio setup is skipped on Windows; POSIX is unaffected.
|
||||
pass
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
|
|
|
|||
|
|
@ -257,6 +257,15 @@ class TestEntryPointsImportBootstrap:
|
|||
We're lenient about the docstring (can be arbitrarily long) and
|
||||
about comment lines — just need to verify the first import
|
||||
statement is the bootstrap.
|
||||
|
||||
Also lenient about a try/except wrapper around the import: entry
|
||||
points may guard the import against ``ModuleNotFoundError`` so a
|
||||
half-finished ``hermes update`` (git-reset landed new code but
|
||||
``uv pip install -e .`` didn't finish re-registering
|
||||
``hermes_bootstrap`` as a top-level module) leaves hermes
|
||||
recoverable instead of crashing on every invocation. When the
|
||||
first top-level node is such a guarded-import block, we peek
|
||||
inside it to verify bootstrap is the imported module.
|
||||
"""
|
||||
# Resolve relative to the hermes-agent repo root. Tests live
|
||||
# at tests/test_hermes_bootstrap.py, so go up one dir.
|
||||
|
|
@ -269,8 +278,7 @@ class TestEntryPointsImportBootstrap:
|
|||
source = full_path.read_text(encoding="utf-8")
|
||||
|
||||
# Find the first non-comment, non-blank line that starts with
|
||||
# 'import ' or 'from '. It must be 'import hermes_bootstrap'.
|
||||
import tokenize
|
||||
# 'import ' or 'from ', or a Try block whose body is the import.
|
||||
import ast
|
||||
tree = ast.parse(source)
|
||||
|
||||
|
|
@ -279,6 +287,15 @@ class TestEntryPointsImportBootstrap:
|
|||
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
||||
first_import_node = node
|
||||
break
|
||||
# Accept a guarded-import Try block where the body is a lone
|
||||
# Import node — this is the recovery-friendly form that lets
|
||||
# hermes start even when hermes_bootstrap hasn't been
|
||||
# re-registered in the venv yet.
|
||||
if isinstance(node, ast.Try) and len(node.body) == 1 and isinstance(
|
||||
node.body[0], (ast.Import, ast.ImportFrom)
|
||||
):
|
||||
first_import_node = node.body[0]
|
||||
break
|
||||
|
||||
assert first_import_node is not None, (
|
||||
f"{path}: no top-level imports found at all"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue