Every memory tool call resolved two profile-home keys (the caller's and
the bound home's) and ran inspect.signature on the provider's
handle_tool_call. The bound home and the provider never change for a
manager, so initialize_all now records the bound home's key with the scope
(bound_scope) and the manager caches each provider's accepted keywords.
A call resolves only the caller's home. Behaviour is unchanged.
Refs #134889 (PLG-11)
Refs #140208
The refusals log under agent.memory_caller_scope, which gateway.log's
component filter (gateway, hermes_plugins, plugins.platforms) dropped: on a
live multiplexed gateway they reached the profile's agent.log and
errors.log but never gateway.log, which the PR, the docs and the
chat-visible refusal all pointed at. The refusal can also happen in
hermes serve, the TUI and Desktop, where there is no gateway.log.
Add agent.memory_caller_scope to the gateway component's prefixes, and
have the generic refusal (tool error and the checkpoint RuntimeError) point
at `hermes logs errors`, which every host writes. The chat still sees no
check name and no home path.
Refs #134889 (PLG-11)
Refs #140208
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