From ee05e39679697437c60a7eb884280a6c84c4e318 Mon Sep 17 00:00:00 2001 From: Paul Robertson Date: Thu, 27 Aug 2026 19:16:10 +0000 Subject: [PATCH 1/2] fix(hermes-adapter): widen _ACTIVE_CLIENTS key with owner_id to prevent intra-process bridge fight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the Hermes gateway runs multiple concurrent sessions (email threads, cron, subagents) in one process, each gets its own MemTensorProvider instance. Every MemosBridgeClient() construction closes whatever client holds the per-process (agent, no_viewer, runtime_home) singleton slot in _ACTIVE_CLIENTS — which may belong to a DIFFERENT sessions provider. This causes a mutual kill loop: session A creates a bridge, session B creates a bridge and closes As bridge, session As keepalive respawns and closes Bs bridge. Signature: gateway holds 2+ bridge.mjs children, PIDs churn every 2-3s while the dashboards single bridge is rock-stable. Fix: widen the singleton key to (agent, no_viewer, runtime_home, owner_id) so concurrent provider instances in one process coexist. Each construction site passes owner_id=f"provider-{id(self)}", giving every provider instance a stable lifetime-scoped identity. Related: #1910 (original bridge-process leak guard), #1927 (initialize leak), #1985 (HTTP transport — the architectural end-state) --- .../adapters/hermes/memos_provider/__init__.py | 4 ++++ .../hermes/memos_provider/bridge_client.py | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) 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..eb95c51df 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,13 @@ def __init__( no_viewer: bool = True, extra_env: dict[str, str] | None = None, runtime_home: str | None = None, + owner_id: str | None = None, + ) -> None:| None = None, + node_binary: str | None = None, + agent: str = "hermes", + no_viewer: bool = True, + extra_env: dict[str, str] | None = None, + runtime_home: str | None = None, ) -> None: self._lock = threading.Lock() self._next_id = 1 @@ -265,6 +272,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 +293,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 +306,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: From f1d444c48f04f34e51dbafbf03406c6307101b4a Mon Sep 17 00:00:00 2001 From: Paul Robertson Date: Fri, 28 Aug 2026 01:49:00 +0000 Subject: [PATCH 2/2] fix: repair mangled __init__ signature from regex overmatch --- .../adapters/hermes/memos_provider/bridge_client.py | 6 ------ 1 file changed, 6 deletions(-) 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 eb95c51df..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 @@ -151,12 +151,6 @@ def __init__( extra_env: dict[str, str] | None = None, runtime_home: str | None = None, owner_id: str | None = None, - ) -> None:| None = None, - node_binary: str | None = None, - agent: str = "hermes", - no_viewer: bool = True, - extra_env: dict[str, str] | None = None, - runtime_home: str | None = None, ) -> None: self._lock = threading.Lock() self._next_id = 1