diff --git a/acp_adapter/entry.py b/acp_adapter/entry.py index 13dcbcbcc4..9f855e3651 100644 --- a/acp_adapter/entry.py +++ b/acp_adapter/entry.py @@ -157,6 +157,19 @@ def _run_setup_browser(assume_yes: bool = False) -> int: return 1 +def _warm_memory_provider_import(logger: logging.Logger) -> None: + """Import ``memory.provider``'s module (no provider instance) before any thread exists.""" + try: + from hermes_cli.config import load_config + from plugins.memory import import_memory_provider_module + + name = str(((load_config() or {}).get("memory") or {}).get("provider") or "").strip() + if name and not import_memory_provider_module(name): + logger.debug("memory provider %r not warmed; agent init will report the real error", name) + except Exception: + logger.debug("memory provider warm-up skipped", exc_info=True) + + def main(argv: list[str] | None = None) -> None: """Entry point: load env, configure logging, run the ACP agent.""" args = _parse_args(argv) @@ -182,6 +195,14 @@ def main(argv: list[str] | None = None) -> None: import acp from .server import HermesACPAgent + # Windows: import the configured memory provider on the main thread while it is still + # the ONLY thread. A first-time native-extension import (numpy via holographic / + # mnemosyne / hindsight) racing the MCP-discovery or ACP stdin-reader thread's import + # chain deadlocked in create_module and session/new never answered (#58083). After + # this the off-loop agent build finds the module in sys.modules. + if sys.platform == "win32": + _warm_memory_provider_import(logger) + # MCP discovery from config.yaml runs in a background daemon thread so the ACP server is # responsive immediately (blocking here cost 2-5 s); per-session MCP servers registered via # asyncio.to_thread are unaffected. Metadata-only hosts can opt out of the global startup. diff --git a/plugins/memory/__init__.py b/plugins/memory/__init__.py index bf2c6dab50..8c293e2887 100644 --- a/plugins/memory/__init__.py +++ b/plugins/memory/__init__.py @@ -209,6 +209,27 @@ def load_memory_provider(name: str, *, register_skills: Optional[bool] = None) - return _loader.load_named(name, provider_dir, _load, kind="Memory provider", noun="provider", logger=logger) +def import_memory_provider_module(name: str) -> bool: + """Import the configured provider's module WITHOUT constructing a provider — the + later ``load_memory_provider`` then hits ``sys.modules`` instead of a fresh native + extension load. Exists so ``hermes acp`` can pay the heavy import (numpy / ML stack) + on the main thread before any other thread starts: on Windows a first-time native + import racing another thread's import chain deadlocked ``session/new`` (#58083). + False when the provider is unknown or its import fails (agent init reports that).""" + try: + if provider_dir := find_provider_dir(name): + return _loader.load_plugin_module( + _module_name(provider_dir, name), provider_dir, parents=("plugins", "plugins.memory"), + logger=logger, synthetic_namespace=None if _is_bundled(provider_dir) else _USER_NAMESPACE, + ) is not None + if (entry_point := find_provider_entry_point(name)) is not None: + entry_point.load() + return True + except Exception: + logger.debug("memory provider '%s' warm-up import failed", name, exc_info=True) + return False + + def _instantiate_subclass(namespace) -> Optional["MemoryProvider"]: """First instantiable ``MemoryProvider`` subclass found among *namespace*'s attributes.""" from agent.memory_provider import MemoryProvider