mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(update): close the stale-bytecode class with a launch-time checkout-fingerprint sweep
The stale-.pyc bug class (#6207, #60242, live WhatsApp report: gateway ImportError 'cannot import name parse_model_flags_detailed' after /update) has one shared shape: the checkout's .py files change while __pycache__ retains bytecode from the previous revision, and a later process trusts the stale .pyc. Update-time clears can never fully close this class: 'hermes update' always executes the PRE-pull updater code, so hardening added to it takes effect one update late — and a manual 'git pull' never runs the updater at all. Class fix: - Launch-time guard in main(): compare the checkout fingerprint (cheap file reads via _read_git_revision_fingerprint, no git subprocess) against a .bytecode-fingerprint stamp; sweep __pycache__ once when they diverge. Covers manual pulls, old updaters, ZIP restores — every entry point (CLI, gateway service, desktop backend) passes through main(). - Record the stamp at all three update-time clear sites (git path pre- install, git path post-install, ZIP path) so a normal update never triggers a redundant launch sweep. - Sibling site: 'hermes plugins update' + dashboard plugin update now clear __pycache__ under the plugin dir after git pull (plugin trees live outside the repo guard). E2E validated: reproduced the stale-pyc shadowing (same-size same-mtime source swap → old symbol wins in a fresh process), confirmed the launch sweep restores the new symbol, no-ops when the checkout is unchanged, and re-sweeps on the next pull. Sabotage-tested: reverting the sweep fails 4 of the new tests.
This commit is contained in:
parent
d76d0d61d8
commit
ea2499bcfe
4 changed files with 281 additions and 0 deletions
|
|
@ -660,6 +660,11 @@ def cmd_update(name: str) -> None:
|
|||
console.print(f"[red]Error:[/red] {output}")
|
||||
sys.exit(1)
|
||||
|
||||
# Same stale-bytecode class as the main checkout (#6207/#60242): the
|
||||
# pull just changed .py files under this plugin dir, so drop any
|
||||
# __pycache__ compiled from the previous revision.
|
||||
_clear_plugin_bytecode(target)
|
||||
|
||||
# Copy any new .example files
|
||||
_copy_example_files(target, console)
|
||||
|
||||
|
|
@ -1954,6 +1959,10 @@ def dashboard_update_user_plugin(name: str) -> dict[str, Any]:
|
|||
if not ok:
|
||||
return {"ok": False, "error": msg}
|
||||
|
||||
# Sibling of the CLI ``hermes plugins update`` path: drop bytecode
|
||||
# compiled from the pre-pull plugin revision.
|
||||
_clear_plugin_bytecode(target)
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
_copy_example_files(target, Console())
|
||||
|
|
@ -1961,6 +1970,30 @@ def dashboard_update_user_plugin(name: str) -> dict[str, Any]:
|
|||
return {"ok": True, "name": name, "output": msg, "unchanged": unchanged}
|
||||
|
||||
|
||||
def _clear_plugin_bytecode(target: Path) -> int:
|
||||
"""Remove ``__pycache__`` dirs under a just-updated plugin checkout.
|
||||
|
||||
Plugin dirs live outside the main repo, so the launch-time checkout
|
||||
fingerprint sweep in ``hermes_cli.main`` never covers them. After a
|
||||
``git pull`` changes a plugin's ``.py`` files, stale bytecode here can
|
||||
produce the same ImportError class as #6207/#60242 in whichever
|
||||
process imports the plugin next. Never raises.
|
||||
"""
|
||||
removed = 0
|
||||
try:
|
||||
for cache_dir in target.rglob("__pycache__"):
|
||||
if not cache_dir.is_dir():
|
||||
continue
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
removed += 1
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
return removed
|
||||
|
||||
|
||||
def _git_pull_plugin_dir(target: Path) -> tuple[bool, str]:
|
||||
git_exe = _resolve_git_executable()
|
||||
if not git_exe:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue