mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 13:26:40 +00:00
On a multiplexed gateway every served profile's agents share one process. Each agent's MemoryManager owns its own provider instance, but nothing stopped a tool call or lifecycle hook from another profile's context, or from a context naming no profile, from reaching it. Nothing stopped two agents being handed one shared instance either. The in-repo providers also bound their stores from the ambient home rather than the one the manager passed. MemoryManager.initialize_all records the profile home (and agent_identity) it bound its providers to. Then: - Tool calls and every lifecycle hook (prefetch, queue_prefetch, sync_turn, on_turn_start, on_memory_write, on_session_end, on_session_switch, the session boundary, on_pre_compress, on_delegation) run only when the caller explicitly names that profile: the context's HERMES_HOME override, or the home its secret scope was built for. On a multiplexed process a caller that names no profile is refused, never resolved to the process's own profile. A refused checkpoint compression raises, so the transcript stays uncompressed. - Background hooks run under the owning profile's HERMES_HOME, set explicitly in the worker. - An instance another profile's manager already bound is dropped, not re-initialized (logged as memory_provider_instance_shared). Claims are tracked by instance identity, never by the provider's own hash or equality; on a multiplexed process an instance that can't be tracked (no weak reference) is refused too. - handle_tool_call passes hermes_home / agent_identity next to the previous commit's caller_gsk. A provider with the two-argument handle_tool_call gets only what it declares; caller_gsk alone made every call to it a TypeError. - The model sees a generic "refused, check gateway.log". gateway.log keeps memory_caller_profile_unresolved and both homes. ByteRover and RetainDB now bind their store from initialize()'s hermes_home. Both already kept their bindings on the instance and hold no profile state at module level. The provider side of the rule is documented on MemoryProvider and in the memory-provider plugin guide: a new instance per load, every binding from initialize()'s hermes_home, no profile state at module or class level, no per-call rebinding. Core cannot see inside an external provider; one that keeps a process-wide binding still mixes profiles' memories. The previous commit's two MagicMock tests in test_run_agent.py move into tests/agent/test_memory_provider_caller_profile.py (test_run_agent.py is over its line cap and may not grow). That file drives both dispatch paths and every hook with real managers and two profiles interleaving on threads; tests/plugins/memory/test_provider_multiplex_contract.py does the same for ByteRover and RetainDB with a fake backend. Refs #134889 (PLG-11) Refs #140208 Refs #140058