From df55660e3c3c17f6123d86938848da78e05d500f Mon Sep 17 00:00:00 2001 From: LeonSGP43 <154585401+LeonSGP43@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:11:59 +0800 Subject: [PATCH] fix(hindsight): disable broken local runtime on unsupported CPUs --- plugins/memory/hindsight/__init__.py | 37 ++++++++++++++- .../plugins/memory/test_hindsight_provider.py | 46 ++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/plugins/memory/hindsight/__init__.py b/plugins/memory/hindsight/__init__.py index 56274efd0..c9ebb95f8 100644 --- a/plugins/memory/hindsight/__init__.py +++ b/plugins/memory/hindsight/__init__.py @@ -23,6 +23,7 @@ Or via $HERMES_HOME/hindsight/config.json (profile-scoped), falling back to from __future__ import annotations import asyncio +import importlib import json import logging import os @@ -54,6 +55,22 @@ _PROVIDER_DEFAULT_MODELS = { } +def _check_local_runtime() -> tuple[bool, str | None]: + """Return whether local embedded Hindsight imports cleanly. + + On older CPUs, importing the local Hindsight stack can raise a runtime + error from NumPy before the daemon starts. Treat that as "unavailable" + so Hermes can degrade gracefully instead of repeatedly trying to start + a broken local memory backend. + """ + try: + importlib.import_module("hindsight") + importlib.import_module("hindsight_embed.daemon_embed_manager") + return True, None + except Exception as exc: + return False, str(exc) + + # --------------------------------------------------------------------------- # Dedicated event loop for Hindsight async calls (one per process, reused). # Avoids creating ephemeral loops that leak aiohttp sessions. @@ -368,7 +385,10 @@ class HindsightMemoryProvider(MemoryProvider): try: cfg = _load_config() mode = cfg.get("mode", "cloud") - if mode in ("local", "local_embedded", "local_external"): + if mode in ("local", "local_embedded"): + available, _ = _check_local_runtime() + return available + if mode == "local_external": return True has_key = bool( cfg.get("apiKey") @@ -604,6 +624,12 @@ class HindsightMemoryProvider(MemoryProvider): """Return the cached Hindsight client (created once, reused).""" if self._client is None: if self._mode == "local_embedded": + available, reason = _check_local_runtime() + if not available: + raise RuntimeError( + "Hindsight local runtime is unavailable" + + (f": {reason}" if reason else "") + ) from hindsight import HindsightEmbedded HindsightEmbedded.__del__ = lambda self: None llm_provider = self._config.get("llm_provider", "") @@ -676,6 +702,15 @@ class HindsightMemoryProvider(MemoryProvider): # "local" is a legacy alias for "local_embedded" if self._mode == "local": self._mode = "local_embedded" + if self._mode == "local_embedded": + available, reason = _check_local_runtime() + if not available: + logger.warning( + "Hindsight local mode disabled because its runtime could not be imported: %s", + reason, + ) + self._mode = "disabled" + return self._api_key = self._config.get("apiKey") or self._config.get("api_key") or os.environ.get("HINDSIGHT_API_KEY", "") default_url = _DEFAULT_LOCAL_URL if self._mode in ("local_embedded", "local_external") else _DEFAULT_API_URL self._api_url = self._config.get("api_url") or os.environ.get("HINDSIGHT_API_URL", default_url) diff --git a/tests/plugins/memory/test_hindsight_provider.py b/tests/plugins/memory/test_hindsight_provider.py index 829a988ae..aed3a2349 100644 --- a/tests/plugins/memory/test_hindsight_provider.py +++ b/tests/plugins/memory/test_hindsight_provider.py @@ -705,6 +705,10 @@ class TestAvailability: lambda: tmp_path / "nonexistent", ) monkeypatch.setenv("HINDSIGHT_MODE", "local") + monkeypatch.setattr( + "plugins.memory.hindsight.importlib.import_module", + lambda name: object(), + ) p = HindsightMemoryProvider() assert p.is_available() @@ -713,7 +717,7 @@ class TestAvailability: config_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text(json.dumps({ "mode": "cloud", - "api_key": "config-key", + "api_key": "***", })) monkeypatch.setattr( "plugins.memory.hindsight.get_hermes_home", @@ -723,3 +727,43 @@ class TestAvailability: p = HindsightMemoryProvider() assert p.is_available() + + def test_local_mode_unavailable_when_runtime_import_fails(self, tmp_path, monkeypatch): + monkeypatch.setattr( + "plugins.memory.hindsight.get_hermes_home", + lambda: tmp_path / "nonexistent", + ) + monkeypatch.setenv("HINDSIGHT_MODE", "local") + + def _raise(_name): + raise RuntimeError( + "NumPy was built with baseline optimizations: (x86_64-v2)" + ) + + monkeypatch.setattr( + "plugins.memory.hindsight.importlib.import_module", + _raise, + ) + p = HindsightMemoryProvider() + assert not p.is_available() + + def test_initialize_disables_local_mode_when_runtime_import_fails(self, tmp_path, monkeypatch): + config = {"mode": "local_embedded"} + config_path = tmp_path / "hindsight" / "config.json" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text(json.dumps(config)) + monkeypatch.setattr( + "plugins.memory.hindsight.get_hermes_home", lambda: tmp_path + ) + + def _raise(_name): + raise RuntimeError("x86_64-v2 unsupported") + + monkeypatch.setattr( + "plugins.memory.hindsight.importlib.import_module", + _raise, + ) + + p = HindsightMemoryProvider() + p.initialize(session_id="test-session", hermes_home=str(tmp_path), platform="cli") + assert p._mode == "disabled"