From c62cadb73abf2087d167193e7d322d3d53f8a2ae Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:15:17 -0700 Subject: [PATCH] fix: make display_hermes_home imports lazy to prevent ImportError during hermes update (#3776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user runs 'hermes update', the Python process caches old modules in sys.modules. After git pull updates files on disk, lazy imports of newly-updated modules fail because they try to import display_hermes_home from the cached (old) hermes_constants which doesn't have the function. This specifically broke the gateway auto-restart in cmd_update — importing hermes_cli/gateway.py triggered the top-level 'from hermes_constants import display_hermes_home' against the cached old module. The ImportError was silently caught, so the gateway was never restarted after update. Users with a running gateway then hit the ImportError on their next Telegram/Discord message when the stale gateway process lazily loaded run_agent.py (new version) which also had the top-level import. Fixes: - hermes_cli/gateway.py: lazy import at call site (line 940) - run_agent.py: lazy import at call site (line 6927) - tools/terminal_tool.py: lazy imports at 3 call sites - tools/tts_tool.py: static schema string (no module-level call) - hermes_cli/auth.py: lazy import at call site (line 2024) - hermes_cli/main.py: reload hermes_constants after git pull in cmd_update Also fixes 4 pre-existing test failures in test_parse_env_var caused by NameError on display_hermes_home in terminal_tool.py. --- hermes_cli/auth.py | 5 +++-- hermes_cli/gateway.py | 6 ++++-- hermes_cli/main.py | 11 +++++++++++ hermes_cli/setup.py | 11 +++++++---- run_agent.py | 5 +++-- tools/terminal_tool.py | 10 ++++++---- tools/tts_tool.py | 4 ++-- 7 files changed, 36 insertions(+), 16 deletions(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index dd1e145af..6b41ac6e5 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -38,7 +38,7 @@ import httpx import yaml from hermes_cli.config import get_hermes_home, get_config_path -from hermes_constants import OPENROUTER_BASE_URL, display_hermes_home +from hermes_constants import OPENROUTER_BASE_URL logger = logging.getLogger(__name__) @@ -2021,7 +2021,8 @@ def _login_openai_codex(args, pconfig: ProviderConfig) -> None: config_path = _update_config_for_provider("openai-codex", creds.get("base_url", DEFAULT_CODEX_BASE_URL)) print() print("Login successful!") - print(f" Auth state: {display_hermes_home()}/auth.json") + from hermes_constants import display_hermes_home as _dhh + print(f" Auth state: {_dhh()}/auth.json") print(f" Config updated: {config_path} (model.provider=openai-codex)") diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 8e6bcc35e..f7bfad370 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -15,7 +15,8 @@ from pathlib import Path PROJECT_ROOT = Path(__file__).parent.parent.resolve() from hermes_cli.config import get_env_value, get_hermes_home, save_env_value, is_managed, managed_error -from hermes_constants import display_hermes_home +# display_hermes_home is imported lazily at call sites to avoid ImportError +# when hermes_constants is cached from a pre-update version during `hermes update`. from hermes_cli.setup import ( print_header, print_info, print_success, print_warning, print_error, prompt, prompt_choice, prompt_yes_no, @@ -936,7 +937,8 @@ def launchd_install(force: bool = False): print() print("Next steps:") print(" hermes gateway status # Check status") - print(f" tail -f {display_hermes_home()}/logs/gateway.log # View logs") + from hermes_constants import display_hermes_home as _dhh + print(f" tail -f {_dhh()}/logs/gateway.log # View logs") def launchd_uninstall(): plist_path = get_launchd_plist_path() diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 93dceeec8..4a00f1ef3 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2971,6 +2971,17 @@ def cmd_update(args): print() print("āœ“ Code updated!") + # 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 diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 0b74d2264..e00aaa4b9 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -289,7 +289,7 @@ from hermes_cli.config import ( get_env_value, ensure_hermes_home, ) -from hermes_constants import display_hermes_home +# display_hermes_home imported lazily at call sites (stale-module safety during hermes update) from hermes_cli.colors import Colors, color @@ -684,7 +684,8 @@ def _print_setup_summary(config: dict, hermes_home): print_warning( "Some tools are disabled. Run 'hermes setup tools' to configure them," ) - print_warning(f"or edit {display_hermes_home()}/.env directly to add the missing API keys.") + from hermes_constants import display_hermes_home as _dhh + print_warning(f"or edit {_dhh()}/.env directly to add the missing API keys.") print() # Done banner @@ -707,7 +708,8 @@ def _print_setup_summary(config: dict, hermes_home): print() # Show file locations prominently - print(color(f"šŸ“ All your files are in {display_hermes_home()}/:", Colors.CYAN, Colors.BOLD)) + from hermes_constants import display_hermes_home as _dhh + print(color(f"šŸ“ All your files are in {_dhh()}/:", Colors.CYAN, Colors.BOLD)) print() print(f" {color('Settings:', Colors.YELLOW)} {get_config_path()}") print(f" {color('API Keys:', Colors.YELLOW)} {get_env_path()}") @@ -2838,7 +2840,8 @@ def setup_gateway(config: dict): save_env_value("WEBHOOK_ENABLED", "true") print() print_success("Webhooks enabled! Next steps:") - print_info(f" 1. Define webhook routes in {display_hermes_home()}/config.yaml") + from hermes_constants import display_hermes_home as _dhh + print_info(f" 1. Define webhook routes in {_dhh()}/config.yaml") print_info(" 2. Point your service (GitHub, GitLab, etc.) at:") print_info(" http://your-server:8644/webhooks/") print() diff --git a/run_agent.py b/run_agent.py index 8bafb5b6d..026e22c45 100644 --- a/run_agent.py +++ b/run_agent.py @@ -45,7 +45,7 @@ import fire from datetime import datetime from pathlib import Path -from hermes_constants import get_hermes_home, display_hermes_home +from hermes_constants import get_hermes_home # Load .env from ~/.hermes/.env first, then project root as dev fallback. # User-managed env files should override stale shell exports on restart. @@ -6924,7 +6924,8 @@ class AIAgent: print(f"{self.log_prefix} Auth method: {auth_method}") print(f"{self.log_prefix} Token prefix: {key[:12]}..." if key and len(key) > 12 else f"{self.log_prefix} Token: (empty or short)") print(f"{self.log_prefix} Troubleshooting:") - _dhh = display_hermes_home() + from hermes_constants import display_hermes_home as _dhh_fn + _dhh = _dhh_fn() print(f"{self.log_prefix} • Check ANTHROPIC_TOKEN in {_dhh}/.env for Hermes-managed OAuth/setup tokens") print(f"{self.log_prefix} • Check ANTHROPIC_API_KEY in {_dhh}/.env for API keys or legacy token values") print(f"{self.log_prefix} • For API keys: verify at https://console.anthropic.com/settings/keys") diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 53a5e9b40..e97bc483c 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -48,7 +48,7 @@ logger = logging.getLogger(__name__) # long-running subprocesses immediately instead of blocking until timeout. # --------------------------------------------------------------------------- from tools.interrupt import is_interrupted, _interrupt_event # noqa: F401 — re-exported -from hermes_constants import display_hermes_home +# display_hermes_home imported lazily at call site (stale-module safety during hermes update) # ============================================================================= @@ -158,7 +158,8 @@ def _handle_sudo_failure(output: str, env_type: str) -> str: for failure in sudo_failures: if failure in output: - return output + f"\n\nšŸ’” Tip: To enable sudo over messaging, add SUDO_PASSWORD to {display_hermes_home()}/.env on the agent machine." + from hermes_constants import display_hermes_home as _dhh + return output + f"\n\nšŸ’” Tip: To enable sudo over messaging, add SUDO_PASSWORD to {_dhh()}/.env on the agent machine." return output @@ -444,7 +445,7 @@ def _parse_env_var(name: str, default: str, converter=int, type_label: str = "in except (ValueError, json.JSONDecodeError): raise ValueError( f"Invalid value for {name}: {raw!r} (expected {type_label}). " - f"Check {display_hermes_home()}/.env or environment variables." + f"Check ~/.hermes/.env or environment variables." ) @@ -1284,7 +1285,8 @@ if __name__ == "__main__": print(f" TERMINAL_MODAL_IMAGE: {os.getenv('TERMINAL_MODAL_IMAGE', default_img)}") print(f" TERMINAL_DAYTONA_IMAGE: {os.getenv('TERMINAL_DAYTONA_IMAGE', default_img)}") print(f" TERMINAL_CWD: {os.getenv('TERMINAL_CWD', os.getcwd())}") - print(f" TERMINAL_SANDBOX_DIR: {os.getenv('TERMINAL_SANDBOX_DIR', f'{display_hermes_home()}/sandboxes')}") + from hermes_constants import display_hermes_home as _dhh + print(f" TERMINAL_SANDBOX_DIR: {os.getenv('TERMINAL_SANDBOX_DIR', f'{_dhh()}/sandboxes')}") print(f" TERMINAL_TIMEOUT: {os.getenv('TERMINAL_TIMEOUT', '60')}") print(f" TERMINAL_LIFETIME_SECONDS: {os.getenv('TERMINAL_LIFETIME_SECONDS', '300')}") diff --git a/tools/tts_tool.py b/tools/tts_tool.py index b9251fe73..60f89787a 100644 --- a/tools/tts_tool.py +++ b/tools/tts_tool.py @@ -33,7 +33,7 @@ import subprocess import tempfile import threading from pathlib import Path -from hermes_constants import get_hermes_home, display_hermes_home +from hermes_constants import get_hermes_home from typing import Callable, Dict, Any, Optional logger = logging.getLogger(__name__) @@ -832,7 +832,7 @@ TTS_SCHEMA = { }, "output_path": { "type": "string", - "description": f"Optional custom file path to save the audio. Defaults to {display_hermes_home()}/cache/audio/.mp3" + "description": "Optional custom file path to save the audio. Defaults to ~/.hermes/audio_cache/.mp3" } }, "required": ["text"]