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.
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
"""Multiplex invariant: every memory provider's background thread runs under the spawner's profile.
|
|
|
|
Profile isolation is a ContextVar-scoped HERMES_HOME override; a plain ``threading.Thread`` starts
|
|
with an EMPTY context, so a provider's prefetch/sync/writer thread would silently resolve the DEFAULT
|
|
profile's home (and fail closed on scoped secrets). Each case drives the provider's real spawn path
|
|
with a fake backend and asserts the thread saw the parent's home.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from hermes_constants import get_hermes_home, reset_hermes_home_override, set_hermes_home_override
|
|
|
|
|
|
def _probe_home(seen: dict, key: str = "home"):
|
|
def _record(*_args, **_kwargs):
|
|
seen[key] = get_hermes_home()
|
|
return _record
|
|
|
|
|
|
def _retaindb(seen, tmp_path):
|
|
import plugins.memory.retaindb as retaindb
|
|
|
|
p = retaindb.RetainDBMemoryProvider()
|
|
p._client = MagicMock()
|
|
p._context_overlay = lambda query: {"context": seen.setdefault("home", get_hermes_home()) and "ctx"}
|
|
p._client.ask_user.return_value = {"answer": ""}
|
|
p._client.get_agent_model.return_value = {}
|
|
p.queue_prefetch("what do you know")
|
|
return list(p._prefetch_threads)
|
|
|
|
|
|
def _byterover(seen, tmp_path):
|
|
import plugins.memory.byterover as byterover
|
|
|
|
p = byterover.ByteRoverMemoryProvider()
|
|
p._curate = _probe_home(seen)
|
|
return [p._curate_in_background("content", name="brv-test", what="test")]
|
|
|
|
|
|
def _openviking(seen, tmp_path):
|
|
import plugins.memory.openviking as openviking
|
|
|
|
p = openviking.OpenVikingMemoryProvider()
|
|
workers: set = set()
|
|
p._spawn_tracked("ov-test", _probe_home(seen), threading.Lock(), lambda: workers)
|
|
return list(workers)
|
|
|
|
|
|
_PROVIDERS = {
|
|
"retaindb": _retaindb, "byterover": _byterover,
|
|
"openviking": _openviking,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("name", sorted(_PROVIDERS))
|
|
def test_provider_background_thread_sees_spawner_profile_home(name, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "default"))
|
|
profile_home = tmp_path / "profiles" / "b"
|
|
profile_home.mkdir(parents=True)
|
|
seen: dict = {}
|
|
token = set_hermes_home_override(profile_home)
|
|
try:
|
|
threads = _PROVIDERS[name](seen, tmp_path)
|
|
finally:
|
|
reset_hermes_home_override(token)
|
|
for t in threads:
|
|
if t is not None:
|
|
t.join(timeout=10)
|
|
assert seen.get("home") == profile_home, f"{name}: background thread resolved {seen.get('home')}"
|