mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(update): refresh runtime modules before lazy backends
Refresh update-sensitive modules before lazy backend refresh so an in-place git update does not keep using pre-pull module objects when newly pulled code imports fresh helpers. Constraint: Issue #60242 reports post-update lazy backend refresh importing stale hermes_constants after a large Windows update. Rejected: Only clearing __pycache__ earlier | the update process can still hold old modules in sys.modules. Confidence: high Scope-risk: narrow Directive: Keep update-time lazy refresh guarded against in-process code skew after git pull. Tested: ./.venv/bin/python -m pytest tests/hermes_cli/test_update_autostash.py::test_cmd_update_reloads_runtime_modules_before_lazy_refresh tests/hermes_cli/test_update_autostash.py::test_reload_updated_runtime_modules_restores_new_hermes_constants_symbol -q Tested: ./.venv/bin/python -m pytest tests/hermes_cli/test_update_autostash.py -q Tested: ./.venv/bin/python -m ruff check hermes_cli/main.py tests/hermes_cli/test_update_autostash.py Tested: ./.venv/bin/python -m py_compile hermes_cli/main.py tests/hermes_cli/test_update_autostash.py Tested: git diff --check Not-tested: Windows v0.14.0 to v0.18.0 end-to-end update.
This commit is contained in:
parent
0543078e97
commit
d76d0d61d8
2 changed files with 100 additions and 12 deletions
|
|
@ -4760,6 +4760,38 @@ def _clear_bytecode_cache(root: Path) -> int:
|
|||
return removed
|
||||
|
||||
|
||||
_UPDATE_RUNTIME_RELOAD_MODULES = (
|
||||
"hermes_constants",
|
||||
"tools.environments.local",
|
||||
"tools.lazy_deps",
|
||||
)
|
||||
|
||||
|
||||
def _reload_updated_runtime_modules() -> None:
|
||||
"""Reload update-sensitive modules after the checkout changes in-place.
|
||||
|
||||
``hermes update`` keeps running in the pre-pull Python process. After a
|
||||
large update, modules already present in ``sys.modules`` can still expose
|
||||
old symbols even though their source files on disk are new. Refresh the
|
||||
small module set used by lazy-backend refresh before that step imports
|
||||
newly-updated code paths.
|
||||
"""
|
||||
try:
|
||||
import importlib
|
||||
|
||||
importlib.invalidate_caches()
|
||||
for module_name in _UPDATE_RUNTIME_RELOAD_MODULES:
|
||||
module = sys.modules.get(module_name)
|
||||
if module is None:
|
||||
continue
|
||||
try:
|
||||
importlib.reload(module)
|
||||
except Exception as exc:
|
||||
logger.debug("Could not reload updated module %s: %s", module_name, exc)
|
||||
except Exception as exc:
|
||||
logger.debug("Could not refresh update runtime modules: %s", exc)
|
||||
|
||||
|
||||
# Critical files that Hermes must be able to import immediately after an
|
||||
# update/install. Most are imported on every CLI startup; ``web_server.py``
|
||||
# is the desktop/dashboard backend path that a fresh Windows install launches
|
||||
|
|
@ -12353,6 +12385,19 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||
# based on a narrow 7-package import probe (#58004 review).
|
||||
_clear_update_incomplete_marker()
|
||||
|
||||
# The update process is still the old Python interpreter process. Run
|
||||
# one final cache/module refresh immediately before lazy backend
|
||||
# refresh, which imports newly-pulled modules that may depend on fresh
|
||||
# symbols in hermes_constants or lazy_deps. The dependency install
|
||||
# above may also have regenerated bytecode from build-cache copies —
|
||||
# this second sweep catches those stragglers (#60242, #65240).
|
||||
removed = _clear_bytecode_cache(PROJECT_ROOT)
|
||||
if removed:
|
||||
print(
|
||||
f" ✓ Cleared {removed} stale __pycache__ director{'y' if removed == 1 else 'ies'}"
|
||||
)
|
||||
_reload_updated_runtime_modules()
|
||||
|
||||
# Upgrade pip before lazy refreshes — stale pip can fail source builds
|
||||
# and leave partially-written packages (#57828).
|
||||
_write_lazy_refresh_incomplete_marker()
|
||||
|
|
@ -12511,18 +12556,6 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||
except Exception as e:
|
||||
logger.debug("Model catalog seed during update failed: %s", e)
|
||||
|
||||
# After git pull, source files on disk are newer than cached Python
|
||||
# modules in this process. Reload hermes_constants so that any lazy
|
||||
# import executed below (skills sync, gateway restart) sees new
|
||||
# attributes like display_hermes_home() added since the last release.
|
||||
try:
|
||||
import importlib
|
||||
import hermes_constants as _hc
|
||||
|
||||
importlib.reload(_hc)
|
||||
except Exception:
|
||||
pass # non-fatal — worst case a lazy import fails gracefully
|
||||
|
||||
# Sync bundled skills (copies new, updates changed, respects user deletions)
|
||||
try:
|
||||
from tools.skills_sync import sync_skills
|
||||
|
|
|
|||
|
|
@ -568,6 +568,61 @@ def test_cmd_update_refreshes_active_memory_provider_dependencies(monkeypatch, t
|
|||
assert refresh_calls == [True]
|
||||
|
||||
|
||||
def test_cmd_update_reloads_runtime_modules_before_lazy_refresh(monkeypatch, tmp_path):
|
||||
"""Lazy refresh must not see pre-pull modules cached in this process."""
|
||||
_setup_update_mocks(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/uv" if name == "uv" else None)
|
||||
monkeypatch.setattr(hermes_main, "_is_termux_env", lambda env=None: False)
|
||||
|
||||
events = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
if cmd == ["git", "fetch", "origin", "main"]:
|
||||
return SimpleNamespace(stdout="", stderr="", returncode=0)
|
||||
if cmd == ["git", "rev-parse", "--abbrev-ref", "HEAD"]:
|
||||
return SimpleNamespace(stdout="main\n", stderr="", returncode=0)
|
||||
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
|
||||
return SimpleNamespace(stdout="1\n", stderr="", returncode=0)
|
||||
if cmd == ["git", "pull", "--ff-only", "origin", "main"]:
|
||||
events.append("pull")
|
||||
return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0)
|
||||
if "pip" in cmd and "install" in cmd:
|
||||
events.append("install")
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
def fake_reload_runtime_modules():
|
||||
events.append("reload")
|
||||
|
||||
def fake_refresh_lazy_features(install_prefix=None, env=None):
|
||||
events.append("lazy-refresh")
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(hermes_main, "_reload_updated_runtime_modules", fake_reload_runtime_modules)
|
||||
monkeypatch.setattr(hermes_main, "_refresh_active_lazy_features", fake_refresh_lazy_features)
|
||||
|
||||
hermes_main.cmd_update(SimpleNamespace())
|
||||
|
||||
assert (
|
||||
events.index("pull")
|
||||
< events.index("install")
|
||||
< events.index("reload")
|
||||
< events.index("lazy-refresh")
|
||||
)
|
||||
|
||||
|
||||
def test_reload_updated_runtime_modules_restores_new_hermes_constants_symbol(monkeypatch):
|
||||
"""A pre-pull module object missing a new helper is repaired by reload."""
|
||||
import hermes_constants
|
||||
|
||||
monkeypatch.delattr(hermes_constants, "apply_subprocess_home_env", raising=False)
|
||||
assert not hasattr(hermes_constants, "apply_subprocess_home_env")
|
||||
|
||||
hermes_main._reload_updated_runtime_modules()
|
||||
|
||||
assert callable(hermes_constants.apply_subprocess_home_env)
|
||||
|
||||
|
||||
def test_install_with_optional_fallback_honors_custom_group(monkeypatch):
|
||||
"""Termux update path should target .[termux-all] when requested."""
|
||||
calls = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue