4 Commits
Author SHA1 Message Date
unsupportedpastels 59ee4989a6 docs(memory): say why a claim's weakref callback checks it is still the entry's reference
Refs #134889 (PLG-11)
Refs #140208
2026-10-17 13:28:18 -04:00
unsupportedpastels efe5751584 perf(memory): resolve the bound profile key and the tool-call signature once per manager
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
2026-10-17 13:28:18 -04:00
unsupportedpastels 93a267dcf2 fix(memory): write memory-provider refusals to gateway.log and name a log every host has
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
2026-10-17 13:28:18 -04:00
unsupportedpastels 419e6627aa fix(memory): serve a memory provider only to the profile it was initialized for
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
2026-10-17 13:28:18 -04:00