Description
When the Hermes gateway runs multiple concurrent sessions (email threads, cron jobs, subagents) in one process, each session 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 belongs to a different session's provider.
Root cause
The module-level singleton tracker _ACTIVE_CLIENTS in bridge_client.py was keyed by (agent, no_viewer, runtime_home) — one slot per process. This was correct as a guard against issue #1910 (bridge process leak where one provider kept spawning new bridges per turn), but fatal when N provider instances coexist in one gateway process.
Evidence chain:
- Gateway process hosts 2+
bridge.mjs --no-viewer children, replacing one every 2-3s (PIDs churn constantly)
- Dashboard (single session, one provider) has one stable bridge, zero churn
- The only difference: provider-instance count
- Daemon (
bridge.cjs --daemon, systemd, :18800) stable throughout
Fix
Widen the singleton key to (agent, no_viewer, runtime_home, owner_id) so concurrent provider instances in one process coexist instead of fighting.
Changes:
- bridge_client.py: key type widened
tuple[str, bool, str] -> tuple[str, bool, str, str]; owner_id: str | None parameter added to __init__; _singleton_owner field set from owner_id or f\"anon-{id(self)}\"; key construction in both _register_active and _unregister_active updated
- init.py: both construction sites (
initialize() and _reconnect_bridge()) in both shared-bridge and legacy modes pass owner_id=f\"provider-{id(self)}\"
Expected steady state after fix: daemon + 1 bridge per host process = 3 total; reconnect counts -> ~0.
Alternatives
Type of change
How Has This Been Tested?
Checklist
Description
When the Hermes gateway runs multiple concurrent sessions (email threads, cron jobs, subagents) in one process, each session gets its own
MemTensorProviderinstance. EveryMemosBridgeClient()construction closes whatever client holds the per-process(agent, no_viewer, runtime_home)singleton slot in_ACTIVE_CLIENTS— which belongs to a different session's provider.Root cause
The module-level singleton tracker
_ACTIVE_CLIENTSinbridge_client.pywas keyed by(agent, no_viewer, runtime_home)— one slot per process. This was correct as a guard against issue #1910 (bridge process leak where one provider kept spawning new bridges per turn), but fatal when N provider instances coexist in one gateway process.Evidence chain:
bridge.mjs --no-viewerchildren, replacing one every 2-3s (PIDs churn constantly)bridge.cjs --daemon, systemd, :18800) stable throughoutFix
Widen the singleton key to
(agent, no_viewer, runtime_home, owner_id)so concurrent provider instances in one process coexist instead of fighting.Changes:
tuple[str, bool, str]->tuple[str, bool, str, str];owner_id: str | Noneparameter added to__init__;_singleton_ownerfield set fromowner_id or f\"anon-{id(self)}\"; key construction in both_register_activeand_unregister_activeupdatedinitialize()and_reconnect_bridge()) in both shared-bridge and legacy modes passowner_id=f\"provider-{id(self)}\"Expected steady state after fix: daemon + 1 bridge per host process = 3 total; reconnect counts -> ~0.
Alternatives
Type of change
How Has This Been Tested?
bridge client is closedstorms since deployment (was thousands/day)Checklist