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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user