mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +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
RetainDB Memory Provider
Cloud memory API with hybrid search (Vector + BM25 + Reranking) and 7 memory types.
Requirements
- RetainDB account ($20/month) from retaindb.com
requestsis part of Hermes's core dependencies; no separate SDK install is needed. For damaged dependencies, usehermes pm repairand restart Hermes.
Setup
hermes memory setup # select "retaindb"
Or manually:
hermes config set memory.provider retaindb
echo "RETAINDB_API_KEY=your-key" >> ~/.hermes/.env
Config
All config via environment variables in .env:
| Env Var | Default | Description |
|---|---|---|
RETAINDB_API_KEY |
(required) | API key |
RETAINDB_BASE_URL |
https://api.retaindb.com |
API endpoint |
RETAINDB_PROJECT |
auto (profile-scoped) | Project identifier |
Tools
| Tool | Description |
|---|---|
retaindb_profile |
User's stable profile |
retaindb_search |
Semantic search |
retaindb_context |
Task-relevant context |
retaindb_remember |
Store a fact with type + importance |
retaindb_forget |
Delete a memory by ID |