diff --git a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py index e2389e498..3e9adcee4 100644 --- a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py +++ b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py @@ -514,6 +514,7 @@ def initialize(self, session_id: str, **kwargs: Any) -> None: # type: ignore[ov MemosBridgeClient( runtime_home=home, extra_env=env, + owner_id=f"provider-{id(self)}", ) ), before_spawn=lambda home=runtime_home: _prepare_shared_bridge(home), @@ -526,6 +527,7 @@ def initialize(self, session_id: str, **kwargs: Any) -> None: # type: ignore[ov new_bridge = MemosBridgeClient( runtime_home=str(runtime_home), extra_env=runtime_env, + owner_id=f"provider-{id(self)}", ) new_bridge.register_host_handler( "host.llm.complete", @@ -2195,6 +2197,7 @@ def _reconnect_bridge(self, session_id: str = "", *, timeout: float = 30.0) -> N MemosBridgeClient( runtime_home=home, extra_env=env, + owner_id=f"provider-{id(self)}", ) ), before_spawn=lambda home=runtime_home: _prepare_shared_bridge(home), @@ -2261,6 +2264,7 @@ def _reconnect_bridge(self, session_id: str = "", *, timeout: float = 30.0) -> N new_bridge = MemosBridgeClient( runtime_home=str(runtime_home), extra_env=runtime_env, + owner_id=f"provider-{id(self)}", ) logger.info( "MemOS: new bridge created (pid=%s)", diff --git a/apps/memos-local-plugin/adapters/hermes/memos_provider/bridge_client.py b/apps/memos-local-plugin/adapters/hermes/memos_provider/bridge_client.py index e137729ed..650cda763 100644 --- a/apps/memos-local-plugin/adapters/hermes/memos_provider/bridge_client.py +++ b/apps/memos-local-plugin/adapters/hermes/memos_provider/bridge_client.py @@ -46,7 +46,7 @@ # This is the Python-side guard against issue #1910 (bridge process leak: # every turn spawns new bridge.cjs). Defence in depth on the Node side # lives in ``bridge.cts`` via ``bridge-stdio.pid``. -_ACTIVE_CLIENTS: dict[tuple[str, bool, str], MemosBridgeClient] = {} +_ACTIVE_CLIENTS: dict[tuple[str, bool, str, str], MemosBridgeClient] = {} _ACTIVE_CLIENTS_LOCK = threading.Lock() @@ -150,6 +150,7 @@ def __init__( no_viewer: bool = True, extra_env: dict[str, str] | None = None, runtime_home: str | None = None, + owner_id: str | None = None, ) -> None: self._lock = threading.Lock() self._next_id = 1 @@ -265,6 +266,10 @@ def __init__( self._singleton_agent = agent self._singleton_no_viewer = bool(no_viewer) self._singleton_runtime_home = str(resolved_runtime_home) + # owner-keying: widen the singleton tracker key to (agent, no_viewer, home, + # owner_id) so concurrent provider instances in ONE process (gateway + # email/cron/subagent sessions) do not reap each other's bridges. + self._singleton_owner = owner_id or f"anon-{id(self)}" previous = self._register_active() if previous is not None and previous is not self: prev_pid = getattr(previous, "pid", "?") @@ -282,6 +287,7 @@ def _register_active(self) -> MemosBridgeClient | None: self._singleton_agent, self._singleton_no_viewer, self._singleton_runtime_home, + self._singleton_owner, ) with _ACTIVE_CLIENTS_LOCK: previous = _ACTIVE_CLIENTS.get(key) @@ -294,6 +300,7 @@ def _unregister_active(self) -> None: self._singleton_agent, self._singleton_no_viewer, self._singleton_runtime_home, + self._singleton_owner, ) with _ACTIVE_CLIENTS_LOCK: if _ACTIVE_CLIENTS.get(key) is self: