diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 52a8f05b0d..d5ad2e9ab6 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -7416,17 +7416,18 @@ _AUX_TASK_SLOTS: Tuple[str, ...] = ( def _dashboard_code_skew_guard() -> Optional[str]: - """Return a clear \"restart required\" message when the dashboard runs stale code. + """Return a clear \"restart required\" message when this process runs stale code. - The dashboard is a long-lived process; its ``sys.modules`` is frozen at - boot. When ``hermes update`` (or a manual ``git pull``) replaces the - checkout underneath it, a first-time lazy import on a new code path can - resolve a freshly-pulled consumer module against a stale cached dependency - -> ImportError — e.g. ``/api/model/options`` 500 after the update added - ``agent.model_metadata.is_grok_46_family`` while the running process kept - serving the pre-update module (#86207). Mirror the gateway's - ``_model_switch_skew_guard``: refuse the risky call with an actionable - message instead of crashing with a cryptic import error. + The dashboard and Desktop-owned ``hermes serve`` are long-lived; their + ``sys.modules`` is frozen at boot. When ``hermes update`` (or a manual + ``git pull``) replaces the checkout underneath them, a first-time lazy + import on a new code path can resolve a freshly-pulled consumer module + against a stale cached dependency -> ImportError — e.g. ``/api/model/options`` + 500 after the update added ``agent.model_metadata.is_grok_46_family`` while + the running process kept serving the pre-update module (#86207). Mirror + the gateway's ``_model_switch_skew_guard``: refuse the risky call with an + actionable, deployment-aware message instead of crashing with a cryptic + import error (#97046). Returns None when no drift is detectable (fresh process, or a non-git install where the boot fingerprint could not be read — never a false @@ -7439,10 +7440,28 @@ def _dashboard_code_skew_guard() -> Optional[str]: return None boot_rev, disk_rev = skew return ( - f"This dashboard is running code from {boot_rev} but the checkout on " + f"This process is running code from {boot_rev} but the checkout on " f"disk is now {disk_rev}. The model picker would risk a stale-module " - f"crash — restart the dashboard to load the new code " - f"(systemctl --user restart hermes-dashboard, or hermes dashboard --port )" + f"crash — {_dashboard_skew_restart_hint()}" + ) + + +def _dashboard_skew_restart_hint() -> str: + """Restart advice that matches how this process is actually owned. + + The same FastAPI app backs the browser dashboard *and* Desktop-owned + ``hermes serve --isolated`` (local or SSH). Hardcoding a systemd unit + misleads macOS/launchd hosts and Desktop SSH backends, which have no + ``hermes-dashboard`` unit (#97046). + """ + if os.environ.get("HERMES_SERVE_HEADLESS") == "1": + return ( + "restart the Desktop-owned backend to load the new code " + "(use Restart backend in Hermes Desktop, or quit and reopen the app)" + ) + return ( + "restart this Hermes process to load the new code " + "(hermes dashboard --port , or the equivalent service restart for this install)" ) diff --git a/tests/test_code_skew.py b/tests/test_code_skew.py index 646260ff47..3f9539cfbc 100644 --- a/tests/test_code_skew.py +++ b/tests/test_code_skew.py @@ -81,12 +81,26 @@ class TestDashboardCodeSkewGuard: def test_dashboard_guard_message_names_revs_and_restart(self, monkeypatch): from hermes_cli import web_server + monkeypatch.delenv("HERMES_SERVE_HEADLESS", raising=False) monkeypatch.setattr(code_skew, "detect_code_skew", lambda: ("abc1234567", "def4567890")) msg = web_server._dashboard_code_skew_guard() assert msg is not None assert "abc1234567" in msg assert "def4567890" in msg assert "restart" in msg.lower() + # Browser-dashboard path: never hardcode a Linux-only unit (#97046). + assert "systemctl" not in msg + + def test_serve_guard_message_points_at_desktop_backend(self, monkeypatch): + from hermes_cli import web_server + + monkeypatch.setenv("HERMES_SERVE_HEADLESS", "1") + monkeypatch.setattr(code_skew, "detect_code_skew", lambda: ("abc1234567", "def4567890")) + msg = web_server._dashboard_code_skew_guard() + assert msg is not None + assert "Desktop-owned backend" in msg + assert "systemctl" not in msg + assert "hermes-dashboard" not in msg class TestModelOptionsSkewGuard: