{"repo":"NousResearch/hermes-agent","public_id":"16401","display_label":"Mem0 self-hosted/OSS mode support efforts","summary":"Multiple issues and pull requests addressing the addition or restoration of self-hosted (OSS) Mem0 support, including host configuration, v3 API migration, and related tooling updates. The group reflects ongoing, overlapping work toward enabling a remote or locally-hosted Mem0 endpoint.","subsystem":"agent","counts":{"members":10,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":4,"merged_prs":3,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":50479},"resolution":{"state":"partially_resolved","by":"pr-56943"},"freshness":{"state":"verified","last_verified_at":"2026-07-07T21:07:16+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.16533333333333333},"fix_edges":[{"issue_number":16401,"pr_number":50479,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":52478,"pr_number":56943,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":52478,"pr_number":60494,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":52921,"pr_number":56943,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":16401,"title":"[Feature]: feat(mem0) add OSS mode, v3 API migration, update/delete tools","url":null,"state":"closed"},{"kind":"issue","number":31135,"title":"[Feature]: Proposal: Self-Hosted Mem0 as Memory Provider in Hermes","url":null,"state":"closed"},{"kind":"issue","number":51894,"title":"[Feature]: Add support to use a remote Mem0 API Endpoint (self-hosted)","url":null,"state":"closed"},{"kind":"issue","number":52478,"title":"Bug: mem0 plugin dropped host/self-hosted support after _backend.py refactor (#15624)","url":null,"state":"closed"},{"kind":"pr","number":31209,"title":"Enhance Mem0 memory provider with self-hosting support","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#50479","number":50479},{"kind":"pr","value":"#13377","number":13377}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":50479,"title":"fix(mem0): add self-hosted support via MEM0_HOST / host config","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/mem0/README.md","plugins/memory/mem0/__init__.py","scripts/release.py"],"fix_hunks":[{"file":"plugins/memory/mem0/README.md","hunks":["@@ -2,30 +2,45 @@\n \n Server-side LLM fact extraction with semantic search, reranking, and automatic deduplication.\n \n+Supports both [Mem0 Cloud](https://app.mem0.ai) and self-hosted instances.\n+\n ## Requirements\n \n - `pip install mem0ai`\n-- Mem0 API key from [app.mem0.ai](https://app.mem0.ai)\n+- Mem0 Cloud API key **or** a self-hosted Mem0 server\n \n ## Setup\n \n+### Cloud\n+\n ```bash\n hermes memory setup    # select \"mem0\"\n ```\n \n Or manually:\n+\n ```bash\n hermes config set memory.provider mem0\n echo \"MEM0_API_KEY=[redacted:secret-assignment]\" >> ~/.hermes/.env\n ```\n \n+### Self-Hosted\n+\n+```bash\n+hermes config set memory.provider mem0\n+echo \"MEM0_HOST=http://your-mem0-server:24220\" >> ~/.hermes/.env\n+echo \"MEM0_API_KEY=[redacted:secret-assignment]\" >> ~/.hermes/.env   # if auth is enabled\n+```\n+\n ## Config\n \n Config file: `$HERMES_HOME/mem0.json`\n \n | Key | Default | Description |\n |-----|---------|-------------|\n-| `user_id` | `hermes-user` | User identifier on Mem0 |\n+| `api_key` | — | API key (required for cloud; optional for self-hosted without auth) |\n+| `host` | `https://api.mem0.ai` | Self-hosted Mem0 URL. When set, overrides the cloud endpoint. |\n+| `user_id` | `hermes-user` | User identifier |\n | `agent_id` | `hermes` | Agent identifier |\n | `rerank` | `true` | Enable reranking for recall |\n "]},{"file":"plugins/memory/mem0/__init__.py","hunks":["@@ -1,12 +1,13 @@\n \"\"\"Mem0 memory plugin — MemoryProvider interface.\n \n Server-side LLM fact extraction, semantic search with reranking, and\n-automatic deduplication via the Mem0 Platform API.\n+automatic deduplication via the Mem0 Platform API or self-hosted instance.\n \n Original PR #2933 by kartik-mem0, adapted to MemoryProvider ABC.\n \n Config via environment variables:\n-  MEM0_API_KEY       — Mem0 Platform API key (required)\n+  MEM0_API_KEY       — Mem0 API key (required for cloud, optional for self-hosted)\n+  MEM0_HOST          — Self-hosted Mem0 URL (default: https://api.mem0.ai)\n   MEM0_USER_ID       — User identifier (default: hermes-user)\n   MEM0_AGENT_ID      — Agent identifier (default: hermes)\n ","@@ -37,6 +38,7 @@\n # Config\n # ---------------------------------------------------------------------------\n \n+\n def _load_config() -> dict:\n     \"\"\"Load config from env vars, with $HERMES_HOME/mem0.json overrides.\n ","@@ -48,6 +50,7 @@ def _load_config() -> dict:\n \n     config = {\n         \"api_key\": os.environ.get(\"MEM0_API_KEY\", \"\"),\n+        \"host\": os.environ.get(\"MEM0_HOST\", \"\"),\n         \"user_id\": os.environ.get(\"MEM0_USER_ID\", \"hermes-user\"),\n         \"agent_id\": os.environ.get(\"MEM0_AGENT_ID\", \"hermes\"),\n         \"rerank\": True,","@@ -117,13 +120,18 @@ def _load_config() -> dict:\n # ---------------------------------------------------------------------------\n \n class Mem0MemoryProvider(MemoryProvider):\n-    \"\"\"Mem0 Platform memory with server-side extraction and semantic search.\"\"\"\n+    \"\"\"Mem0 memory with server-side extraction and semantic search.\n+\n+    Supports both Mem0 Cloud (api.mem0.ai) and self-hosted instances\n+    via the ``host`` config key or ``MEM0_HOST`` env var.\n+    \"\"\"\n \n     def __init__(self):\n         self._config = None\n         self._client = None\n         self._client_lock = threading.Lock()\n         self._api_key = \"\"\n+        self._host = \"\"\n         self._user_id = \"hermes-user\"\n         self._agent_id = \"hermes\"\n         self._rerank = True","@@ -141,7 +149,9 @@ def name(self) -> str:\n \n     def is_available(self) -> bool:\n         cfg = _load_config()\n-        return bool(cfg.get(\"api_key\"))\n+        host = cfg.get(\"host\", \"\")\n+        api_key = [redacted:secret-assignment]\"api_key\", \"\")\n+        return bool(host) or bool(api_key)\n \n     def save_config(self, values, hermes_home):\n         \"\"\"Write config to $HERMES_HOME/mem0.json.\"\"\"","@@ -160,7 +170,8 @@ def save_config(self, values, hermes_home):\n \n     def get_config_schema(self):\n         return [\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\": \"api_key\", \"description\": \"Mem0 API key (cloud or self-hosted)\", \"secret\": True, \"required\": True, \"env_var\": \"MEM0_API_KEY\", \"url\": \"https://app.mem0.ai\"},\n+            {\"key\": \"host\", \"description\": \"Self-hosted Mem0 URL (e.g. http://localhost:24220)\", \"default\": \"\", \"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\"]},","@@ -173,7 +184,14 @@ def _get_client(self):\n                 return self._client\n             try:\n                 from mem0 import MemoryClient\n-                self._client = MemoryClient(api_key=[redacted:secret-assignment]\n+                kwargs = {}\n+                if self._host:\n+                    kwargs[\"host\"] = self._host\n+                if self._api_key:\n+                    kwargs[\"api_key\"] = self._api_key\n+                elif not self._host:\n+                    raise ValueError(\"Mem0: either api_key or host is required\")\n+                self._client = MemoryClient(**kwargs)\n                 return self._client\n             except ImportError:\n                 raise RuntimeError(\"mem0 package not installed. Run: pip install mem0ai\")","@@ -204,6 +222,7 @@ def _record_failure(self):\n     def initialize(self, session_id: str, **kwargs) -> None:\n         self._config = _load_config()\n         self._api_key = [redacted:secret-assignment]\"api_key\", \"\")\n+        self._host = self._config.get(\"host\", \"\")\n         # Prefer gateway-provided user_id for per-user memory scoping;\n         # fall back to config/env default for CLI (single-user) sessions.\n         self._user_id = kwargs.get(\"user_id\") or self._config.get(\"user_id\", \"hermes-user\")","@@ -228,8 +247,9 @@ def _unwrap_results(response: Any) -> list:\n         return []\n \n     def system_prompt_block(self) -> str:\n+        target = self._host or \"cloud\"\n         return (\n-            \"# Mem0 Memory\\n\"\n+            f\"# Mem0 Memory ({target})\\n\"\n             f\"Active. User: {self._user_id}.\\n\"\n             \"Use mem0_search to find memories, mem0_conclude to store facts, \"\n             \"mem0_profile for a full overview.\""]},{"file":"scripts/release.py","hunks":["@@ -123,6 +123,7 @@\n     \"[redacted:email]\": \"synapsesx\",\n     \"[redacted:email]\": \"itsflownium\",\n     \"[redacted:email]\": \"dirtyren\",\n+    \"[redacted:email]\": \"buihongduc132\",\n     \"[redacted:email]\": \"EtherAura\",  # PR #45205 salvage (Linux in-app update relaunch / GUI-skew terminal state)\n     \"[redacted:email]\": \"devorun\",\n     \"[redacted:email]\": \"x7peeps\",\n"]}]},{"kind":"pr","number":52487,"title":"fix(mem0): restore self-hosted support dropped by v3 refactor","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#56943","number":56943},{"kind":"pr","value":"#55614","number":55614},{"kind":"pr","value":"#60494","number":60494},{"kind":"pr","value":"#56943","number":56943},{"kind":"pr","value":"#60494","number":60494}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":55614,"title":"feat(mem0): add self-hosted dashboard support and remove list command from the plugin","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"2a14205ff","number":0},{"kind":"commit","value":"53edf6f98","number":0},{"kind":"commit","value":"9a322726a","number":0},{"kind":"pr","value":"#56943","number":56943},{"kind":"pr","value":"#60494","number":60494}],"category":"merged_via","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","implication":"superseded"},"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\": \"your-admin-api-key\"\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 = [redacted:secret-assignment]\"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\", \"sk-test\"])\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\": \"your-admin-api-key\" }\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"]}]},{"kind":"pr","number":60494,"title":"feat(mem0): add self-hosted mode to the setup wizard","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/mem0/README.md","plugins/memory/mem0/_setup.py","tests/plugins/memory/test_mem0_setup.py","website/docs/user-guide/features/memory-providers.md"],"fix_hunks":[{"file":"plugins/memory/mem0/README.md","hunks":["@@ -42,7 +42,13 @@ The plugin has three connection modes:\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+2. Point the plugin at it — via the setup wizard:\n+   ```bash\n+   hermes memory setup    # select \"mem0\" → \"Self-hosted server\"\n+   # Or non-interactive:\n+   hermes memory setup mem0 --mode selfhosted --host http://localhost:8888 --api-key your-admin-api-key\n+   ```\n+   or via env vars:\n    ```bash\n    echo \"MEM0_HOST=http://localhost:8888\" >> ~/.hermes/.env\n    echo \"MEM0_API_KEY=[redacted:secret-assignment]\" >> ~/.hermes/.env"]},{"file":"plugins/memory/mem0/_setup.py","hunks":["@@ -67,6 +67,7 @@ def parse_flags(argv: list[str] | None = None) -> dict[str, str]:\n     flags: dict[str, str] = {\n         \"mode\": \"\",\n         \"api_key\": \"\",\n+        \"host\": \"\",\n         \"oss_llm\": \"openai\",\n         \"oss_llm_key\": \"\",\n         \"oss_llm_model\": \"\",","@@ -90,6 +91,7 @@ def parse_flags(argv: list[str] | None = None) -> dict[str, str]:\n     flag_map = {\n         \"--mode\": \"mode\",\n         \"--api-key\": \"api_key\",\n+        \"--host\": \"host\",\n         \"--oss-llm\": \"oss_llm\",\n         \"--oss-llm-key\": \"oss_llm_key\",\n         \"--oss-llm-model\": \"oss_llm_model\",","@@ -331,6 +333,102 @@ def _setup_platform(hermes_home: str, config: dict, flags: dict[str, str]) -> No\n     print(\"\\n  Start a new session to activate.\\n\")\n \n \n+def _check_selfhosted_server(host: str) -> None:\n+    \"\"\"Best-effort reachability check for a self-hosted Mem0 server (non-fatal).\"\"\"\n+    import urllib.error\n+    import urllib.request as _urlreq\n+\n+    try:\n+        req = _urlreq.Request(f\"{host.rstrip('/')}/docs\", method=\"GET\")\n+        _urlreq.urlopen(req, timeout=5)\n+        print(f\"  ✓ Mem0 server reachable at {host}\")\n+    except urllib.error.HTTPError:\n+        # Any HTTP response (401/403/404) still means something is listening.\n+        print(f\"  ✓ Mem0 server responding at {host}\")\n+    except Exception:\n+        print(f\"  ⚠ Could not reach {host} — check the URL and that the server is running.\")\n+\n+\n+def _setup_selfhosted(hermes_home: str, config: dict, flags: dict[str, str]) -> None:\n+    \"\"\"Self-hosted mode setup — point at an existing Mem0 dashboard server.\n+\n+    For users already running the Dockerized Mem0 FastAPI server: stores the\n+    server URL (behavioral -> mem0.json) and an optional API key\n+    (secret -> .env as MEM0_API_KEY).\n+    \"\"\"\n+    existing_config = {}\n+    config_path = Path(hermes_home) / \"mem0.json\"\n+    if config_path.exists():\n+        try:\n+            existing_config = json.loads(config_path.read_text())\n+        except Exception:\n+            pass\n+\n+    provider_config = dict(existing_config)\n+\n+    print(\"\\n  Configuring mem0 (self-hosted server):\\n\")\n+\n+    host = flags.get(\"host\") or _prompt(\n+        \"Mem0 server URL (e.g. http://localhost:8888)\",\n+        default=provider_config.get(\"host\") or None,\n+    )\n+    if not host:\n+        print(\"  Error: a server URL is required for self-hosted mode.\", file=sys.stderr)\n+        return\n+    host = host.rstrip(\"/\")\n+\n+    env_writes: dict[str, str] = {}\n+    if flags.get(\"api_key\"):\n+        env_writes[\"MEM0_API_KEY\"] = flags[\"api_key\"]\n+    else:\n+        existing_key = os.environ.get(\"MEM0_API_KEY\", \"\")\n+        if existing_key:\n+            masked = f\"...{existing_key[-4:]}\" if len(existing_key) > 4 else \"set\"\n+            val = _prompt(f\"Server API key (current: {masked}, blank to keep)\", secret=True)\n+        else:\n+            val = _prompt(\"Server API key (blank if AUTH_DISABLED)\", secret=True)\n+        if val:\n+            env_writes[\"MEM0_API_KEY\"] = val\n+\n+    user_id = flags.get(\"user_id\") or _prompt(\n+        \"User identifier\", default=provider_config.get(\"user_id\") or \"hermes-user\"\n+    )\n+    agent_id = _prompt(\"Agent identifier\", default=provider_config.get(\"agent_id\") or \"hermes\")\n+\n+    if flags.get(\"dry_run\"):\n+        print(f\"\\n  [dry-run] Would save config: host={host}, user_id={user_id}, agent_id={agent_id}\")\n+        if env_writes:\n+            print(\"  [dry-run] Would write API key to .env\")\n+        _check_selfhosted_server(host)\n+        print(\"  [dry-run] No files written.\\n\")\n+        return\n+\n+    provider_config[\"mode\"] = \"platform\"  # routing: oss > host > platform; host wins\n+    provider_config[\"host\"] = host\n+    provider_config[\"user_id\"] = user_id\n+    provider_config[\"agent_id\"] = agent_id\n+\n+    from hermes_cli.config import save_config\n+    config[\"memory\"][\"provider\"] = \"mem0\"\n+    save_config(config)\n+\n+    from plugins.memory.mem0 import Mem0MemoryProvider\n+    provider = Mem0MemoryProvider()\n+    provider.save_config(provider_config, hermes_home)\n+\n+    if env_writes:\n+        _write_env(Path(hermes_home) / \".env\", env_writes)\n+\n+    _check_selfhosted_server(host)\n+    print(\"\\n  Memory provider: mem0 (self-hosted)\")\n+    print(f\"  Server: {host}\")\n+    print(\"  Activation saved to config.yaml\")\n+    print(\"  Provider config saved\")\n+    if env_writes:\n+        print(\"  API key saved to .env\")\n+    print(\"\\n  Start a new session to activate.\\n\")\n+\n+\n def _setup_oss(hermes_home: str, config: dict, flags: dict[str, str]) -> None:\n     \"\"\"OSS mode setup — build config from flags or interactive prompts.\n ","@@ -846,10 +944,10 @@ def _check_min_dep_version() -> None:\n def post_setup(hermes_home: str, config: dict) -> None:\n     \"\"\"Entry point called by hermes memory setup framework.\n \n-    Only intercepts when OSS mode is requested (via --mode oss flag or\n-    interactive picker). For platform mode, returns without action so the\n-    framework's schema-based flow handles it (preserving the original\n-    platform onboarding experience).\n+    Routes on --mode (platform / selfhosted / oss); with no flag it shows an\n+    interactive picker with all three modes. Platform keeps the framework's\n+    original schema-based onboarding; selfhosted points at an existing Mem0\n+    server; oss builds a local SDK config.\n     \"\"\"\n     _check_min_dep_version()\n     flags = parse_flags(sys.argv[1:])","@@ -859,6 +957,10 @@ def post_setup(hermes_home: str, config: dict) -> None:\n         _setup_oss(hermes_home, config, flags)\n         return\n \n+    if flags[\"mode\"] in (\"selfhosted\", \"self-hosted\"):\n+        _setup_selfhosted(hermes_home, config, flags)\n+        return\n+\n     if flags[\"mode\"] == \"platform\":\n         _setup_platform(hermes_home, config, flags)\n         return","@@ -866,10 +968,13 @@ def post_setup(hermes_home: str, config: dict) -> None:\n     # No --mode flag: show interactive picker\n     mode_items = [\n         (\"Platform\", \"Mem0 Cloud API (lightweight, just needs an API key)\"),\n+        (\"Self-hosted server\", \"Connect to an existing self-hosted Mem0 server (Docker/FastAPI)\"),\n         (\"Open Source\", \"Run Mem0 locally (self-hosted LLM + vector store)\"),\n     ]\n     mode_idx = _curses_select(\"  Select mode\", mode_items, 0)\n     if mode_idx == 1:\n+        _setup_selfhosted(hermes_home, config, flags)\n+    elif mode_idx == 2:\n         flags[\"_mode_from_flag\"] = False\n         _setup_oss(hermes_home, config, flags)\n     else:"]},{"file":"tests/plugins/memory/test_mem0_setup.py","hunks":["@@ -209,6 +209,52 @@ def test_oss_flag_mode(self, tmp_path, monkeypatch):\n         assert mem0_json[\"mode\"] == \"oss\"\n         assert mem0_json[\"oss\"][\"llm\"][\"provider\"] == \"openai\"\n \n+    def test_selfhosted_flag_mode(self, tmp_path, monkeypatch):\n+        monkeypatch.setattr(\"sys.argv\", [\n+            \"hermes\", \"--mode\", \"selfhosted\",\n+            \"--host\", \"http://localhost:8888/\", \"--api-key\", \"admin-key\",\n+        ])\n+        monkeypatch.setattr(\"plugins.memory.mem0._setup.get_hermes_home\", lambda: tmp_path)\n+        _inject_fake_hermes_cli(monkeypatch)\n+        monkeypatch.setattr(\"plugins.memory.mem0._setup._check_selfhosted_server\", lambda h: None)\n+        config = {\"memory\": {}}\n+        post_setup(str(tmp_path), config)\n+        assert config[\"memory\"][\"provider\"] == \"mem0\"\n+        env_content = (tmp_path / \".env\").read_text()\n+        assert \"MEM0_API_KEY=[redacted:secret-assignment]\" in env_content\n+        mem0_json = json.loads((tmp_path / \"mem0.json\").read_text())\n+        assert mem0_json[\"host\"] == \"http://localhost:8888\"  # trailing slash stripped\n+        assert mem0_json[\"user_id\"] == \"hermes-user\"\n+\n+    def test_selfhosted_no_api_key_auth_disabled(self, tmp_path, monkeypatch):\n+        # AUTH_DISABLED servers need no key — setup must not write one.\n+        monkeypatch.setattr(\"sys.argv\", [\n+            \"hermes\", \"--mode\", \"self-hosted\", \"--host\", \"http://mem0.lan:8888\",\n+        ])\n+        monkeypatch.setattr(\"plugins.memory.mem0._setup.get_hermes_home\", lambda: tmp_path)\n+        monkeypatch.delenv(\"MEM0_API_KEY\", raising=False)\n+        _inject_fake_hermes_cli(monkeypatch)\n+        monkeypatch.setattr(\"plugins.memory.mem0._setup._check_selfhosted_server\", lambda h: None)\n+        config = {\"memory\": {}}\n+        post_setup(str(tmp_path), config)\n+        assert not (tmp_path / \".env\").exists()\n+        mem0_json = json.loads((tmp_path / \"mem0.json\").read_text())\n+        assert mem0_json[\"host\"] == \"http://mem0.lan:8888\"\n+\n+    def test_selfhosted_dry_run_no_files(self, tmp_path, monkeypatch):\n+        monkeypatch.setattr(\"sys.argv\", [\n+            \"hermes\", \"--mode\", \"selfhosted\",\n+            \"--host\", \"http://localhost:8888\", \"--api-key\", \"k\", \"--dry-run\",\n+        ])\n+        monkeypatch.setattr(\"plugins.memory.mem0._setup.get_hermes_home\", lambda: tmp_path)\n+        _inject_fake_hermes_cli(monkeypatch)\n+        monkeypatch.setattr(\"plugins.memory.mem0._setup._check_selfhosted_server\", lambda h: None)\n+        config = {\"memory\": {}}\n+        post_setup(str(tmp_path), config)\n+        assert not (tmp_path / \".env\").exists()\n+        assert not (tmp_path / \"mem0.json\").exists()\n+        assert \"provider\" not in config[\"memory\"]\n+\n \n class TestDryRun:\n "]},{"file":"website/docs/user-guide/features/memory-providers.md","hunks":["@@ -348,7 +348,15 @@ 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+**Setup (Self-Hosted Dashboard):** connect to a Mem0 server you run via Docker (the dashboard's REST API):\n+\n+```bash\n+hermes memory setup    # select \"mem0\" → \"Self-hosted server\"\n+# Or via flags:\n+hermes memory setup mem0 --mode selfhosted --host http://localhost:8888 --api-key your-admin-api-key\n+```\n+\n+Or configure manually — either as env vars:\n \n ```bash\n echo \"MEM0_HOST=http://localhost:8888\" >> ~/.hermes/.env","@@ -381,7 +389,7 @@ The plugin authenticates with `X-API-Key` and uses the server's `/search` / `/me\n | Embedder | openai, ollama |\n | Vector Store | qdrant (local/server), pgvector |\n \n-**Switching modes:** Re-run `hermes memory setup mem0 --mode <platform|oss>` or edit `mem0.json` directly.\n+**Switching modes:** Re-run `hermes memory setup mem0 --mode <platform|selfhosted|oss>` or edit `mem0.json` directly.\n \n ---\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}