fix(acp): pre-import the memory provider on the main thread on Windows (#58083)

Every faulthandler dump in the thread shows session/new stuck in numpy's
create_module on the main thread while another thread (MCP discovery / ACP
stdin reader) sits in the same lazy import chain — a first-time native
extension import racing another thread deadlocks on Windows (holographic,
mnemosyne and hindsight all reproduce it; a sitecustomize `import numpy`
before any thread exists resolves it every time).

hermes acp now imports the configured memory.provider's module on the main
thread before the MCP-discovery thread and asyncio.run() start (Windows only —
the deadlock is Windows-specific and the import is paid once either way).
plugins.memory.import_memory_provider_module imports the module without
constructing a provider or running register(); the agent build later finds it
in sys.modules.

Trimmed from #91775 (@tigercraft4): same placement and gating; reuses the
existing plugin loader instead of a second module-import routine.

Co-authored-by: tigercraft4 <tigercraft4@tigercraft4.com>
This commit is contained in:
teknium1
2026-09-18 22:21:00 -07:00
committed by Teknium
parent 2a2c3e3ce9
commit 6a93afa0a8
2 changed files with 42 additions and 0 deletions

View File

@@ -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.

View File

@@ -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