{"repo":"NousResearch/hermes-agent","public_id":"52921","display_label":"mem0 backend get_all count and pagination fixes","summary":"These members address issues with mem0 backends returning incorrect memory counts, specifically around top_k parameter passing and server-side pagination limits, along with related self-hosted backend work.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":56943,"merged_at":"2026-07-07T20:27:12+00:00"},"resolution":{"state":"resolved","by":"pr-56943"},"freshness":{"state":"verified","last_verified_at":"2026-07-07T20:27:12+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":52478,"pr_number":56943,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-07T20:27:12+00:00"},{"issue_number":52921,"pr_number":56943,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-07T20:27:12+00:00"}],"members":[{"kind":"issue","number":52921,"title":"SelfHostedBackend.get_all() returns incorrect count when memories exceed server page limit","url":null,"state":"closed"},{"kind":"pr","number":52973,"title":"fix(mem0): pass top_k to SDK in OSSBackend.get_all for accurate count","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#58323","number":58323},{"kind":"pr","value":"#58410","number":58410},{"kind":"pr","value":"#56943","number":56943}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":56943,"title":"feat(mem0): self-hosted dashboard backend + recall tuning (salvage #55614)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/mem0/README.md","plugins/memory/mem0/__init__.py","plugins/memory/mem0/_backend.py","plugins/memory/mem0/_setup.py","plugins/memory/mem0/plugin.yaml","tests/plugins/memory/test_mem0_backend.py","tests/plugins/memory/test_mem0_setup.py","tests/plugins/memory/test_mem0_v3.py","website/docs/user-guide/features/memory-providers.md"],"fix_hunks":[{"file":"plugins/memory/mem0/README.md","hunks":["@@ -25,14 +25,44 @@ Behavioral settings live in `$HERMES_HOME/mem0.json` (set them via `hermes memor\n \n | Key | Default | Description |\n |-----|---------|-------------|\n-| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-hosted) |\n+| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-managed, in-process) |\n+| `host` | — | Self-hosted Mem0 server URL (the Docker dashboard). When set, connects over HTTP with `X-API-Key`. Don't combine with `mode: oss` |\n | `user_id` | `hermes-user` | User identifier on Mem0 |\n | `agent_id` | `hermes` | Agent identifier |\n-| `rerank` | `true` | Rerank search results for relevance (platform mode only) |\n+| `rerank` | `false` | Rerank search results for relevance (platform mode only) |\n+\n+The plugin has three connection modes:\n+\n+- **Platform** — Mem0's hosted cloud (`api.mem0.ai`). Set `MEM0_API_KEY`. (default)\n+- **Self-hosted dashboard** — a Mem0 server you run yourself via Docker. Set `host`. See below.\n+- **OSS** — run Mem0 in-process with your own LLM + vector store. Set `mode: oss`. See below.\n+\n+## Self-Hosted Dashboard (Server) Mode\n+\n+Connect the plugin to a standalone Mem0 server you run yourself — the Docker-shipped Mem0 dashboard/server with its own REST API. Unlike OSS mode (which runs `mem0ai` in-process with your own vector store), here the plugin just talks HTTP to your server.\n+\n+1. Run the Mem0 server (FastAPI + pgvector) from its Docker image and note its URL and `ADMIN_API_KEY`.\n+2. Point the plugin at it — either via env vars:\n+   ```bash\n+   echo \"MEM0_HOST=http://localhost:8888\" >> ~/.hermes/.env\n+   echo \"MEM0_API_KEY=[redacted:secret-assignment]\" >> ~/.hermes/.env\n+   ```\n+   or in `$HERMES_HOME/mem0.json`:\n+   ```json\n+   {\n+     \"host\": \"http://localhost:8888\",\n+     \"api_key\": \"[redacted:secret-assignment]\"\n+   }\n+   ```\n+3. Start a fresh Hermes session and call `mem0_search` — it connects to your server.\n+\n+The plugin authenticates with `X-API-Key` and uses the server's `/search` and `/memories` routes. `api_key` is optional — omit it only for servers running with `AUTH_DISABLED`.\n+\n+> Setting `host` routes to the self-hosted server automatically. Don't set `mode: oss` — OSS takes precedence and ignores `host`.\n \n ## OSS (Self-Hosted) Mode\n \n-Run Mem0 locally with your own LLM, embedder, and vector store.\n+Run Mem0 locally with your own LLM, embedder, and vector store. This is the in-process SDK mode. To instead connect to a Mem0 server you run via Docker, see [Self-Hosted Dashboard (Server) Mode](#self-hosted-dashboard-server-mode) above.\n \n ### Interactive Setup\n ","@@ -106,7 +136,6 @@ hermes memory setup mem0 --mode oss --oss-llm-key sk-... --dry-run\n \n | Tool | Description |\n |------|-------------|\n-| `mem0_list` | List all stored memories (paginated) |\n | `mem0_search` | Semantic search by meaning |\n | `mem0_add` | Store a fact verbatim (no LLM extraction) |\n | `mem0_update` | Update a memory's text by ID |"]},{"file":"plugins/memory/mem0/__init__.py","hunks":["@@ -9,10 +9,15 @@\n -------------\n Secret (lives in $HERMES_HOME/.env or the environment):\n   MEM0_API_KEY       — Mem0 Platform API key (required for platform mode)\n+  MEM0_HOST          — Base URL of a self-hosted Mem0 server. When set, the\n+                       plugin talks to that server directly over HTTP\n+                       (X-API-Key auth) instead of the cloud API.\n \n Behavioral settings (live in $HERMES_HOME/mem0.json, set via `hermes memory\n setup`):\n   mode               — Backend mode: \"platform\" (default) or \"oss\"\n+  host               — Self-hosted Mem0 server URL (alt: MEM0_HOST env var).\n+                       When set, routes to the self-hosted HTTP backend.\n   user_id            — Canonical user identifier. When set, it is applied\n                        uniformly across every gateway (CLI, Telegram, Slack,\n                        Discord, …) so the same human gets one merged memory","@@ -44,7 +49,7 @@\n # for _BREAKER_COOLDOWN_SECS to avoid hammering a down server.\n _BREAKER_THRESHOLD = 5\n _BREAKER_COOLDOWN_SECS = 120\n-_PREFETCH_WAIT_SECS = 1.5\n+_PREFETCH_WAIT_SECS = 3\n \n _CLIENT_ERROR_TYPES = (\"MemoryNotFoundError\", \"ValidationError\")\n ","@@ -81,6 +86,7 @@ def _load_config() -> dict:\n     config = {\n         \"mode\": os.environ.get(\"MEM0_MODE\", \"platform\"),\n         \"api_key\": os.environ.get(\"MEM0_API_KEY\", \"\"),\n+        \"host\": os.environ.get(\"MEM0_HOST\", \"\"),\n         \"agent_id\": os.environ.get(\"MEM0_AGENT_ID\", \"hermes\"),\n         \"oss\": {},\n     }","@@ -107,41 +113,22 @@ def _load_config() -> dict:\n # Tool schemas\n # ---------------------------------------------------------------------------\n \n-LIST_SCHEMA = {\n-    \"name\": \"mem0_list\",\n-    \"description\": (\n-        \"List ALL stored memories about the user, unranked and paginated. \"\n-        \"Use for a full overview/audit at conversation start, or to browse \"\n-        \"everything when you don't have a specific query. For answering a \"\n-        \"specific question, prefer mem0_search.\"\n-    ),\n-    \"parameters\": {\n-        \"type\": \"object\",\n-        \"properties\": {\n-            \"page\": {\"type\": \"integer\", \"description\": \"Page number (default: 1).\"},\n-            \"page_size\": {\"type\": \"integer\", \"description\": \"Results per page (default: 100, max: 200).\"},\n-        },\n-        \"required\": [],\n-    },\n-}\n-\n SEARCH_SCHEMA = {\n     \"name\": \"mem0_search\",\n     \"description\": (\n         \"Search the user's memories by meaning; returns facts ranked by \"\n-        \"relevance. Use this BEFORE answering any question that may depend on \"\n+        \"relevance. Use this before answering any question that may depend on \"\n         \"what you know about the user (preferences, facts, history, people, \"\n         \"projects, past decisions). For multi-part or multi-hop questions, \"\n-        \"call it MULTIPLE times — vary the wording and run follow-up searches \"\n-        \"on what earlier results reveal; one search is rarely enough. Set \"\n-        \"rerank=true for higher accuracy on important queries.\"\n+        \"call it several times — vary the wording and run follow-up searches \"\n+        \"on what earlier results reveal; one search is rarely enough.\"\n     ),\n     \"parameters\": {\n         \"type\": \"object\",\n         \"properties\": {\n             \"query\": {\"type\": \"string\", \"description\": \"What to search for.\"},\n             \"top_k\": {\"type\": \"integer\", \"description\": \"Max results (default: 10, max: 50).\"},\n-            \"rerank\": {\"type\": \"boolean\", \"description\": \"Rerank results for relevance (default: true, platform mode only).\"},\n+            \"rerank\": {\"type\": \"boolean\", \"description\": \"Rerank results for relevance (default: false, platform mode only).\"},\n         },\n         \"required\": [\"query\"],\n     },","@@ -169,7 +156,7 @@ def _load_config() -> dict:\n     \"name\": \"mem0_update\",\n     \"description\": (\n         \"Replace the text of an existing memory by its ID (take the ID from a \"\n-        \"mem0_search or mem0_list result). Use when a stored fact has changed \"\n+        \"mem0_search result). Use when a stored fact has changed \"\n         \"or was wrong — correct it in place instead of adding a duplicate.\"\n     ),\n     \"parameters\": {","@@ -185,7 +172,7 @@ def _load_config() -> dict:\n DELETE_SCHEMA = {\n     \"name\": \"mem0_delete\",\n     \"description\": (\n-        \"Delete a memory by its ID (take the ID from a mem0_search or mem0_list \"\n+        \"Delete a memory by its ID (take the ID from a mem0_search \"\n         \"result). Use when a stored fact is obsolete or the user asks you to \"\n         \"forget it; prefer mem0_update if the fact merely changed.\"\n     ),","@@ -214,8 +201,10 @@ def __init__(self):\n         self._backend = None\n         self._mode = \"platform\"\n         self._api_key = \"\"\n+        self._host = \"\"\n         self._user_id = _DEFAULT_USER_ID\n         self._agent_id = \"hermes\"\n+        self._rerank_default = False\n         self._channel = \"cli\"  # gateway channel name (cli/telegram/discord/...)\n         self._sync_thread = None\n         self._prefetch_thread = None","@@ -239,7 +228,9 @@ def is_available(self) -> bool:\n         mode = cfg.get(\"mode\", \"platform\")\n         if mode == \"oss\":\n             return bool(cfg.get(\"oss\", {}).get(\"vector_store\"))\n-        return bool(cfg.get(\"api_key\"))\n+        # Platform needs an api_key; self-hosted needs a host (api_key optional\n+        # when the server runs with AUTH_DISABLED).\n+        return bool(cfg.get(\"api_key\") or cfg.get(\"host\"))\n \n     def save_config(self, values, hermes_home):\n         \"\"\"Write config to $HERMES_HOME/mem0.json.\"\"\"","@@ -262,9 +253,10 @@ def get_config_schema(self):\n         api_key_required = mode != \"oss\"\n         return [\n             {\"key\": \"api_key\", \"description\": \"Mem0 Platform API key\", \"secret\": True, \"required\": api_key_required, \"env_var\": \"MEM0_API_KEY\", \"url\": \"https://app.mem0.ai\"},\n+            {\"key\": \"host\", \"description\": \"Self-hosted Mem0 server URL (leave blank for cloud)\", \"required\": False, \"env_var\": \"MEM0_HOST\"},\n             {\"key\": \"user_id\", \"description\": \"User identifier\", \"default\": \"hermes-user\"},\n             {\"key\": \"agent_id\", \"description\": \"Agent identifier\", \"default\": \"hermes\"},\n-            {\"key\": \"rerank\", \"description\": \"Enable reranking for recall\", \"default\": \"true\", \"choices\": [\"true\", \"false\"]},\n+            {\"key\": \"rerank\", \"description\": \"Enable reranking for recall\", \"default\": \"false\", \"choices\": [\"true\", \"false\"]},\n         ]\n \n     def post_setup(self, hermes_home: str, config: dict) -> None:","@@ -288,6 +280,9 @@ def _create_backend(self):\n             if self._mode == \"oss\":\n                 from ._backend import OSSBackend\n                 return OSSBackend(self._config.get(\"oss\", {}))\n+            if self._host:\n+                from ._backend import SelfHostedBackend\n+                return SelfHostedBackend(self._api_key, self._host)\n             from ._backend import PlatformBackend\n             return PlatformBackend(self._api_key)\n         except Exception as e:","@@ -342,6 +337,7 @@ def initialize(self, session_id: str, **kwargs) -> None:\n         self._config = _load_config()\n         self._mode = self._config.get(\"mode\", \"platform\")\n         self._api_key = self._config.get(\"api_key\", \"\")\n+        self._host = self._config.get(\"host\", \"\")\n         # Resolution order for user_id:\n         #   1. Operator-configured MEM0_USER_ID (env or $HERMES_HOME/mem0.json) —\n         #      the canonical principal, applied across every gateway so the same","@@ -358,6 +354,14 @@ def initialize(self, session_id: str, **kwargs) -> None:\n             configured = None\n         self._user_id = configured or kwargs.get(\"user_id\") or _DEFAULT_USER_ID\n         self._agent_id = self._config.get(\"agent_id\", \"hermes\")\n+        # Persisted rerank preference (setup wizard / mem0.json). Used as the\n+        # DEFAULT for mem0_search when the model doesn't pass ``rerank``\n+        # explicitly; per-call args still win. Platform-only feature — other\n+        # backends accept-and-ignore the flag.\n+        _rr = self._config.get(\"rerank\", False)\n+        self._rerank_default = (\n+            _rr.lower() in (\"true\", \"1\", \"yes\") if isinstance(_rr, str) else bool(_rr)\n+        )\n         self._channel = kwargs.get(\"platform\") or \"cli\"\n         self._backend = self._create_backend()\n         if self._backend and not self._atexit_registered:","@@ -378,22 +382,32 @@ def _write_metadata(self) -> Dict[str, Any]:\n         return {\"channel\": self._channel} if self._channel else {}\n \n     def system_prompt_block(self) -> str:\n-        mode_label = \"platform (cloud API)\" if self._mode == \"platform\" else \"OSS (self-hosted)\"\n-        rerank_note = \" Rerank is available on search.\" if self._mode == \"platform\" else \"\"\n+        # Mirror the precedence in _create_backend (oss > host > platform) so\n+        # the label always names the backend that actually runs. Checking\n+        # ``host`` first here would mislabel an ``oss``+``host`` config as\n+        # self-hosted HTTP even though OSS wins the routing.\n+        if self._mode == \"oss\":\n+            mode_label = \"OSS (self-hosted)\"\n+        elif self._host:\n+            mode_label = \"self-hosted (HTTP API)\"\n+        else:\n+            mode_label = \"platform (cloud API)\"\n+        # Rerank is a Mem0 Platform feature only.\n+        rerank_note = \" Rerank is available on search.\" if (self._mode == \"platform\" and not self._host) else \"\"\n         return (\n             \"# Mem0 Memory\\n\"\n             f\"Active. Mode: {mode_label}. User: {self._user_id}.\\n\"\n             \"You have persistent memory of this user from past conversations. \"\n-            \"ALWAYS call mem0_search before answering anything that could depend \"\n+            \"You should call mem0_search before answering anything that could depend \"\n             \"on prior context (the user's preferences, facts, history, people, \"\n             \"projects, or earlier decisions) — do not rely on the chat window \"\n             \"alone, and do not assume you have no memory.\\n\"\n-            \"For multi-part or multi-hop questions, run SEVERAL searches with \"\n+            \"For multi-part or multi-hop questions, run several searches with \"\n             \"different wording/angles and follow-up searches on what the first \"\n             \"results surface; one search is rarely enough. Keep searching until \"\n             \"you have every fact the question needs before you answer.\\n\"\n             \"Tools: mem0_search to find memories, mem0_add to store facts, \"\n-            f\"mem0_list for a full overview, mem0_update and mem0_delete to manage by ID.{rerank_note}\"\n+            f\"mem0_update and mem0_delete to manage by ID.{rerank_note}\"\n         )\n \n     def on_turn_start(self, turn_number: int, message: str, **kwargs) -> None:","@@ -426,7 +440,7 @@ def _run():\n             body = \"\"\n             try:\n                 results = backend.search(\n-                    query, filters=self._read_filters(), top_k=10, rerank=True,\n+                    query, filters=self._read_filters(), top_k=10, rerank=False,\n                 )\n                 lines = [r.get(\"memory\", \"\") for r in (results or []) if r.get(\"memory\")]\n                 if lines:","@@ -497,7 +511,7 @@ def _sync():\n             self._sync_thread.start()\n \n     def get_tool_schemas(self) -> List[Dict[str, Any]]:\n-        return [LIST_SCHEMA, SEARCH_SCHEMA, ADD_SCHEMA, UPDATE_SCHEMA, DELETE_SCHEMA]\n+        return [SEARCH_SCHEMA, ADD_SCHEMA, UPDATE_SCHEMA, DELETE_SCHEMA]\n \n     def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:\n         if self._backend is None:","@@ -516,36 +530,13 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:\n                 msg += f\" Check that your {vs.get('provider', 'vector store')} is running.\"\n             return json.dumps({\"error\": msg})\n \n-        if tool_name == \"mem0_list\":\n-            try:\n-                page = max(1, int(args.get(\"page\", 1)))\n-                page_size = min(max(1, int(args.get(\"page_size\", 100))), 200)\n-                response = self._backend.get_all(\n-                    filters=self._read_filters(), page=page, page_size=page_size,\n-                )\n-                self._record_success()\n-                results = response.get(\"results\", [])\n-                if not results:\n-                    return json.dumps({\"result\": \"No memories stored yet.\"})\n-                items = [{\"id\": m.get(\"id\"), \"memory\": m.get(\"memory\", \"\")}\n-                         for m in results]\n-                return json.dumps({\n-                    \"results\": items,\n-                    \"count\": response.get(\"count\", len(items)),\n-                    \"page\": page, \"page_size\": page_size,\n-                })\n-            except Exception as e:\n-                if not _is_client_error(e):\n-                    self._record_failure()\n-                return tool_error(self._format_error(\"Failed to list memories\", e))\n-\n-        elif tool_name == \"mem0_search\":\n+        if tool_name == \"mem0_search\":\n             query = args.get(\"query\", \"\")\n             if not query:\n                 return tool_error(\"Missing required parameter: query\")\n             try:\n                 top_k = max(1, min(int(args.get(\"top_k\", 10)), 50))\n-                rerank_raw = args.get(\"rerank\", True)\n+                rerank_raw = args.get(\"rerank\", getattr(self, \"_rerank_default\", False))\n                 if isinstance(rerank_raw, str):\n                     rerank = rerank_raw.lower() not in (\"false\", \"0\", \"no\")\n                 else:","@@ -576,7 +567,8 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:\n                 )\n                 self._record_success()\n                 event_id = result.get(\"event_id\") if isinstance(result, dict) else None\n-                msg = \"Fact stored.\" if self._mode == \"oss\" else \"Fact queued for storage.\"\n+                # Cloud add is async (server-side extraction); OSS and self-hosted store synchronously.\n+                msg = \"Fact stored.\" if (self._mode == \"oss\" or self._host) else \"Fact queued for storage.\"\n                 return json.dumps({\"result\": msg, \"event_id\": event_id})\n             except Exception as e:\n                 self._record_failure()"]},{"file":"plugins/memory/mem0/_backend.py","hunks":["@@ -10,11 +10,7 @@ class Mem0Backend(ABC):\n     \"\"\"Unified interface over Platform (MemoryClient) and OSS (Memory) backends.\"\"\"\n \n     @abstractmethod\n-    def search(self, query: str, *, filters: dict, top_k: int = 10, rerank: bool = True) -> list[dict]:\n-        ...\n-\n-    @abstractmethod\n-    def get_all(self, *, filters: dict, page: int = 1, page_size: int = 100) -> dict:\n+    def search(self, query: str, *, filters: dict, top_k: int = 10, rerank: bool = False) -> list[dict]:\n         ...\n \n     @abstractmethod","@@ -57,16 +53,10 @@ def __init__(self, api_key: str):\n         from mem0 import MemoryClient\n         self._client = MemoryClient(api_key=[redacted:secret-assignment]\n \n-    def search(self, query: str, *, filters: dict, top_k: int = 10, rerank: bool = True) -> list[dict]:\n+    def search(self, query: str, *, filters: dict, top_k: int = 10, rerank: bool = False) -> list[dict]:\n         response = self._client.search(query, filters=filters, top_k=top_k, rerank=rerank)\n         return _unwrap_results(response)\n \n-    def get_all(self, *, filters: dict, page: int = 1, page_size: int = 100) -> dict:\n-        response = self._client.get_all(filters=filters, page=page, page_size=page_size)\n-        results = response.get(\"results\", []) if isinstance(response, dict) else response\n-        count = response.get(\"count\", len(results)) if isinstance(response, dict) else len(results)\n-        return {\"results\": results, \"count\": count}\n-\n     def add(\n         self,\n         messages: list,","@@ -90,6 +80,79 @@ def delete(self, memory_id: str) -> dict:\n         return {\"result\": \"Memory deleted.\", \"memory_id\": memory_id}\n \n \n+class SelfHostedBackend(Mem0Backend):\n+    \"\"\"Direct HTTP backend for a self-hosted Mem0 server (the FastAPI ``server/``).\n+\n+    mem0.MemoryClient can't be reused for self-hosted: it is hardwired to the\n+    cloud API — ``Authorization: Token`` auth and a ``GET /v1/ping/`` validation\n+    call in ``__init__`` that the self-hosted server does not expose (it would\n+    404 before any real request). This client talks to that server directly,\n+    using its actual contract: ``X-API-Key`` auth and the ``/memories`` /\n+    ``/search`` routes.\n+    \"\"\"\n+\n+    def __init__(self, api_key: str, host: str, transport=None):\n+        import httpx\n+\n+        headers = {\"Content-Type\": \"application/json\"}\n+        if api_key:\n+            headers[\"X-API-Key\"] = api_key  # omitted only for AUTH_DISABLED servers\n+        # Connect-level retries smooth over transient blips so a single\n+        # dropped SYN doesn't count toward the provider failure breaker.\n+        # ``transport`` is injectable for tests (httpx.MockTransport).\n+        if transport is None:\n+            transport = httpx.HTTPTransport(retries=2)\n+        self._client = httpx.Client(\n+            base_url=host.rstrip(\"/\"), headers=headers, timeout=30.0,\n+            transport=transport,\n+        )\n+\n+    def _json(self, method: str, path: str, **kwargs) -> Any:\n+        resp = self._client.request(method, path, **kwargs)\n+        resp.raise_for_status()\n+        return resp.json() if resp.content else {}\n+\n+    def search(self, query: str, *, filters: dict, top_k: int = 10, rerank: bool = False) -> list[dict]:\n+        # rerank is a platform-only feature; the self-hosted /search ignores it.\n+        body: dict[str, Any] = {\"query\": query, \"top_k\": top_k}\n+        if filters:\n+            body[\"filters\"] = filters  # user_id belongs in filters (top-level is deprecated)\n+        return _unwrap_results(self._json(\"POST\", \"/search\", json=body))\n+\n+    def add(\n+        self,\n+        messages: list,\n+        *,\n+        user_id: str,\n+        agent_id: str,\n+        infer: bool = False,\n+        metadata: dict | None = None,\n+    ) -> dict:\n+        body: dict[str, Any] = {\n+            \"messages\": messages,\n+            \"user_id\": user_id,\n+            \"agent_id\": agent_id,\n+            \"infer\": infer,\n+        }\n+        if metadata:\n+            body[\"metadata\"] = metadata\n+        return self._json(\"POST\", \"/memories\", json=body)\n+\n+    def update(self, memory_id: str, text: str) -> dict:\n+        self._json(\"PUT\", f\"/memories/{memory_id}\", json={\"text\": text})\n+        return {\"result\": \"Memory updated.\", \"memory_id\": memory_id}\n+\n+    def delete(self, memory_id: str) -> dict:\n+        self._json(\"DELETE\", f\"/memories/{memory_id}\")\n+        return {\"result\": \"Memory deleted.\", \"memory_id\": memory_id}\n+\n+    def close(self) -> None:\n+        try:\n+            self._client.close()\n+        except Exception:\n+            pass\n+\n+\n class OSSBackend(Mem0Backend):\n     \"\"\"Wraps mem0.Memory for self-hosted (OSS) mode.\"\"\"\n ","@@ -189,18 +252,10 @@ def _recreate_collection_if_dims_changed(provider: str, vs_config: dict, expecte\n             except Exception:\n                 pass\n \n-    def search(self, query: str, *, filters: dict, top_k: int = 10, rerank: bool = True) -> list[dict]:\n+    def search(self, query: str, *, filters: dict, top_k: int = 10, rerank: bool = False) -> list[dict]:\n         response = self._memory.search(query, filters=filters, top_k=top_k)\n         return _unwrap_results(response)\n \n-    def get_all(self, *, filters: dict, page: int = 1, page_size: int = 100) -> dict:\n-        response = self._memory.get_all(filters=filters)\n-        all_results = _unwrap_results(response)\n-        total = len(all_results)\n-        start = (page - 1) * page_size\n-        results = all_results[start : start + page_size]\n-        return {\"results\": results, \"count\": total}\n-\n     def add(\n         self,\n         messages: list,"]},{"file":"plugins/memory/mem0/_setup.py","hunks":["@@ -230,7 +230,7 @@ def _setup_platform(hermes_home: str, config: dict, flags: dict[str, str]) -> No\n         {\"key\": \"api_key\", \"description\": \"Mem0 Platform API key\", \"secret\": True, \"required\": True, \"env_var\": \"MEM0_API_KEY\", \"url\": \"https://app.mem0.ai\"},\n         {\"key\": \"user_id\", \"description\": \"User identifier\", \"default\": \"hermes-user\"},\n         {\"key\": \"agent_id\", \"description\": \"Agent identifier\", \"default\": \"hermes\"},\n-        {\"key\": \"rerank\", \"description\": \"Enable reranking for recall\", \"default\": \"true\", \"choices\": [\"true\", \"false\"]},\n+        {\"key\": \"rerank\", \"description\": \"Enable reranking for recall\", \"default\": \"false\", \"choices\": [\"true\", \"false\"]},\n     ]\n \n     existing_config = {}","@@ -293,6 +293,24 @@ def _setup_platform(hermes_home: str, config: dict, flags: dict[str, str]) -> No\n         return\n \n     provider_config[\"mode\"] = \"platform\"\n+    # Clear any stale self-hosted host: routing checks ``host`` before platform\n+    # (see _create_backend), so leaving it would silently keep routing to the\n+    # self-hosted server even though the user just chose platform mode. Set it\n+    # to \"\" rather than pop() — save_config merges into the existing mem0.json\n+    # (existing.update), so a popped key would survive; an empty value overwrites\n+    # it and reads as falsy at routing time.\n+    provider_config[\"host\"] = \"\"\n+    # The json-file clear above can't help when the host comes from the\n+    # environment: _load_config() seeds ``host`` from MEM0_HOST, and the\n+    # docs tell self-hosted users to put MEM0_HOST in ~/.hermes/.env. Warn\n+    # so the user knows platform mode won't take effect until it's removed.\n+    if os.environ.get(\"MEM0_HOST\", \"\").strip():\n+        print(\n+            \"\\n  ⚠ MEM0_HOST is set in your environment \"\n+            f\"({os.environ['MEM0_HOST']}). It overrides platform mode — \"\n+            \"remove it from ~/.hermes/.env (or unset it) or Hermes will keep \"\n+            \"routing to the self-hosted server.\"\n+        )\n \n     from hermes_cli.config import save_config\n     config[\"memory\"][\"provider\"] = \"mem0\""]},{"file":"plugins/memory/mem0/plugin.yaml","hunks":["@@ -1,5 +1,5 @@\n name: mem0\n-version: 1.2.0\n-description: \"Mem0 — server-side LLM fact extraction with semantic search, reranking, and automatic deduplication.\"\n+version: 1.3.0\n+description: \"Mem0 — server-side LLM fact extraction with semantic search, automatic deduplication, and opt-in reranking (platform mode).\"\n pip_dependencies:\n-  - mem0ai>=2.0.7,<3\n+  - mem0ai>=2.0.10,<3"]},{"file":"tests/plugins/memory/test_mem0_backend.py","hunks":["@@ -1,8 +1,13 @@\n-\"\"\"Tests for Mem0Backend abstraction — PlatformBackend and OSSBackend.\"\"\"\n+\"\"\"Tests for Mem0Backend abstraction — PlatformBackend, OSSBackend, SelfHostedBackend.\"\"\"\n \n import pytest\n \n-from plugins.memory.mem0._backend import Mem0Backend, PlatformBackend, OSSBackend\n+from plugins.memory.mem0._backend import (\n+    Mem0Backend,\n+    PlatformBackend,\n+    OSSBackend,\n+    SelfHostedBackend,\n+)\n \n \n class FakePlatformClient:","@@ -52,10 +57,10 @@ def test_search_forwards_rerank(self):\n         backend.search(\"q\", filters={}, rerank=False)\n         assert client.calls[0][2][\"rerank\"] is False\n \n-    def test_search_rerank_default_true(self):\n+    def test_search_rerank_default_false(self):\n         backend, client = self._make()\n         backend.search(\"q\", filters={})\n-        assert client.calls[0][2][\"rerank\"] is True\n+        assert client.calls[0][2][\"rerank\"] is False\n \n     def test_search_returns_list(self):\n         backend, _ = self._make()","@@ -63,13 +68,6 @@ def test_search_returns_list(self):\n         assert isinstance(result, list)\n         assert result[0][\"id\"] == \"m1\"\n \n-    def test_get_all_forwards_pagination(self):\n-        backend, client = self._make()\n-        result = backend.get_all(filters={\"user_id\": \"u1\"}, page=2, page_size=50)\n-        assert client.calls[0][1][\"page\"] == 2\n-        assert client.calls[0][1][\"page_size\"] == 50\n-        assert \"count\" in result\n-\n     def test_add_forwards_kwargs(self):\n         backend, client = self._make()\n         msgs = [{\"role\": \"user\", \"content\": \"hi\"}]","@@ -163,21 +161,6 @@ def test_search_ignores_rerank(self):\n         backend.search(\"q\", filters={}, rerank=True)\n         assert \"rerank\" not in memory.calls[0][2]\n \n-    def test_get_all_ignores_pagination(self):\n-        \"\"\"OSSBackend accepts page/page_size but does NOT forward to Memory.get_all().\"\"\"\n-        backend, memory = self._make()\n-        result = backend.get_all(filters={\"user_id\": \"u1\"}, page=2, page_size=50)\n-        call_kwargs = memory.calls[0][1]\n-        assert \"page\" not in call_kwargs\n-        assert \"page_size\" not in call_kwargs\n-        assert result[\"count\"] == 1\n-\n-    def test_get_all_returns_envelope(self):\n-        backend, _ = self._make()\n-        result = backend.get_all(filters={\"user_id\": \"u1\"})\n-        assert \"results\" in result\n-        assert \"count\" in result\n-\n     def test_add_forwards_kwargs(self):\n         backend, memory = self._make()\n         msgs = [{\"role\": \"user\", \"content\": \"hi\"}]","@@ -207,3 +190,115 @@ def test_delete_normalizes_response(self):\n         backend, _ = self._make()\n         result = backend.delete(\"m1\")\n         assert result == {\"result\": \"Memory deleted.\", \"memory_id\": \"m1\"}\n+\n+\n+httpx = pytest.importorskip(\"httpx\")\n+\n+\n+class _StubServer:\n+    \"\"\"Records requests and serves the real self-hosted server's response shapes.\"\"\"\n+\n+    def __init__(self, rows=10):\n+        self.requests = []\n+        self._rows = [{\"id\": f\"m{i}\", \"memory\": f\"f{i}\"} for i in range(rows)]\n+\n+    def handler(self, request):\n+        self.requests.append(request)\n+        path, method = request.url.path, request.method\n+        if path == \"/search\" and method == \"POST\":\n+            return httpx.Response(200, json={\"results\": [{\"id\": \"m1\", \"memory\": \"tea\", \"score\": 0.9}]})\n+        if path == \"/memories\" and method == \"GET\":\n+            top_k = int(request.url.params.get(\"top_k\", len(self._rows)))\n+            return httpx.Response(200, json={\"results\": self._rows[:top_k]})\n+        if path == \"/memories\" and method == \"POST\":\n+            return httpx.Response(200, json={\"results\": [{\"id\": \"new\", \"memory\": \"stored\", \"event\": \"ADD\"}]})\n+        if path.startswith(\"/memories/\") and method in (\"PUT\", \"DELETE\"):\n+            if path.endswith(\"/missing\"):  # server 404s unknown ids\n+                return httpx.Response(404, json={\"detail\": \"Memory not found\"})\n+            verb = \"updated\" if method == \"PUT\" else \"Memory deleted successfully\"\n+            return httpx.Response(200, json={\"message\": verb})\n+        return httpx.Response(404, json={\"detail\": \"not found\"})\n+\n+\n+def _backend(server, api_key=\"[redacted:secret-assignment]\", host=\"http://sh:8888\"):\n+    \"\"\"Build a SelfHostedBackend routed through the stub transport.\n+\n+    Uses the real __init__ (via the injectable ``transport`` kwarg) so the\n+    constructor's header/base_url setup is exercised by every test here.\n+    \"\"\"\n+    return SelfHostedBackend(\n+        api_key, host, transport=httpx.MockTransport(server.handler)\n+    )\n+\n+\n+class TestSelfHostedBackend:\n+    # --- constructor / auth setup (the crux of the bug) -------------------\n+\n+    def test_init_uses_x_api_key_not_token_auth(self):\n+        b = SelfHostedBackend(\"adminkey\", \"http://sh:8888\")\n+        assert b._client.headers[\"x-api-key\"] == \"adminkey\"\n+        assert \"authorization\" not in b._client.headers  # NOT the cloud 'Token' scheme\n+\n+    def test_init_strips_trailing_slash(self):\n+        b = SelfHostedBackend(\"k\", \"http://sh:8888/\")\n+        assert str(b._client.base_url) == \"http://sh:8888\"\n+\n+    def test_init_omits_api_key_header_when_blank(self):\n+        b = SelfHostedBackend(\"\", \"http://sh:8888\")  # AUTH_DISABLED server\n+        assert \"x-api-key\" not in b._client.headers\n+\n+    # --- search ----------------------------------------------------------\n+\n+    def test_search_posts_to_search_with_filters_in_body(self):\n+        s = _StubServer()\n+        results = _backend(s).search(\"drink\", filters={\"user_id\": \"u1\"}, top_k=5)\n+        req = s.requests[-1]\n+        assert (req.method, req.url.path) == (\"POST\", \"/search\")\n+        import json\n+        body = json.loads(req.content)\n+        assert body == {\"query\": \"drink\", \"top_k\": 5, \"filters\": {\"user_id\": \"u1\"}}\n+        assert results == [{\"id\": \"m1\", \"memory\": \"tea\", \"score\": 0.9}]\n+\n+    def test_search_sends_x_api_key_header(self):\n+        s = _StubServer()\n+        _backend(s).search(\"q\", filters={\"user_id\": \"u1\"})\n+        req = s.requests[-1]\n+        assert req.headers[\"x-api-key\"] == \"adminkey\"\n+        assert \"authorization\" not in req.headers\n+\n+    # --- add / update / delete ------------------------------------------\n+\n+    def test_add_posts_messages_and_identity(self):\n+        s = _StubServer()\n+        msgs = [{\"role\": \"user\", \"content\": \"likes tea\"}]\n+        result = _backend(s).add(msgs, user_id=\"u1\", agent_id=\"hermes\", infer=False, metadata={\"channel\": \"cli\"})\n+        req = s.requests[-1]\n+        assert (req.method, req.url.path) == (\"POST\", \"/memories\")\n+        import json\n+        body = json.loads(req.content)\n+        assert body == {\"messages\": msgs, \"user_id\": \"u1\", \"agent_id\": \"hermes\",\n+                        \"infer\": False, \"metadata\": {\"channel\": \"cli\"}}\n+        assert result[\"results\"][0][\"id\"] == \"new\"\n+\n+    def test_update_puts_text_to_memory_id(self):\n+        s = _StubServer()\n+        result = _backend(s).update(\"abc\", \"new text\")\n+        req = s.requests[-1]\n+        assert (req.method, req.url.path) == (\"PUT\", \"/memories/abc\")\n+        import json\n+        assert json.loads(req.content) == {\"text\": \"new text\"}\n+        assert result == {\"result\": \"Memory updated.\", \"memory_id\": \"abc\"}\n+\n+    def test_delete_calls_delete_endpoint(self):\n+        s = _StubServer()\n+        result = _backend(s).delete(\"abc\")\n+        req = s.requests[-1]\n+        assert (req.method, req.url.path) == (\"DELETE\", \"/memories/abc\")\n+        assert result == {\"result\": \"Memory deleted.\", \"memory_id\": \"abc\"}\n+\n+    # --- error propagation (feeds the plugin's circuit breaker) ----------\n+\n+    def test_http_error_raises(self):\n+        s = _StubServer()\n+        with pytest.raises(httpx.HTTPStatusError):\n+            _backend(s).delete(\"missing\")  # 404 -> raise_for_status; 'not found' won't trip breaker"]},{"file":"tests/plugins/memory/test_mem0_setup.py","hunks":["@@ -179,6 +179,22 @@ def test_platform_flag_mode(self, tmp_path, monkeypatch):\n         mem0_json = json.loads((tmp_path / \"mem0.json\").read_text())\n         assert mem0_json[\"mode\"] == \"platform\"\n \n+    def test_platform_setup_clears_stale_host(self, tmp_path, monkeypatch):\n+        # A user who previously ran self-hosted has host in mem0.json. Switching\n+        # to platform must drop host — otherwise routing (host > platform) keeps\n+        # sending them to the self-hosted server despite --mode platform.\n+        (tmp_path / \"mem0.json\").write_text(\n+            json.dumps({\"mode\": \"platform\", \"host\": \"http://old-selfhosted:8888\"})\n+        )\n+        monkeypatch.setattr(\"sys.argv\", [\"hermes\", \"--mode\", \"platform\", \"--api-key\", \"[redacted:secret-assignment]\"])\n+        monkeypatch.setattr(\"plugins.memory.mem0._setup.get_hermes_home\", lambda: tmp_path)\n+        _inject_fake_hermes_cli(monkeypatch)\n+        config = {\"memory\": {}}\n+        post_setup(str(tmp_path), config)\n+        mem0_json = json.loads((tmp_path / \"mem0.json\").read_text())\n+        assert mem0_json[\"mode\"] == \"platform\"\n+        assert not mem0_json.get(\"host\")  # cleared to falsy so routing → platform\n+\n     def test_oss_flag_mode(self, tmp_path, monkeypatch):\n         monkeypatch.setattr(\"sys.argv\", [\n             \"hermes\", \"--mode\", \"oss\", \"--oss-llm-key\", \"sk-oai\","]},{"file":"tests/plugins/memory/test_mem0_v3.py","hunks":["@@ -52,33 +52,6 @@ def _make_provider(self, monkeypatch, backend):\n         provider._backend = backend\n         return provider\n \n-    def test_list_returns_paginated_with_ids(self, monkeypatch):\n-        backend = FakeBackend(all_results={\n-            \"count\": 2,\n-            \"results\": [\n-                {\"id\": \"mem-1\", \"memory\": \"alpha\"},\n-                {\"id\": \"mem-2\", \"memory\": \"beta\"},\n-            ]\n-        })\n-        provider = self._make_provider(monkeypatch, backend)\n-        result = json.loads(provider.handle_tool_call(\"mem0_list\", {}))\n-        assert result[\"count\"] == 2\n-        assert result[\"results\"][0][\"id\"] == \"mem-1\"\n-        assert result[\"results\"][0][\"memory\"] == \"alpha\"\n-\n-    def test_list_pagination_params(self, monkeypatch):\n-        backend = FakeBackend()\n-        provider = self._make_provider(monkeypatch, backend)\n-        provider.handle_tool_call(\"mem0_list\", {\"page\": 2, \"page_size\": 50})\n-        assert backend.captured[0][1][\"page\"] == 2\n-        assert backend.captured[0][1][\"page_size\"] == 50\n-\n-    def test_list_empty(self, monkeypatch):\n-        backend = FakeBackend()\n-        provider = self._make_provider(monkeypatch, backend)\n-        result = json.loads(provider.handle_tool_call(\"mem0_list\", {}))\n-        assert result[\"result\"] == \"No memories stored yet.\"\n-\n     def test_search_returns_ids(self, monkeypatch):\n         backend = FakeBackend(search_results=[{\"id\": \"mem-1\", \"memory\": \"foo\", \"score\": 0.9}])\n         provider = self._make_provider(monkeypatch, backend)","@@ -92,11 +65,11 @@ def test_search_uses_filters(self, monkeypatch):\n         assert backend.captured[0][2][\"filters\"] == {\"user_id\": \"u123\"}\n         assert backend.captured[0][2][\"top_k\"] == 3\n \n-    def test_search_rerank_default_true(self, monkeypatch):\n+    def test_search_rerank_default_false(self, monkeypatch):\n         backend = FakeBackend()\n         provider = self._make_provider(monkeypatch, backend)\n         provider.handle_tool_call(\"mem0_search\", {\"query\": \"test\"})\n-        assert backend.captured[0][2][\"rerank\"] is True\n+        assert backend.captured[0][2][\"rerank\"] is False\n \n     def test_search_rerank_override_false(self, monkeypatch):\n         backend = FakeBackend()","@@ -104,6 +77,15 @@ def test_search_rerank_override_false(self, monkeypatch):\n         provider.handle_tool_call(\"mem0_search\", {\"query\": \"test\", \"rerank\": False})\n         assert backend.captured[0][2][\"rerank\"] is False\n \n+    def test_search_rerank_config_default_used_when_arg_absent(self, monkeypatch):\n+        \"\"\"The persisted mem0.json ``rerank`` preference is the tool default;\n+        a per-call arg still wins (see the explicit-False override above).\"\"\"\n+        backend = FakeBackend()\n+        provider = self._make_provider(monkeypatch, backend)\n+        provider._rerank_default = True  # as initialize() sets from config\n+        provider.handle_tool_call(\"mem0_search\", {\"query\": \"test\"})\n+        assert backend.captured[0][2][\"rerank\"] is True\n+\n     def test_add_uses_content_param(self, monkeypatch):\n         backend = FakeBackend()\n         provider = self._make_provider(monkeypatch, backend)","@@ -280,6 +262,8 @@ def test_old_tool_names_return_unknown(self, monkeypatch):\n         assert \"error\" in result\n         result = json.loads(provider.handle_tool_call(\"mem0_conclude\", {}))\n         assert \"error\" in result\n+        result = json.loads(provider.handle_tool_call(\"mem0_list\", {}))\n+        assert \"error\" in result\n \n \n class TestMem0Prefetch:","@@ -308,7 +292,7 @@ def test_prefetch_searches_current_query(self):\n         assert query == \"what theme do I like?\"\n         assert opts[\"filters\"] == {\"user_id\": \"u123\"}\n         assert opts[\"top_k\"] == 10\n-        assert opts[\"rerank\"] is True\n+        assert opts[\"rerank\"] is False\n         assert \"## Mem0 Memory\" in result\n         assert \"user prefers dark mode\" in result\n ","@@ -368,11 +352,11 @@ def test_queue_prefetch_fires_no_search(self):\n \n class TestMem0V3Config:\n \n-    def test_tool_schemas_five_tools(self):\n+    def test_tool_schemas_four_tools(self):\n         provider = Mem0MemoryProvider()\n         schemas = provider.get_tool_schemas()\n         names = [s[\"name\"] for s in schemas]\n-        assert names == [\"mem0_list\", \"mem0_search\", \"mem0_add\", \"mem0_update\", \"mem0_delete\"]\n+        assert names == [\"mem0_search\", \"mem0_add\", \"mem0_update\", \"mem0_delete\"]\n \n     def test_system_prompt_new_tool_names(self):\n         provider = Mem0MemoryProvider()","@@ -380,9 +364,9 @@ def test_system_prompt_new_tool_names(self):\n         block = provider.system_prompt_block()\n         assert \"mem0_search\" in block\n         assert \"mem0_add\" in block\n-        assert \"mem0_list\" in block\n         assert \"mem0_update\" in block\n         assert \"mem0_delete\" in block\n+        assert \"mem0_list\" not in block\n         assert \"mem0_profile\" not in block\n         assert \"mem0_conclude\" not in block\n ","@@ -455,7 +439,7 @@ def test_tool_schemas_unchanged(self):\n         provider = Mem0MemoryProvider()\n         schemas = provider.get_tool_schemas()\n         names = [s[\"name\"] for s in schemas]\n-        assert names == [\"mem0_list\", \"mem0_search\", \"mem0_add\", \"mem0_update\", \"mem0_delete\"]\n+        assert names == [\"mem0_search\", \"mem0_add\", \"mem0_update\", \"mem0_delete\"]\n \n     def test_system_prompt_includes_mode(self):\n         provider = Mem0MemoryProvider()","@@ -463,7 +447,6 @@ def test_system_prompt_includes_mode(self):\n         provider._mode = \"oss\"\n         block = provider.system_prompt_block()\n         assert \"mem0_search\" in block\n-        assert \"mem0_list\" in block\n         assert \"OSS\" in block\n \n ","@@ -547,3 +530,92 @@ def test_sync_turn_passes_channel_metadata(self):\n         adds = [c for c in provider._backend.captured if c[0] == \"add\"]\n         assert adds, \"expected an add call from sync_turn\"\n         assert adds[-1][2][\"metadata\"] == {\"channel\": \"discord\"}\n+\n+\n+class _SentinelBackend:\n+    def __init__(self, *args):\n+        self.args = args\n+\n+\n+class TestCreateBackendRouting:\n+    \"\"\"_create_backend() must pick the backend matching the configured mode/host.\"\"\"\n+\n+    def _provider(self, monkeypatch, *, mode=\"platform\", api_key=\"k\", host=\"\"):\n+        # Neutralize lazy-install so the routing decision is all we exercise.\n+        monkeypatch.setattr(\"tools.lazy_deps.ensure\", lambda *a, **k: None, raising=False)\n+        provider = Mem0MemoryProvider()\n+        provider._mode = mode\n+        provider._api_key = [redacted:secret-assignment]\n+        provider._host = host\n+        provider._config = {\"oss\": {\"vector_store\": {\"provider\": \"qdrant\"}}}\n+        return provider\n+\n+    def test_routes_to_selfhosted_when_host_set(self, monkeypatch):\n+        captured = {}\n+\n+        class SH(_SentinelBackend):\n+            def __init__(self, api_key, host):\n+                captured[\"args\"] = (api_key, host)\n+\n+        monkeypatch.setattr(\"plugins.memory.mem0._backend.SelfHostedBackend\", SH)\n+        provider = self._provider(monkeypatch, host=\"http://sh:8888\", api_key=\"[redacted:secret-assignment]\")\n+        backend = provider._create_backend()\n+        assert isinstance(backend, SH)\n+        assert captured[\"args\"] == (\"adminkey\", \"http://sh:8888\")\n+\n+    def test_routes_to_platform_when_no_host(self, monkeypatch):\n+        class PB(_SentinelBackend):\n+            def __init__(self, api_key):\n+                pass\n+\n+        monkeypatch.setattr(\"plugins.memory.mem0._backend.PlatformBackend\", PB)\n+        provider = self._provider(monkeypatch, host=\"\")\n+        assert isinstance(provider._create_backend(), PB)\n+\n+    def test_routes_to_oss_when_mode_oss(self, monkeypatch):\n+        class OB(_SentinelBackend):\n+            def __init__(self, cfg):\n+                pass\n+\n+        monkeypatch.setattr(\"plugins.memory.mem0._backend.OSSBackend\", OB)\n+        provider = self._provider(monkeypatch, mode=\"oss\")\n+        assert isinstance(provider._create_backend(), OB)\n+\n+    def test_oss_mode_takes_precedence_over_host(self, monkeypatch):\n+        class OB(_SentinelBackend):\n+            def __init__(self, cfg):\n+                pass\n+\n+        monkeypatch.setattr(\"plugins.memory.mem0._backend.OSSBackend\", OB)\n+        provider = self._provider(monkeypatch, mode=\"oss\", host=\"http://sh:8888\")\n+        assert isinstance(provider._create_backend(), OB)\n+\n+    def test_prompt_label_matches_routing_when_oss_and_host_both_set(self, monkeypatch):\n+        # system_prompt_block must mirror _create_backend precedence: with both\n+        # mode=oss and host set, OSS wins the routing, so the prompt must label\n+        # OSS — not \"self-hosted (HTTP API)\". Guards the prompt-vs-routing lie.\n+        provider = self._provider(monkeypatch, mode=\"oss\", host=\"http://sh:8888\")\n+        provider._user_id = \"test\"\n+        block = provider.system_prompt_block()\n+        assert \"OSS\" in block\n+        assert \"HTTP API\" not in block\n+\n+\n+class TestSelfHostedConfig:\n+    \"\"\"Config plumbing for self-hosted (MEM0_HOST env + is_available).\"\"\"\n+\n+    def test_load_config_reads_mem0_host_env(self, monkeypatch):\n+        monkeypatch.setenv(\"MEM0_HOST\", \"http://localhost:8888\")\n+        assert mem0_plugin._load_config()[\"host\"] == \"http://localhost:8888\"\n+\n+    def test_is_available_true_with_host_only(self, monkeypatch):\n+        monkeypatch.delenv(\"MEM0_API_KEY\", raising=False)\n+        monkeypatch.setenv(\"MEM0_MODE\", \"platform\")\n+        monkeypatch.setenv(\"MEM0_HOST\", \"http://localhost:8888\")\n+        assert Mem0MemoryProvider().is_available() is True\n+\n+    def test_is_available_false_without_key_or_host(self, monkeypatch):\n+        monkeypatch.delenv(\"MEM0_API_KEY\", raising=False)\n+        monkeypatch.delenv(\"MEM0_HOST\", raising=False)\n+        monkeypatch.setenv(\"MEM0_MODE\", \"platform\")\n+        assert Mem0MemoryProvider().is_available() is False"]},{"file":"website/docs/user-guide/features/memory-providers.md","hunks":["@@ -317,16 +317,16 @@ echo \"OPENVIKING_API_KEY=...\" >> ~/.hermes/.env\n \n ### Mem0\n \n-Server-side LLM fact extraction with semantic search, reranking, and automatic deduplication. Supports both Mem0 Platform (cloud) and OSS (self-hosted) modes.\n+Server-side LLM fact extraction with semantic search, reranking, and automatic deduplication. Three connection modes: **Platform** (Mem0 Cloud), **self-hosted dashboard** (a Mem0 server you run via Docker), and **OSS** (Mem0 in-process with your own LLM + vector store).\n \n | | |\n |---|---|\n | **Best for** | Hands-off memory management — Mem0 handles extraction automatically |\n-| **Requires** | `pip install mem0ai` + API key (platform) or LLM/vector store (OSS) |\n-| **Data storage** | Mem0 Cloud (platform) or self-hosted (OSS) |\n-| **Cost** | Mem0 pricing (platform) / free (OSS) |\n+| **Requires** | `pip install mem0ai` + API key (platform), a running Mem0 server (self-hosted dashboard), or an LLM + vector store (OSS) |\n+| **Data storage** | Mem0 Cloud (platform), your own Mem0 server (self-hosted dashboard), or in-process (OSS) |\n+| **Cost** | Mem0 pricing (platform) / free (self-hosted or OSS) |\n \n-**Tools (5):** `mem0_list` (list all memories, paginated), `mem0_search` (semantic search with reranking in platform mode), `mem0_add` (store verbatim facts), `mem0_update` (update by ID), `mem0_delete` (delete by ID)\n+**Tools (4):** `mem0_search` (semantic search; optional reranking in platform mode, off by default), `mem0_add` (store verbatim facts), `mem0_update` (update by ID), `mem0_delete` (delete by ID)\n \n **Setup (Platform):**\n ```bash","@@ -348,14 +348,30 @@ Preview without writing files:\n hermes memory setup mem0 --mode oss --oss-llm-key sk-... --dry-run\n ```\n \n+**Setup (Self-Hosted Dashboard):** connect to a Mem0 server you run via Docker (the dashboard's REST API). Set `host` and an API key — either as env vars:\n+\n+```bash\n+echo \"MEM0_HOST=http://localhost:8888\" >> ~/.hermes/.env\n+echo \"MEM0_API_KEY=[redacted:secret-assignment]\" >> ~/.hermes/.env\n+```\n+\n+or in `mem0.json`:\n+\n+```json\n+{ \"host\": \"http://localhost:8888\", \"api_key\": \"[redacted:secret-assignment]\" }\n+```\n+\n+The plugin authenticates with `X-API-Key` and uses the server's `/search` / `/memories` routes. `api_key` is optional (omit only for `AUTH_DISABLED` servers). Don't set `mode: oss` — it takes precedence over `host`.\n+\n **Config:** `$HERMES_HOME/mem0.json` (behavioral settings). Only the secret `MEM0_API_KEY` belongs in `~/.hermes/.env`.\n \n | Key | Default | Description |\n |-----|---------|-------------|\n-| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-hosted) |\n+| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-managed, in-process) |\n+| `host` | — | Self-hosted Mem0 server URL (Docker dashboard). Routes over HTTP with `X-API-Key`; don't combine with `mode: oss` |\n | `user_id` | `hermes-user` | User identifier |\n | `agent_id` | `hermes` | Agent identifier |\n-| `rerank` | `true` | Rerank search results for relevance (platform mode only) |\n+| `rerank` | `false` | Rerank search results for relevance (platform mode only) |\n \n **OSS supported providers:**\n ","@@ -595,7 +611,7 @@ hermes memory setup\n |----------|---------|------|-------|-------------|----------------|\n | **Honcho** | Cloud | Paid | 5 | `honcho-ai` | Dialectic user modeling + session-scoped context |\n | **OpenViking** | Self-hosted | Free | 5 | `openviking` + server | Filesystem hierarchy + tiered loading |\n-| **Mem0** | Cloud/Self-hosted | Free/Paid | 5 | `mem0ai` | Server-side LLM extraction + OSS mode |\n+| **Mem0** | Cloud/Self-hosted | Free/Paid | 4 | `mem0ai` | Server-side LLM extraction + self-hosted/OSS modes |\n | **Hindsight** | Cloud/Local | Free/Paid | 3 | `hindsight-client` | Knowledge graph + reflect synthesis |\n | **Holographic** | Local | Free | 2 | None | HRR algebra + trust scoring |\n | **RetainDB** | Cloud | $20/mo | 5 | `requests` | Delta compression |\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}