plugin-catalog/mem0.yaml (#131267) points at mem0ai/mem0's own integrations/hermes-plugin-mem0 at a reviewed pin. The entry stays dormant while a bundled copy exists (bundled providers win in find_provider_dir), so deleting the in-tree copy is what lets hermes_cli/memory_provider_migration.py install it for every home whose memory.provider is "mem0". The vendor copy keeps the provider name, mem0.json, the MEM0_* keys and the setup wizard. Also dropped: the hermes-agent[mem0] extra, its win-arm64 platform row and exclude-newer row, the pm/extras.py anchor (uv lock removes only mem0ai, posthog, qdrant-client and sqlalchemy), the mem0-specific doctor check (the generic check now names `hermes plugins install mem0`), and the tests that imported plugins.memory.mem0. Prose that lists the bundled set now names mem0 as a catalog provider. MEM0_* env hints and redaction stay: the catalog copy still reads them.
138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
"""Tests for per-user memory scoping via user_id threading.
|
|
|
|
Verifies that gateway user_id flows from AIAgent -> MemoryManager -> plugins,
|
|
so each gateway user gets their own memory bucket instead of sharing a static one.
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from agent.memory_provider import MemoryProvider
|
|
from agent.memory_manager import MemoryManager
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Concrete test provider that records init kwargs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class RecordingProvider(MemoryProvider):
|
|
"""Minimal provider that records what initialize() receives."""
|
|
|
|
def __init__(self, name="recording"):
|
|
self._name = name
|
|
self._init_kwargs = {}
|
|
self._init_session_id = None
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._name
|
|
|
|
def is_available(self) -> bool:
|
|
return True
|
|
|
|
def initialize(self, session_id: str, **kwargs) -> None:
|
|
self._init_session_id = session_id
|
|
self._init_kwargs = dict(kwargs)
|
|
|
|
def system_prompt_block(self) -> str:
|
|
return ""
|
|
|
|
def prefetch(self, query: str, *, session_id: str = "") -> str:
|
|
return ""
|
|
|
|
def sync_turn(self, user_content, assistant_content, *, session_id=""):
|
|
pass
|
|
|
|
def get_tool_schemas(self):
|
|
return []
|
|
|
|
def handle_tool_call(self, tool_name, args, **kwargs):
|
|
return json.dumps({})
|
|
|
|
def shutdown(self):
|
|
pass
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MemoryManager user_id threading tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestMemoryManagerUserIdThreading:
|
|
"""Verify user_id reaches providers via initialize_all."""
|
|
|
|
def test_no_user_id_when_cli(self):
|
|
"""CLI sessions should not have user_id in kwargs."""
|
|
mgr = MemoryManager()
|
|
p = RecordingProvider()
|
|
mgr.add_provider(p)
|
|
|
|
mgr.initialize_all(
|
|
session_id="sess-456",
|
|
platform="cli",
|
|
)
|
|
|
|
assert "user_id" not in p._init_kwargs
|
|
assert p._init_kwargs.get("platform") == "cli"
|
|
|
|
def test_multiple_providers_all_receive_user_id(self):
|
|
mgr = MemoryManager()
|
|
# Use one provider named "builtin" (always accepted) and one external
|
|
p1 = RecordingProvider("builtin")
|
|
p2 = RecordingProvider("external")
|
|
mgr.add_provider(p1)
|
|
mgr.add_provider(p2)
|
|
|
|
mgr.initialize_all(
|
|
session_id="sess-multi",
|
|
platform="slack",
|
|
user_id="slack_U12345",
|
|
)
|
|
|
|
assert p1._init_kwargs.get("user_id") == "slack_U12345"
|
|
assert p1._init_kwargs.get("platform") == "slack"
|
|
assert p2._init_kwargs.get("user_id") == "slack_U12345"
|
|
assert p2._init_kwargs.get("platform") == "slack"
|
|
|
|
def test_session_title_provenance_and_cwd_reach_provider(self, tmp_path):
|
|
from run_agent import AIAgent
|
|
|
|
provider = RecordingProvider()
|
|
session_db = MagicMock()
|
|
session_db.get_session_title.return_value = "Generated title"
|
|
session_db.get_session_title_source.return_value = "llm"
|
|
|
|
with patch(
|
|
"model_tools.get_tool_definitions",
|
|
return_value=[],
|
|
), patch(
|
|
"model_tools.check_toolset_requirements",
|
|
return_value={},
|
|
), patch(
|
|
"agent.process_bootstrap.OpenAI",
|
|
), patch(
|
|
"hermes_cli.config.load_config_readonly",
|
|
return_value={"memory": {"provider": "recording"}},
|
|
), patch(
|
|
"plugins.memory.load_memory_provider",
|
|
return_value=provider,
|
|
):
|
|
agent = AIAgent(
|
|
api_key="test-key-1234567890",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
quiet_mode=True,
|
|
skip_context_files=True,
|
|
platform="telegram",
|
|
session_id="session-with-title",
|
|
session_db=session_db,
|
|
gateway_session_key="agent:main:telegram:dm:42",
|
|
cwd=str(tmp_path),
|
|
)
|
|
|
|
assert provider._init_kwargs["session_title"] == "Generated title"
|
|
assert provider._init_kwargs["session_title_source"] == "llm"
|
|
assert (
|
|
provider._init_kwargs["gateway_session_key"]
|
|
== "agent:main:telegram:dm:42"
|
|
)
|
|
assert provider._init_kwargs["cwd"] == str(tmp_path)
|
|
agent.close()
|
|
|