Files
hermes-agent/hermes_cli/stale_modules.py
Cursor Agent a39dfffd65 fix(update): bridge stale utils so pre-handoff upgrades finish restart
Upgrades from ≤v2026.9.14 keep a cached root utils without file_signature
(narrow package-prefix purge). The post-pull gateway restart then imports
fresh hermes_cli.config and dies with ImportError, failing the daily
Install & Update E2E hermes-update legs. Drop the incomplete utils cache
before that import so the one pre-handoff upgrade can complete.

Co-authored-by: Samik Bandyopadhyay <callsamik@users.noreply.github.com>
2026-09-18 09:38:34 -07:00

46 lines
1.8 KiB
Python

"""Heal mixed ``sys.modules`` after an in-place checkout update.
Pre-reexec updaters (Hermes ≤ v2026.9.14) purged only package prefixes
(``hermes_cli``, ``gateway``, ``tools``, ``tui_gateway``, ``agent``) and left
root modules like ``utils`` cached in the updater process. The post-pull
gateway-restart phase then imports new ``hermes_cli.gateway`` /
``hermes_cli.config`` into that process; those need symbols the stale
``utils`` lacks (``file_signature``), and ``hermes update`` exits 1 with
``gateway auto-restart failed: cannot import name 'file_signature' from
'utils'``.
Post-swap hand-off (``hermes_cli.update_handoff``) makes this class dead for
updaters that already include it. This module is the bridge for the one
upgrade from a pre-handoff release onto a tree that needs new root symbols:
freshly imported ``hermes_cli`` code drops the incomplete cache before
importing ``utils``.
"""
from __future__ import annotations
import sys
from typing import Mapping, Sequence
# Root modules the narrow purge left behind, keyed by attributes that must
# exist on the on-disk copy after this release. Extend when a new root-level
# symbol would otherwise break the pre-handoff upgrade path.
_ROOT_MODULE_REQUIRED_ATTRS: dict[str, tuple[str, ...]] = {
"utils": ("file_signature",),
}
def drop_stale_root_modules(
required: Mapping[str, Sequence[str]] | None = None,
) -> list[str]:
"""Drop cached root modules missing required attrs. Returns dropped names."""
checks = _ROOT_MODULE_REQUIRED_ATTRS if required is None else required
dropped: list[str] = []
for name, attrs in checks.items():
mod = sys.modules.get(name)
if mod is None:
continue
if any(not hasattr(mod, attr) for attr in attrs):
sys.modules.pop(name, None)
dropped.append(name)
return dropped