From c4cf20f56469f49058791388f68d25fe75fe7c79 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:23:36 -0700 Subject: [PATCH] fix: clear __pycache__ during update to prevent stale bytecode ImportError (#3819) Third report of gateway crashing with: ImportError: cannot import name 'get_hermes_home' from 'hermes_constants' Root cause: stale .pyc bytecode files survive code updates. When Python loads a cached .pyc that references names from the old source, the import fails and the gateway won't start. Two bugs fixed: 1. Git update path: no cache clearing at all after git pull 2. ZIP update path: __pycache__ was explicitly in the preserve set Added _clear_bytecode_cache() helper that removes all __pycache__ dirs under PROJECT_ROOT (skipping venv/node_modules/.git/.worktrees). Called in both git and ZIP update paths, before pip install. --- hermes_cli/main.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 97428e091..e01bc660a 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2461,6 +2461,34 @@ def cmd_uninstall(args): run_uninstall(args) +def _clear_bytecode_cache(root: Path) -> int: + """Remove all __pycache__ directories under *root*. + + Stale .pyc files can cause ImportError after code updates when Python + loads a cached bytecode file that references names that no longer exist + (or don't yet exist) in the updated source. Clearing them forces Python + to recompile from the .py source on next import. + + Returns the number of directories removed. + """ + removed = 0 + for dirpath, dirnames, _ in os.walk(root): + # Skip venv / node_modules / .git entirely + dirnames[:] = [ + d for d in dirnames + if d not in ("venv", ".venv", "node_modules", ".git", ".worktrees") + ] + if os.path.basename(dirpath) == "__pycache__": + try: + import shutil as _shutil + _shutil.rmtree(dirpath) + removed += 1 + except OSError: + pass + dirnames.clear() # nothing left to recurse into + return removed + + def _update_via_zip(args): """Update Hermes Agent by downloading a ZIP archive. @@ -2502,7 +2530,7 @@ def _update_via_zip(args): break # Copy updated files over existing installation, preserving venv/node_modules/.git - preserve = {'venv', 'node_modules', '.git', '__pycache__', '.env'} + preserve = {'venv', 'node_modules', '.git', '.env'} update_count = 0 for item in os.listdir(extracted): if item in preserve: @@ -2525,6 +2553,11 @@ def _update_via_zip(args): except Exception as e: print(f"✗ ZIP update failed: {e}") sys.exit(1) + + # Clear stale bytecode after ZIP extraction + removed = _clear_bytecode_cache(PROJECT_ROOT) + if removed: + print(f" ✓ Cleared {removed} stale __pycache__ director{'y' if removed == 1 else 'ies'}") # Reinstall Python dependencies (try .[all] first for optional extras, # fall back to . if extras fail — mirrors the install script behavior) @@ -2923,6 +2956,13 @@ def cmd_update(args): ) _invalidate_update_cache() + + # Clear stale .pyc bytecode cache — prevents ImportError on gateway + # restart when updated source references names that didn't exist in + # the old bytecode (e.g. get_hermes_home added to hermes_constants). + removed = _clear_bytecode_cache(PROJECT_ROOT) + if removed: + print(f" ✓ Cleared {removed} stale __pycache__ director{'y' if removed == 1 else 'ies'}") # Reinstall Python dependencies (try .[all] first for optional extras, # fall back to . if extras fail — mirrors the install script behavior)