From 1ad8a59b0c992e177025ff40c6229a1559f38d11 Mon Sep 17 00:00:00 2001 From: Rakhee Singh Date: Wed, 8 Apr 2026 19:21:11 +0530 Subject: [PATCH 1/5] fix(deepseek): forward response_format to OpenAI-compatible API (#4688) From 3ffea554bc67d26efc6d96094005e58ac74d3d1f Mon Sep 17 00:00:00 2001 From: Rakhee Singh Date: Wed, 8 Apr 2026 19:22:02 +0530 Subject: [PATCH 2/5] fix(azure_openai): forward response_format to Azure OpenAI API (#4689) --- mem0/llms/azure_openai.py | 2 ++ tests/llms/test_azure_openai.py | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/mem0/llms/azure_openai.py b/mem0/llms/azure_openai.py index 3abd7de49..7a088a17c 100644 --- a/mem0/llms/azure_openai.py +++ b/mem0/llms/azure_openai.py @@ -134,6 +134,8 @@ class AzureOpenAILLM(LLMBase): "messages": messages, }) + if response_format: + params["response_format"] = response_format if tools: params["tools"] = tools params["tool_choice"] = tool_choice diff --git a/tests/llms/test_azure_openai.py b/tests/llms/test_azure_openai.py index c10dc1635..ee6c01d7c 100644 --- a/tests/llms/test_azure_openai.py +++ b/tests/llms/test_azure_openai.py @@ -91,6 +91,50 @@ def test_generate_response_with_tools(mock_openai_client): assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."} +def test_generate_response_with_response_format(mock_openai_client): + config = AzureOpenAIConfig(model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, top_p=TOP_P) + llm = AzureOpenAILLM(config) + messages = [ + {"role": "system", "content": "You are a memory extraction assistant."}, + {"role": "user", "content": "I like hiking on weekends."}, + ] + + mock_response = Mock() + mock_response.choices = [Mock(message=Mock(content='{"facts": ["User likes hiking on weekends"]}'))] + mock_openai_client.chat.completions.create.return_value = mock_response + + response = llm.generate_response(messages, response_format={"type": "json_object"}) + + mock_openai_client.chat.completions.create.assert_called_once_with( + model=MODEL, + messages=messages, + temperature=TEMPERATURE, + max_tokens=MAX_TOKENS, + top_p=TOP_P, + response_format={"type": "json_object"}, + ) + assert response == '{"facts": ["User likes hiking on weekends"]}' + + +def test_generate_response_without_response_format(mock_openai_client): + config = AzureOpenAIConfig(model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, top_p=TOP_P) + llm = AzureOpenAILLM(config) + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Tell me a joke."}, + ] + + mock_response = Mock() + mock_response.choices = [Mock(message=Mock(content="Why did the chicken cross the road?"))] + mock_openai_client.chat.completions.create.return_value = mock_response + + response = llm.generate_response(messages) + + call_kwargs = mock_openai_client.chat.completions.create.call_args[1] + assert "response_format" not in call_kwargs + assert response == "Why did the chicken cross the road?" + + def test_reasoning_model_with_reasoning_effort(mock_openai_client): """Test that reasoning_effort is passed to the API for Azure reasoning models.""" config = AzureOpenAIConfig(model="o3-mini", reasoning_effort="low") From 2434b9d550160b49da4bf2ec6a4e7d43022ad6be Mon Sep 17 00:00:00 2001 From: Kartik Date: Wed, 8 Apr 2026 20:16:04 +0530 Subject: [PATCH 3/5] docs: add ChatDev integration guide and update integrations list (#4751) --- docs/docs.json | 3 +- docs/integrations.mdx | 7 + docs/integrations/chatdev.mdx | 243 ++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 docs/integrations/chatdev.mdx diff --git a/docs/docs.json b/docs/docs.json index afeaaa955..d8729fb8c 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -416,7 +416,8 @@ "integrations/openai-agents-sdk", "integrations/google-ai-adk", "integrations/mastra", - "integrations/vercel-ai-sdk" + "integrations/vercel-ai-sdk", + "integrations/chatdev" ] }, { diff --git a/docs/integrations.mdx b/docs/integrations.mdx index d519ba8d2..e07a8394c 100644 --- a/docs/integrations.mdx +++ b/docs/integrations.mdx @@ -409,4 +409,11 @@ Here are the available integrations for Mem0: > Use Mem0 with AWS Bedrock and OpenSearch Service for cloud-native persistent semantic memory storage. + + Add persistent cloud-managed memory to ChatDev multi-agent workflows with zero-code YAML configuration. + diff --git a/docs/integrations/chatdev.mdx b/docs/integrations/chatdev.mdx new file mode 100644 index 000000000..e6b0c42dc --- /dev/null +++ b/docs/integrations/chatdev.mdx @@ -0,0 +1,243 @@ +--- +title: ChatDev +description: "Add persistent, cloud-managed memory to ChatDev multi-agent workflows with Mem0 — no code required, just YAML configuration." +--- + +Build multi-agent workflows in [ChatDev](https://github.com/OpenBMB/ChatDev) with persistent memory powered by Mem0. ChatDev is a zero-code multi-agent platform where agents, tools, and workflows are defined entirely in YAML. Mem0 integrates as a built-in memory store (`type: mem0`), giving your agents cloud-managed semantic search and cross-session persistence — all without writing any code. + +## Overview + +In this guide, you'll: +1. Set up ChatDev with the Mem0 memory store +2. Configure agents with persistent memory using YAML +3. Enable automatic memory retrieval and storage across conversations +4. Leverage cross-session persistence for personalized multi-agent interactions + +## Prerequisites + +- **Python 3.12+** +- **[uv](https://docs.astral.sh/uv/)** — Python package manager +- **Node.js 18+** and **npm** — only needed if using the web console +- A **Mem0 API key** from [app.mem0.ai](https://app.mem0.ai) +- An **OpenAI API key** (or another LLM provider supported by ChatDev) + +## Setup and Configuration + +Install ChatDev and its dependencies (includes `mem0ai`): + +```bash +git clone https://github.com/OpenBMB/ChatDev.git +cd ChatDev +uv sync +``` + +If you plan to use the web console, also install the frontend: + +```bash +cd frontend && npm install && cd .. +``` + +Set up your environment variables in a `.env` file: + +Get your Mem0 API key from [Mem0 Platform](https://app.mem0.ai). + +```bash +MEM0_API_KEY=your-mem0-api-key +API_KEY=your-openai-api-key +BASE_URL=https://api.openai.com/v1 +``` + +## Configure Mem0 Memory Store + +In your ChatDev workflow YAML, add a Mem0 memory store in the `memory` section: + +```yaml +memory: + - name: mem0_store + type: mem0 + config: + api_key: ${MEM0_API_KEY} + user_id: my-user-123 # optional: scope memories to a user + agent_id: my-agent # optional: scope memories to an agent +``` + +Mem0 handles all storage, embeddings, and search server-side — no local vector databases or embedding models are needed. + +## Attach Memory to an Agent + +Reference the memory store in your agent node's `memories` list: + +```yaml +nodes: + - id: writer + type: agent + config: + role: | + You are a knowledgeable writer. Use your memories to build + on past interactions. + memories: + - name: mem0_store + top_k: 5 + similarity_threshold: 0.5 # minimum relevance score (0.0–1.0); set to -1.0 to disable + retrieve_stage: + - gen + read: true + write: true +``` + +- **`read: true`** — Agent retrieves relevant memories before generating a response +- **`write: true`** — Agent stores new memories from user input after each interaction +- **`top_k`** — Number of memories to retrieve per query +- **`similarity_threshold`** — Minimum relevance score for retrieved memories. Set to `-1.0` to return all results regardless of score +- **`retrieve_stage`** — When to retrieve memories. Options: `pre_gen_thinking` (before generation), `gen` (during generation), `post_gen_thinking` (after generation), `finished` (after completion) + +## Full Example Workflow + +Here's a complete workflow YAML that creates a memory-backed conversational agent: + +```yaml +version: 0.4.0 +graph: + description: Memory-backed conversation using Mem0 + + nodes: + - id: writer + type: agent + config: + base_url: ${BASE_URL} + api_key: ${API_KEY} + provider: openai + name: gpt-5.4 + role: | + You are a knowledgeable writer. Use your memories to build + on past interactions. If memory sections are provided + (wrapped by ===== Related Memories =====), incorporate + relevant context from those memories into your response. + params: + temperature: 0.7 + max_tokens: 2000 + memories: + - name: mem0_store + top_k: 5 + retrieve_stage: + - gen + read: true + write: true + + memory: + - name: mem0_store + type: mem0 + config: + api_key: ${MEM0_API_KEY} + user_id: project-user-123 + agent_id: writer-agent + + start: + - writer + end: [] +``` + +Run the workflow: + +```bash +# Option 1: CLI (recommended for quick testing) +uv run python run.py --path yaml_instance/demo_mem0_memory.yaml --name my_project + +# Option 2: Web console +make dev +# Backend starts at http://localhost:6400, frontend at http://localhost:5173 +``` + +To use the web console, open `http://localhost:5173`, create a new workflow, and paste your YAML configuration into the editor. The web console provides a visual chat interface for interacting with your memory-backed agents. + +## How It Works + +When an agent with Mem0 memory receives input, the following cycle runs automatically: + +**1. Retrieve** — Before generating a response, ChatDev queries Mem0 with the user's input using semantic search. Relevant memories are injected into the agent's context in this format: + +``` +===== Related Memories ===== +--- mem0_store --- +1. User's favorite language is Rust +2. User lives in San Francisco +===== End of Memory ===== +``` + +This is why the role prompt in the example references `===== Related Memories =====` — the agent needs to know how to use this injected context. + +**2. Generate** — The agent produces a response using the retrieved memories as additional context. + +**3. Store** — After generation, the user's input is sent to Mem0 via `client.add()`. Mem0's extraction model automatically identifies and stores facts, preferences, and key information. Only user input is stored — agent output is excluded to keep memories clean. + +Memories persist in Mem0's cloud across all sessions. The next time the same `user_id` or `agent_id` is used, previous memories are automatically retrieved. + +## Dual-Scope Memory (User + Agent) + +When both `user_id` and `agent_id` are configured, Mem0 uses an OR filter to search across both scopes in a single query: + +```yaml +memory: + - name: shared_store + type: mem0 + config: + api_key: ${MEM0_API_KEY} + user_id: alice # stores user preferences ("Alice prefers dark mode") + agent_id: support-bot # stores agent-learned context ("Resolved Alice's billing issue") +``` + +This means retrieval returns memories from **both** the user's scope and the agent's scope. Writes include both IDs, so each memory is accessible from either dimension. Use this when you want an agent to remember both what the user told it *and* what the agent learned across sessions. + +## Configuration Reference + +### Memory Store Config + +| Field | Required | Description | +|-------|----------|-------------| +| `api_key` | Yes | Mem0 API key from [app.mem0.ai](https://app.mem0.ai) | +| `user_id` | No | Scope memories to a specific user | +| `agent_id` | No | Scope memories to a specific agent | + +### Memory Attachment Config + +| Field | Default | Description | +|-------|---------|-------------| +| `top_k` | `3` | Number of memories to retrieve | +| `similarity_threshold` | `-1.0` (disabled) | Minimum relevance score. Set a value between `0.0` and `1.0` to filter low-relevance results. Default (`-1.0`) returns all matches without filtering | +| `retrieve_stage` | `["gen"]` | When to retrieve: `pre_gen_thinking`, `gen`, `post_gen_thinking`, or `finished` | +| `read` | `true` | Whether the agent retrieves memories | +| `write` | `true` | Whether the agent stores new memories | + +## Tips and Common Pitfalls + + +**Indexing delay** — Freshly stored memories may take a few seconds to become searchable. If a memory isn't retrieved immediately after being stored, wait a moment and try again. + + +- **No memories returned on first run** — This is expected. Memories are stored *after* the agent responds, so the first interaction has no prior context. Memories appear starting from the second interaction onward. +- **`mem0ai` not installed** — If you see `ImportError: mem0ai is required for Mem0Memory`, run `uv add mem0ai` or `pip install mem0ai` to add the dependency. +- **Invalid API key** — A wrong or expired `MEM0_API_KEY` will log errors like `Mem0 search failed` or `Mem0 add failed` but won't crash the agent. Check your key at [app.mem0.ai](https://app.mem0.ai). +- **Pipeline headers in memories** — ChatDev automatically strips internal pipeline headers (e.g., `=== INPUT FROM TASK (user) ===`) before sending text to Mem0, so your memories stay clean. +- **Clearing test memories** — To delete memories created during testing, use the Mem0 dashboard at [app.mem0.ai](https://app.mem0.ai) or the Python SDK: `MemoryClient().delete_all(user_id="your-test-user")`. + +## Key Features + +1. **Zero-Code Integration** — Configure Mem0 entirely through YAML, no Python code required +2. **Cloud-Managed Storage** — Mem0 handles embeddings, persistence, and search server-side +3. **Semantic Search** — Retrieve contextually relevant memories, not just keyword matches +4. **Cross-Session Persistence** — Memories survive across runs, sessions, and restarts +5. **Multi-Agent Memory Sharing** — Multiple agents can share memories through common `user_id` or `agent_id` scopes +6. **Intelligent Input Processing** — Only user input is stored; agent output is excluded to prevent noisy memories + +## Conclusion + +By adding Mem0 as a memory store in ChatDev, your multi-agent workflows gain persistent, intelligent memory with zero code changes. Agents automatically remember past interactions and use that context to provide personalized, coherent responses across sessions. + + + + Build multi-agent systems with CrewAI and Mem0 + + + Build conversational agents with AutoGen and Mem0 + + From 081eca6d8fa36c5cf6b4f5883c529e1586824a00 Mon Sep 17 00:00:00 2001 From: Ignazio De Santis <132545917+IgnazioDS@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:25:52 +0800 Subject: [PATCH 4/5] fix: guard temp_uuid_mapping lookups against LLM-hallucinated IDs (fixes #3931) (#4674) Co-authored-by: kartik-mem0 --- mem0/memory/main.py | 41 +++++-- tests/memory/test_main.py | 222 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 8 deletions(-) diff --git a/mem0/memory/main.py b/mem0/memory/main.py index 1804e7831..582e56d88 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -63,6 +63,19 @@ def _normalize_iso_timestamp_to_utc(timestamp: Optional[str]) -> Optional[str]: return parsed.astimezone(timezone.utc).isoformat() +def _resolve_mapped_id(temp_uuid_mapping, resp, event_type): + """Resolve a temp integer ID from the LLM response to a real UUID. + + Returns the UUID if found, or None (with a warning log) if the LLM + hallucinated an ID that doesn't exist in the mapping. + """ + raw_id = resp.get("id") + memory_id = temp_uuid_mapping.get(raw_id) + if memory_id is None: + logger.warning(f"{event_type} skipped: LLM returned unknown id {raw_id!r}") + return memory_id + + # Fields that hold runtime auth/connection objects and must be preserved. # These are non-serializable objects (e.g. AWSV4SignerAuth, RequestsHttpConnection) # needed by clients like OpenSearch — not sensitive strings to redact. @@ -639,28 +652,34 @@ class Memory(MemoryBase): ) returned_memories.append({"id": memory_id, "memory": action_text, "event": event_type}) elif event_type == "UPDATE": + memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "UPDATE") + if memory_id is None: + continue # Ensure action_text has an embedding cached to avoid redundant API calls if action_text not in new_message_embeddings: new_message_embeddings[action_text] = self.embedding_model.embed(action_text, "update") self._update_memory( - memory_id=temp_uuid_mapping[resp.get("id")], + memory_id=memory_id, data=action_text, existing_embeddings=new_message_embeddings, metadata=deepcopy(metadata), ) returned_memories.append( { - "id": temp_uuid_mapping[resp.get("id")], + "id": memory_id, "memory": action_text, "event": event_type, "previous_memory": resp.get("old_memory"), } ) elif event_type == "DELETE": - self._delete_memory(memory_id=temp_uuid_mapping[resp.get("id")]) + memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "DELETE") + if memory_id is None: + continue + self._delete_memory(memory_id=memory_id) returned_memories.append( { - "id": temp_uuid_mapping[resp.get("id")], + "id": memory_id, "memory": action_text, "event": event_type, } @@ -1755,6 +1774,9 @@ class AsyncMemory(MemoryBase): ) memory_tasks.append((task, resp, "ADD", None)) elif event_type == "UPDATE": + memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "UPDATE") + if memory_id is None: + continue # Ensure action_text has an embedding cached to avoid redundant API calls if action_text not in new_message_embeddings: new_message_embeddings[action_text] = await asyncio.to_thread( @@ -1762,16 +1784,19 @@ class AsyncMemory(MemoryBase): ) task = asyncio.create_task( self._update_memory( - memory_id=temp_uuid_mapping[resp["id"]], + memory_id=memory_id, data=action_text, existing_embeddings=new_message_embeddings, metadata=deepcopy(metadata), ) ) - memory_tasks.append((task, resp, "UPDATE", temp_uuid_mapping[resp["id"]])) + memory_tasks.append((task, resp, "UPDATE", memory_id)) elif event_type == "DELETE": - task = asyncio.create_task(self._delete_memory(memory_id=temp_uuid_mapping[resp.get("id")])) - memory_tasks.append((task, resp, "DELETE", temp_uuid_mapping[resp.get("id")])) + memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "DELETE") + if memory_id is None: + continue + task = asyncio.create_task(self._delete_memory(memory_id=memory_id)) + memory_tasks.append((task, resp, "DELETE", memory_id)) elif event_type == "NONE": # Even if content doesn't need updating, update session IDs if provided memory_id = temp_uuid_mapping.get(resp.get("id")) diff --git a/tests/memory/test_main.py b/tests/memory/test_main.py index 9938baf38..15507e2be 100644 --- a/tests/memory/test_main.py +++ b/tests/memory/test_main.py @@ -627,6 +627,228 @@ async def test_async_update_preserves_actor_id_when_different_actor_updates(mock assert stored["actor_id"] == "Alice" +class TestHallucinatedIdGuard: + """Tests for temp_uuid_mapping guard against LLM-hallucinated IDs (issue #3931). + + When the LLM returns an UPDATE or DELETE with an ID that doesn't exist in + temp_uuid_mapping, the code should skip gracefully instead of raising KeyError. + """ + + def test_sync_update_with_hallucinated_id_skips_gracefully(self, mocker, caplog): + """Sync UPDATE with an out-of-range ID should be skipped with a warning.""" + memory = _build_memory_instance(mocker, Memory) + memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3] + mocker.patch("mem0.memory.main.capture_event") + + # Simulate: 2 existing memories (IDs "0" and "1"), but LLM returns UPDATE for ID "12" + existing_mem = MagicMock() + existing_mem.id = "uuid-aaa" + existing_mem.payload = {"data": "User likes coffee"} + memory.vector_store.search.return_value = [existing_mem] + + # First LLM call: fact extraction → returns one fact + # Second LLM call: memory update actions → returns UPDATE with hallucinated ID "12" + memory.llm.generate_response.side_effect = [ + '{"facts": ["User likes tea"]}', + '{"memory": [{"id": "12", "text": "User likes tea", "event": "UPDATE", "old_memory": "User likes coffee"}]}', + ] + + with caplog.at_level(logging.WARNING): + result = memory._add_to_vector_store( + messages=[{"role": "user", "content": "I like tea"}], + metadata={}, + filters={}, + infer=True, + ) + + # Should not crash, should return empty (the hallucinated UPDATE was skipped) + assert result == [] + assert "UPDATE skipped: LLM returned unknown id" in caplog.text + # _update_memory should NOT have been called + memory.vector_store.update.assert_not_called() + + def test_sync_delete_with_hallucinated_id_skips_gracefully(self, mocker, caplog): + """Sync DELETE with an out-of-range ID should be skipped with a warning.""" + memory = _build_memory_instance(mocker, Memory) + memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3] + mocker.patch("mem0.memory.main.capture_event") + + existing_mem = MagicMock() + existing_mem.id = "uuid-aaa" + existing_mem.payload = {"data": "User likes coffee"} + memory.vector_store.search.return_value = [existing_mem] + + memory.llm.generate_response.side_effect = [ + '{"facts": ["Remove coffee preference"]}', + '{"memory": [{"id": "9", "text": "User likes coffee", "event": "DELETE"}]}', + ] + + with caplog.at_level(logging.WARNING): + result = memory._add_to_vector_store( + messages=[{"role": "user", "content": "I no longer like coffee"}], + metadata={}, + filters={}, + infer=True, + ) + + assert result == [] + assert "DELETE skipped: LLM returned unknown id" in caplog.text + memory.vector_store.delete.assert_not_called() + + def test_sync_valid_id_still_processes_normally(self, mocker, caplog): + """A valid ID should still be processed — the guard must not block legitimate operations.""" + memory = _build_memory_instance(mocker, Memory) + memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3] + mocker.patch("mem0.memory.main.capture_event") + + existing_mem = MagicMock() + existing_mem.id = "uuid-aaa" + existing_mem.payload = {"data": "User likes coffee"} + memory.vector_store.search.return_value = [existing_mem] + memory.vector_store.get.return_value = MagicMock( + payload={"data": "User likes coffee", "created_at": "2026-01-01T00:00:00+00:00"} + ) + + # ID "0" is valid since there's exactly 1 existing memory + memory.llm.generate_response.side_effect = [ + '{"facts": ["User likes tea now"]}', + '{"memory": [{"id": "0", "text": "User likes tea now", "event": "UPDATE", "old_memory": "User likes coffee"}]}', + ] + + with caplog.at_level(logging.WARNING): + result = memory._add_to_vector_store( + messages=[{"role": "user", "content": "I like tea now"}], + metadata={}, + filters={}, + infer=True, + ) + + assert len(result) == 1 + assert result[0]["event"] == "UPDATE" + assert result[0]["memory"] == "User likes tea now" + assert result[0]["id"] == "uuid-aaa" + assert "skipped" not in caplog.text + + @pytest.mark.asyncio + async def test_async_update_with_hallucinated_id_skips_gracefully(self, mocker, caplog): + """Async UPDATE with an out-of-range ID should be skipped with a warning.""" + memory = _build_memory_instance(mocker, AsyncMemory) + memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3] + mocker.patch("mem0.memory.main.capture_event") + + existing_mem = MagicMock() + existing_mem.id = "uuid-bbb" + existing_mem.payload = {"data": "User works at Acme"} + memory.vector_store.search.return_value = [existing_mem] + + memory.llm.generate_response.side_effect = [ + '{"facts": ["User works at Globex"]}', + '{"memory": [{"id": "7", "text": "User works at Globex", "event": "UPDATE", "old_memory": "User works at Acme"}]}', + ] + + with caplog.at_level(logging.WARNING): + result = await memory._add_to_vector_store( + messages=[{"role": "user", "content": "I now work at Globex"}], + metadata={}, + effective_filters={}, + infer=True, + ) + + assert result == [] + assert "UPDATE skipped: LLM returned unknown id" in caplog.text + + @pytest.mark.asyncio + async def test_async_delete_with_hallucinated_id_skips_gracefully(self, mocker, caplog): + """Async DELETE with an out-of-range ID should be skipped with a warning.""" + memory = _build_memory_instance(mocker, AsyncMemory) + memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3] + mocker.patch("mem0.memory.main.capture_event") + + existing_mem = MagicMock() + existing_mem.id = "uuid-ccc" + existing_mem.payload = {"data": "User lives in SF"} + memory.vector_store.search.return_value = [existing_mem] + + memory.llm.generate_response.side_effect = [ + '{"facts": ["Remove SF reference"]}', + '{"memory": [{"id": "16", "text": "User lives in SF", "event": "DELETE"}]}', + ] + + with caplog.at_level(logging.WARNING): + result = await memory._add_to_vector_store( + messages=[{"role": "user", "content": "I moved away from SF"}], + metadata={}, + effective_filters={}, + infer=True, + ) + + assert result == [] + assert "DELETE skipped: LLM returned unknown id" in caplog.text + + def test_sync_update_with_missing_id_key_skips_gracefully(self, mocker, caplog): + """UPDATE where the LLM omits the 'id' field entirely should be skipped.""" + memory = _build_memory_instance(mocker, Memory) + memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3] + mocker.patch("mem0.memory.main.capture_event") + + existing_mem = MagicMock() + existing_mem.id = "uuid-aaa" + existing_mem.payload = {"data": "User likes coffee"} + memory.vector_store.search.return_value = [existing_mem] + + # LLM response has no "id" key at all + memory.llm.generate_response.side_effect = [ + '{"facts": ["User likes tea"]}', + '{"memory": [{"text": "User likes tea", "event": "UPDATE", "old_memory": "User likes coffee"}]}', + ] + + with caplog.at_level(logging.WARNING): + result = memory._add_to_vector_store( + messages=[{"role": "user", "content": "I like tea"}], + metadata={}, + filters={}, + infer=True, + ) + + assert result == [] + assert "UPDATE skipped: LLM returned unknown id" in caplog.text + + @pytest.mark.asyncio + async def test_async_valid_id_still_processes_normally(self, mocker, caplog): + """Async path: a valid ID should process normally — no false positives from the guard.""" + memory = _build_memory_instance(mocker, AsyncMemory) + memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3] + mocker.patch("mem0.memory.main.capture_event") + + existing_mem = MagicMock() + existing_mem.id = "uuid-bbb" + existing_mem.payload = {"data": "User works at Acme"} + memory.vector_store.search.return_value = [existing_mem] + memory.vector_store.get.return_value = MagicMock( + payload={"data": "User works at Acme", "created_at": "2026-01-01T00:00:00+00:00"} + ) + + # ID "0" is valid since there's exactly 1 existing memory + memory.llm.generate_response.side_effect = [ + '{"facts": ["User works at Globex now"]}', + '{"memory": [{"id": "0", "text": "User works at Globex now", "event": "UPDATE", "old_memory": "User works at Acme"}]}', + ] + + with caplog.at_level(logging.WARNING): + result = await memory._add_to_vector_store( + messages=[{"role": "user", "content": "I now work at Globex"}], + metadata={}, + effective_filters={}, + infer=True, + ) + + assert len(result) == 1 + assert result[0]["event"] == "UPDATE" + assert result[0]["memory"] == "User works at Globex now" + assert result[0]["id"] == "uuid-bbb" + assert "skipped" not in caplog.text + + def test_normalize_iso_timestamp_to_utc_preserves_naive_values(): assert _normalize_iso_timestamp_to_utc("2026-03-18T00:00:00") == "2026-03-18T00:00:00" From 88f696a60a24e8a84a6191faeb1337b036cb4add Mon Sep 17 00:00:00 2001 From: Kartik Date: Thu, 9 Apr 2026 14:57:33 +0530 Subject: [PATCH 5/5] refactor: drop orgId, projectId, enableGraph config options, update CLI prompts, and clean up related code (#4734) --- openclaw/CHANGELOG.md | 10 + openclaw/backend/base.ts | 3 - openclaw/backend/platform.ts | 3 - openclaw/cli/commands.ts | 28 +- openclaw/cli/config-file.ts | 6 - openclaw/config.ts | 23 +- openclaw/index.ts | 86 ++++-- openclaw/openclaw.plugin.json | 1 + openclaw/package.json | 10 +- openclaw/providers.ts | 109 ++++--- openclaw/skills/memory-dream/SKILL.md | 2 +- openclaw/skills/memory-triage/SKILL.md | 2 +- openclaw/sqlite-resilience.test.ts | 396 ++++++++++++++++++++++++- openclaw/tests/cli-commands.test.ts | 95 ++++-- openclaw/tests/config-file.test.ts | 12 +- openclaw/tests/config.test.ts | 35 +-- openclaw/tests/providers.test.ts | 2 - openclaw/tests/tools.test.ts | 1 - openclaw/tools/memory-add.ts | 1 - openclaw/types.ts | 5 - 20 files changed, 624 insertions(+), 206 deletions(-) diff --git a/openclaw/CHANGELOG.md b/openclaw/CHANGELOG.md index bc9742379..f020c5555 100644 --- a/openclaw/CHANGELOG.md +++ b/openclaw/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file. +## [1.0.5] - 2026-04-07 + +### Fixed +- **Init interactive choice bug**: Fixed number selection in `openclaw mem0 init` — entering 1/2/3 now correctly selects the corresponding option (was broken by readline prefill concatenating with user input) +- **OSS pgvector crash** ([#4727]): Fixed "Client has already been connected" cascade when using pgvector in OSS mode. The warmup call swallowed errors leaving a half-initialized pg client; concurrent recall/capture then all hit `client.connect()` on the same client. Fix: let warmup errors propagate (so `initPromise` resets and retries with a fresh Memory + fresh pg client) and build fresh config objects per attempt instead of mutating shared state. + +### Removed +- **`orgId` / `projectId` config parameters**: Removed from config schema, CLI (`config show/get/set`), init display, and providers. The API key is project-scoped, so separate org/project IDs are unnecessary and could cause access errors if mismatched. +- **`enableGraph` config parameter**: Removed from all config surfaces, providers, backend, and tools. Graph memory is being deprecated — removing the flag avoids unnecessary exposure. + ## [1.0.4] - 2026-04-04 ### Added diff --git a/openclaw/backend/base.ts b/openclaw/backend/base.ts index d34d6ee3b..6a52a3922 100644 --- a/openclaw/backend/base.ts +++ b/openclaw/backend/base.ts @@ -14,7 +14,6 @@ export interface AddOptions { infer?: boolean; expires?: string; categories?: string[]; - enableGraph?: boolean; } export interface SearchOptions { @@ -28,7 +27,6 @@ export interface SearchOptions { keyword?: boolean; filters?: Record; fields?: string[]; - enableGraph?: boolean; } export interface ListOptions { @@ -41,7 +39,6 @@ export interface ListOptions { category?: string; after?: string; before?: string; - enableGraph?: boolean; } export interface DeleteOptions { diff --git a/openclaw/backend/platform.ts b/openclaw/backend/platform.ts index 66865c2cb..481de5873 100644 --- a/openclaw/backend/platform.ts +++ b/openclaw/backend/platform.ts @@ -111,7 +111,6 @@ export class PlatformBackend implements Backend { if (opts.infer === false) payload.infer = false; if (opts.expires) payload.expiration_date = opts.expires; if (opts.categories) payload.categories = opts.categories; - if (opts.enableGraph) payload.enable_graph = true; return (await this._request("POST", "/v1/memories/", { json: payload, @@ -171,7 +170,6 @@ export class PlatformBackend implements Backend { if (opts.rerank) payload.rerank = true; if (opts.keyword) payload.keyword_search = true; if (opts.fields) payload.fields = opts.fields; - if (opts.enableGraph) payload.enable_graph = true; const result = (await this._request("POST", "/v2/memories/search/", { json: payload, @@ -222,7 +220,6 @@ export class PlatformBackend implements Backend { extraFilters: Object.keys(extra).length > 0 ? extra : undefined, }); if (apiFilters) payload.filters = apiFilters; - if (opts.enableGraph) payload.enable_graph = true; const result = (await this._request("POST", "/v2/memories/", { json: payload, diff --git a/openclaw/cli/commands.ts b/openclaw/cli/commands.ts index 015279e3a..603f5a002 100644 --- a/openclaw/cli/commands.ts +++ b/openclaw/cli/commands.ts @@ -384,10 +384,6 @@ export function registerCliCommands( console.log(` User ID: ${existingAuth.userId}`); if (existingAuth.mode) console.log(` Mode: ${existingAuth.mode}`); - if (existingAuth.orgId) - console.log(` Org ID: ${existingAuth.orgId}`); - if (existingAuth.projectId) - console.log(` Project: ${existingAuth.projectId}`); console.log(""); // Validate existing key before asking @@ -408,7 +404,7 @@ export function registerCliCommands( } const reuse = await promptInput( - " Keep existing configuration? (Y/n): ", + " Keep existing configuration? (y/n): ", ); if ( reuse === "" || @@ -432,7 +428,7 @@ export function registerCliCommands( console.log(" 2. Enter API key manually"); console.log(" 3. Open-source mode (self-hosted)\n"); - const choice = (await promptInput(" Choice: ", "1")) || "1"; + const choice = (await promptInput(" Choice (1/2/3): ")) || "1"; if (choice === "1") { // --- Email interactive flow --- @@ -952,9 +948,6 @@ export function registerCliCommands( email: "userEmail", base_url: "baseUrl", user_id: "userId", - org_id: "orgId", - project_id: "projectId", - enable_graph: "enableGraph", auto_recall: "autoRecall", auto_capture: "autoCapture", top_k: "topK", @@ -969,6 +962,8 @@ export function registerCliCommands( vector_host: "oss.vectorStore.config.host", vector_port: "oss.vectorStore.config.port", collection_name: "oss.vectorStore.config.collectionName", + vector_db_name: "oss.vectorStore.config.dbname", + vector_db_user: "oss.vectorStore.config.user", vector_db_path: "oss.vectorStore.config.dbPath", history_db_path: "oss.historyDbPath", disable_history: "oss.disableHistory", @@ -979,7 +974,6 @@ export function registerCliCommands( // Boolean config fields — coerce "true"/"1"/"yes" on set const BOOLEAN_KEYS = new Set([ - "enableGraph", "autoRecall", "autoCapture", "oss.disableHistory", @@ -1009,11 +1003,8 @@ export function registerCliCommands( apiKey: auth.apiKey ?? cfg.apiKey, baseUrl: auth.baseUrl ?? cfg.baseUrl ?? "https://api.mem0.ai", userId: auth.userId ?? cfg.userId, - orgId: auth.orgId ?? cfg.orgId, - projectId: auth.projectId ?? cfg.projectId, mode: auth.mode ?? cfg.mode, userEmail: auth.userEmail, - enableGraph: cfg.enableGraph, autoRecall: cfg.autoRecall, autoCapture: cfg.autoCapture, topK: cfg.topK, @@ -1055,9 +1046,6 @@ export function registerCliCommands( entries.push( ["api_key", "apiKey"], ["email", "userEmail"], - ["org_id", "orgId"], - ["project_id", "projectId"], - ["enable_graph", "enableGraph"], ); } else { entries.push( @@ -1238,6 +1226,10 @@ export function registerCliCommands( .description("List recent background events") .action(async () => { try { + if (!backend || cfg.mode === "open-source") { + console.log("Event tracking is only available in platform mode."); + return; + } const results = await backend.listEvents(); if (!results.length) { console.log("No events found."); @@ -1282,6 +1274,10 @@ export function registerCliCommands( .argument("", "Event ID to check") .action(async (eventId: string) => { try { + if (!backend || cfg.mode === "open-source") { + console.log("Event tracking is only available in platform mode."); + return; + } const ev = await backend.getEvent(eventId); const status = String(ev.status ?? "—"); diff --git a/openclaw/cli/config-file.ts b/openclaw/cli/config-file.ts index 060b96d0d..86287f644 100644 --- a/openclaw/cli/config-file.ts +++ b/openclaw/cli/config-file.ts @@ -29,11 +29,8 @@ export interface PluginAuthConfig { apiKey?: string; baseUrl?: string; userId?: string; - orgId?: string; - projectId?: string; userEmail?: string; mode?: string; - enableGraph?: boolean; autoRecall?: boolean; autoCapture?: boolean; topK?: number; @@ -76,11 +73,8 @@ export function readPluginAuth(): PluginAuthConfig { apiKey: (cfg.apiKey ?? cfg.api_key) as string | undefined, baseUrl: (cfg.baseUrl ?? cfg.base_url) as string | undefined, userId: (cfg.userId ?? cfg.user_id) as string | undefined, - orgId: (cfg.orgId ?? cfg.org_id) as string | undefined, - projectId: (cfg.projectId ?? cfg.project_id) as string | undefined, userEmail: (cfg.userEmail ?? cfg.user_email) as string | undefined, mode: cfg.mode as string | undefined, - enableGraph: cfg.enableGraph as boolean | undefined, autoRecall: cfg.autoRecall as boolean | undefined, autoCapture: cfg.autoCapture as boolean | undefined, topK: cfg.topK as number | undefined, diff --git a/openclaw/config.ts b/openclaw/config.ts index 2b16ba2ac..94cb1226c 100644 --- a/openclaw/config.ts +++ b/openclaw/config.ts @@ -19,8 +19,6 @@ import type { Mem0Config, Mem0Mode } from "./types.ts"; export interface FileConfig { apiKey?: string; baseUrl?: string; - orgId?: string; - projectId?: string; } // ============================================================================ @@ -152,14 +150,11 @@ const ALLOWED_KEYS = [ "baseUrl", "userId", "userEmail", - "orgId", - "projectId", "autoCapture", "autoRecall", "customInstructions", "customCategories", "customPrompt", - "enableGraph", "searchThreshold", "topK", "oss", @@ -185,6 +180,15 @@ export const mem0ConfigSchema = { assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-mem0 config"); // Only two modes: "platform" (default) or "open-source" + if ( + typeof cfg.mode === "string" && + cfg.mode !== "platform" && + cfg.mode !== "open-source" + ) { + console.warn( + `[mem0] Unknown mode "${cfg.mode}" — expected "platform" or "open-source". Defaulting to "platform".`, + ); + } const mode: Mem0Mode = cfg.mode === "open-source" ? "open-source" : "platform"; @@ -193,15 +197,9 @@ export const mem0ConfigSchema = { typeof cfg.apiKey === "string" ? cfg.apiKey : undefined; let resolvedBaseUrl = typeof cfg.baseUrl === "string" ? cfg.baseUrl : undefined; - let resolvedOrgId = typeof cfg.orgId === "string" ? cfg.orgId : undefined; - let resolvedProjectId = - typeof cfg.projectId === "string" ? cfg.projectId : undefined; if (mode === "platform" && !resolvedApiKey && fileConfig) { if (fileConfig.apiKey) resolvedApiKey = fileConfig.apiKey; if (fileConfig.baseUrl) resolvedBaseUrl = fileConfig.baseUrl; - if (!resolvedOrgId && fileConfig.orgId) resolvedOrgId = fileConfig.orgId; - if (!resolvedProjectId && fileConfig.projectId) - resolvedProjectId = fileConfig.projectId; } // Platform mode requires apiKey — but don't throw on missing config. @@ -228,8 +226,6 @@ export const mem0ConfigSchema = { return "default"; } })(), - orgId: resolvedOrgId, - projectId: resolvedProjectId, autoCapture: cfg.autoCapture !== false, autoRecall: cfg.autoRecall !== false, customInstructions: @@ -246,7 +242,6 @@ export const mem0ConfigSchema = { typeof cfg.customPrompt === "string" ? cfg.customPrompt : DEFAULT_CUSTOM_INSTRUCTIONS, - enableGraph: cfg.enableGraph === true, searchThreshold: typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5, topK: typeof cfg.topK === "number" ? cfg.topK : 5, diff --git a/openclaw/index.ts b/openclaw/index.ts index 32e3648cb..8eddd37fb 100644 --- a/openclaw/index.ts +++ b/openclaw/index.ts @@ -103,15 +103,21 @@ const memoryPlugin = definePluginEntry({ const fileConfig: FileConfig = { apiKey: pluginAuth.apiKey, baseUrl: pluginAuth.baseUrl, - orgId: pluginAuth.orgId, - projectId: pluginAuth.projectId, }; const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig); // Telemetry context bound to this plugin instance's config - const telemetryCtx = { apiKey: cfg.apiKey, mode: cfg.mode, skillsActive: false }; + const telemetryCtx = { + apiKey: cfg.apiKey, + mode: cfg.mode, + skillsActive: false, + }; const _captureEvent = (event: string, props?: Record) => { - try { captureEvent(event, props, telemetryCtx); } catch { /* silently swallow */ } + try { + captureEvent(event, props, telemetryCtx); + } catch { + /* silently swallow */ + } }; if (cfg.needsSetup) { @@ -182,7 +188,7 @@ const memoryPlugin = definePluginEntry({ }); api.logger.info( - `openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, graph: ${cfg.enableGraph}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`, + `openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`, ); // Helper: build add options @@ -197,7 +203,6 @@ const memoryPlugin = definePluginEntry({ }; if (runId) opts.run_id = runId; if (cfg.mode === "platform") { - opts.enable_graph = cfg.enableGraph; opts.output_format = "v1.1"; } return opts; @@ -242,7 +247,10 @@ const memoryPlugin = definePluginEntry({ getCurrentSessionId: () => currentSessionId, skillsActive, captureToolEvent: (toolName: string, props: Record) => { - _captureEvent(`openclaw.tool.${toolName}`, { tool_name: toolName, ...props }); + _captureEvent(`openclaw.tool.${toolName}`, { + tool_name: toolName, + ...props, + }); }, }; registerAllTools(toolDeps); @@ -328,7 +336,10 @@ function registerHooks( getStateDir: () => string | undefined; }, skillsActive: boolean = false, - _captureEvent: (event: string, props?: Record) => void = () => {}, + _captureEvent: ( + event: string, + props?: Record, + ) => void = () => {}, ) { // ======================================================================== // SKILLS MODE: Agentic memory via before_prompt_build @@ -355,16 +366,17 @@ function registerHooks( return; } - // Skip recall for system/bootstrap prompts. These are OpenClaw internal - // commands (/new, /reset) that contain system instructions, not user queries. - // Sending them to mem0 search wastes API calls and returns noise. const promptLower = event.prompt.toLowerCase(); + const isChannelSystemEvent = /^system(?:\s*\(untrusted\))?:\s*\[/i.test( + event.prompt, + ); const isSystemPrompt = - promptLower.includes("a new session was started") || - promptLower.includes("session startup sequence") || - promptLower.includes("/new or /reset") || - promptLower.startsWith("system:") || - promptLower.startsWith("run your session"); + !isChannelSystemEvent && + (promptLower.includes("a new session was started") || + promptLower.includes("session startup sequence") || + promptLower.includes("/new or /reset") || + promptLower.startsWith("system:") || + promptLower.startsWith("run your session")); if (isSystemPrompt) { api.logger.info( "openclaw-mem0: skills-mode skipping recall for system/bootstrap prompt", @@ -472,7 +484,10 @@ function registerHooks( "\n"; // Track which session triggered dream (session-keyed, not global) dreamSessionId = sessionId; - _captureEvent("openclaw.hook.dream", { phase: "triggered", memory_count: memCount }); + _captureEvent("openclaw.hook.dream", { + phase: "triggered", + memory_count: memCount, + }); api.logger.info( `openclaw-mem0: auto-dream triggered (${memCount} memories, gate passed)`, ); @@ -545,7 +560,10 @@ function registerHooks( if (writeToolUsed) { releaseDreamLock(stateDir); recordDreamCompletion(stateDir); - _captureEvent("openclaw.hook.dream", { phase: "completed", write_tools_used: true }); + _captureEvent("openclaw.hook.dream", { + phase: "completed", + write_tools_used: true, + }); api.logger.info( "openclaw-mem0: auto-dream completed (verified write tool usage), lock released", ); @@ -599,14 +617,17 @@ function registerHooks( return; } - // Skip recall for system/bootstrap prompts to save API calls const promptLower = event.prompt.toLowerCase(); + const isChannelSystemEvent = /^system(?:\s*\(untrusted\))?:\s*\[/i.test( + event.prompt, + ); const isSystemPrompt = - promptLower.includes("a new session was started") || - promptLower.includes("session startup sequence") || - promptLower.includes("/new or /reset") || - promptLower.startsWith("system:") || - promptLower.startsWith("run your session"); + !isChannelSystemEvent && + (promptLower.includes("a new session was started") || + promptLower.includes("session startup sequence") || + promptLower.includes("/new or /reset") || + promptLower.startsWith("system:") || + promptLower.startsWith("run your session")); if (isSystemPrompt) { api.logger.info( "openclaw-mem0: skipping recall for system/bootstrap prompt", @@ -775,11 +796,17 @@ function registerHooks( // Update shared state for tools (best-effort — tools don't have ctx) if (sessionId) session.setCurrentSessionId(sessionId); - const MEMORY_MUTATE_TOOLS = new Set(["memory_add", "memory_update", "memory_delete"]); + const MEMORY_MUTATE_TOOLS = new Set([ + "memory_add", + "memory_update", + "memory_delete", + ]); const agentUsedMemoryTool = event.messages.some((msg: any) => { - if (msg?.role !== "assistant" || !Array.isArray(msg?.content)) return false; + if (msg?.role !== "assistant" || !Array.isArray(msg?.content)) + return false; return msg.content.some( - (block: any) => block?.type === "tool_use" && MEMORY_MUTATE_TOOLS.has(block.name), + (block: any) => + block?.type === "tool_use" && MEMORY_MUTATE_TOOLS.has(block.name), ); }); if (agentUsedMemoryTool) { @@ -848,7 +875,10 @@ function registerHooks( if (!textContent) continue; } // Strip OpenClaw sender metadata prefix (prevents storing TUI identity as memory) - if (textContent.includes("Sender") && textContent.includes("untrusted metadata")) { + if ( + textContent.includes("Sender") && + textContent.includes("untrusted metadata") + ) { textContent = textContent .replace( /Sender\s*\(untrusted metadata\):\s*```json[\s\S]*?```\s*/gi, diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json index 70f56d6f1..1f1abff2e 100644 --- a/openclaw/openclaw.plugin.json +++ b/openclaw/openclaw.plugin.json @@ -2,6 +2,7 @@ "id": "openclaw-mem0", "name": "Memory (Mem0)", "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source", + "version": "1.0.5", "kind": "memory", "skills": ["skills"], "contracts": { diff --git a/openclaw/package.json b/openclaw/package.json index 125141bce..ff38848cc 100644 --- a/openclaw/package.json +++ b/openclaw/package.json @@ -1,6 +1,6 @@ { "name": "@mem0/openclaw-mem0", - "version": "1.0.4", + "version": "1.0.5", "type": "module", "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source", "license": "Apache-2.0", @@ -42,12 +42,12 @@ "./dist/index.js" ], "compat": { - "pluginApi": ">=2026.3.24-beta.2", - "minGatewayVersion": "2026.3.24-beta.2" + "pluginApi": ">=2026.3.28 <=2026.4.2", + "minGatewayVersion": "2026.3.28" }, "build": { - "openclawVersion": "2026.3.24-beta.2", - "pluginSdkVersion": "2026.3.24-beta.2" + "openclawVersion": "2026.4.1", + "pluginSdkVersion": "2026.4.1" } }, "devDependencies": { diff --git a/openclaw/providers.ts b/openclaw/providers.ts index 22a56226c..565554a44 100644 --- a/openclaw/providers.ts +++ b/openclaw/providers.ts @@ -82,8 +82,6 @@ class PlatformProvider implements Mem0Provider { constructor( private readonly apiKey: string, private readonly baseUrl?: string, - private readonly orgId?: string, - private readonly projectId?: string, ) {} private async ensureClient(): Promise { @@ -101,14 +99,10 @@ class PlatformProvider implements Mem0Provider { const opts: { apiKey: string; host?: string; - organizationId?: string; - projectId?: string; } = { apiKey: this.apiKey, }; if (this.baseUrl) opts.host = this.baseUrl; - if (this.orgId) opts.organizationId = this.orgId; - if (this.projectId) opts.projectId = this.projectId; this.client = new MemoryClient(opts); } @@ -123,7 +117,6 @@ class PlatformProvider implements Mem0Provider { opts.custom_instructions = options.custom_instructions; if (options.custom_categories) opts.custom_categories = options.custom_categories; - if (options.enable_graph) opts.enable_graph = options.enable_graph; if (options.output_format) opts.output_format = options.output_format; if (options.source) opts.source = options.source; // Agentic harness: direct storage bypass @@ -208,9 +201,7 @@ class PlatformProvider implements Mem0Provider { await this.client.deleteAll({ user_id: userId }); } - async history( - memoryId: string, - ): Promise< + async history(memoryId: string): Promise< Array<{ id: string; old_memory: string; @@ -249,23 +240,29 @@ class OSSProvider implements Mem0Provider { return this.initPromise; } - private async _init(): Promise { - const { Memory } = await import("mem0ai/oss"); - + private _buildConfig(disableHistory = false): Record { const config: Record = { version: "v1.1" }; - const defaultEmbedder = { provider: "openai", config: { model: "text-embedding-3-small" } }; + const defaultEmbedder = { + provider: "openai", + config: { model: "text-embedding-3-small" }, + }; const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } }; - // Helper: strip empty-string values so they don't clobber defaults const stripEmpty = (obj: Record) => { const out = { ...obj }; - for (const k of Object.keys(out)) { if (out[k] === "") delete out[k]; } + for (const k of Object.keys(out)) { + if (out[k] === "") delete out[k]; + } return out; }; if (this.ossConfig?.embedder) { const ec = stripEmpty(this.ossConfig.embedder.config ?? {}); + if (ec.host && !ec.url) { + ec.url = ec.host; + delete ec.host; + } config.embedder = { provider: this.ossConfig.embedder.provider || defaultEmbedder.provider, config: { ...defaultEmbedder.config, ...ec }, @@ -276,6 +273,10 @@ class OSSProvider implements Mem0Provider { if (this.ossConfig?.llm) { const lc = stripEmpty(this.ossConfig.llm.config ?? {}); + if (lc.host && !lc.url) { + lc.url = lc.host; + delete lc.host; + } config.llm = { provider: this.ossConfig.llm.provider || defaultLlm.provider, config: { ...defaultLlm.config, ...lc }, @@ -285,7 +286,7 @@ class OSSProvider implements Mem0Provider { } if (this.ossConfig?.vectorStore) - config.vectorStore = this.ossConfig.vectorStore; + config.vectorStore = { ...this.ossConfig.vectorStore }; if (this.ossConfig?.historyDbPath) { const dbPath = this.resolvePath @@ -294,42 +295,61 @@ class OSSProvider implements Mem0Provider { config.historyDbPath = dbPath; } - if (this.ossConfig?.disableHistory) { + if (disableHistory || this.ossConfig?.disableHistory) { config.disableHistory = true; } if (this.customPrompt) config.customPrompt = this.customPrompt; + return config; + } + private async _init(): Promise { + const mod = await import("mem0ai/oss"); + const Memory = mod.Memory; + for (const cls of ["PGVector", "RedisDB", "Qdrant"]) { + const VectorCls = (mod as any)[cls]; + if (!VectorCls || VectorCls.prototype.__patched) continue; + const origInit = VectorCls.prototype.initialize; + VectorCls.prototype.initialize = function (this: any) { + if (!this.config?.embeddingModelDims && this.config?.dimension) { + this.config.embeddingModelDims = this.config.dimension; + } + // Qdrant reads this.dimension directly + if (!this.dimension && this.config?.dimension) { + this.dimension = this.config.dimension; + } + // Skip premature constructor call when dimensions unknown + const dims = this.config?.embeddingModelDims ?? this.dimension; + if (!dims) return Promise.resolve(); + // Run the real initialize only once + if (!this._initializePromise) { + this._initializePromise = origInit.call(this); + } + return this._initializePromise; + }; + VectorCls.prototype.__patched = true; + } + + let mem: any; try { - this.memory = new Memory(config); + mem = new Memory(this._buildConfig()); } catch (err) { - // If initialization fails (e.g. native SQLite binding resolution under - // jiti), retry with history disabled — the history DB is the most common - // source of native-binding failures and is not required for core - // memory operations. - if (!config.disableHistory) { + // If constructor fails (e.g. native SQLite binding under jiti/Docker), + // retry with a FRESH config that has history disabled. + if (!this.ossConfig?.disableHistory) { console.warn( "[mem0] Memory initialization failed, retrying with history disabled:", err instanceof Error ? err.message : err, ); - config.disableHistory = true; - this.memory = new Memory(config); + mem = new Memory(this._buildConfig(true)); } else { throw err; } } - // Force the SDK's internal auto-initialization to complete now. - // Without this, concurrent method calls (e.g. auto-recall + search) - // both trigger _autoInitialize() simultaneously, causing PGVector's - // pg client to call connect() twice → "Client has already been - // connected" crash. (#4638) - try { - await this.memory.getAll({ userId: "__mem0_warmup__" }); - } catch { - // Warmup errors are non-fatal — the SDK may still work for - // subsequent calls once its internal state settles. - } + await mem.getAll({ userId: "__mem0_warmup__" }); + + this.memory = mem; } async add( @@ -423,9 +443,7 @@ class OSSProvider implements Mem0Provider { await this.memory.deleteAll({ userId }); } - async history( - memoryId: string, - ): Promise< + async history(memoryId: string): Promise< Array<{ id: string; old_memory: string; @@ -438,8 +456,12 @@ class OSSProvider implements Mem0Provider { try { const result = await this.memory.history(memoryId); return Array.isArray(result) ? result : []; - } catch { - // OSS may not support history depending on config + } catch (err) { + // OSS may not support history depending on config (e.g. disableHistory) + console.warn( + "[mem0] OSS history() failed:", + err instanceof Error ? err.message : err, + ); return []; } } @@ -459,7 +481,7 @@ export function createProvider( ); } - return new PlatformProvider(cfg.apiKey!, cfg.baseUrl, cfg.orgId, cfg.projectId); + return new PlatformProvider(cfg.apiKey!, cfg.baseUrl); } // ============================================================================ @@ -489,7 +511,6 @@ export function providerToBackend( ...(opts.immutable && { immutable: true }), ...(opts.infer === false && { infer: false }), ...(opts.expires && { expiration_date: opts.expires }), - ...(opts.enableGraph && { enable_graph: true }), }, ); return result as unknown as Record; diff --git a/openclaw/skills/memory-dream/SKILL.md b/openclaw/skills/memory-dream/SKILL.md index 651bdcd83..9224eafea 100644 --- a/openclaw/skills/memory-dream/SKILL.md +++ b/openclaw/skills/memory-dream/SKILL.md @@ -7,7 +7,7 @@ description: > Also triggers automatically after sufficient activity (configurable). user-invocable: true metadata: - {"openclaw": {"emoji": "💤"}} + {"openclaw": {"emoji": "💤", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}} --- # Memory Consolidation diff --git a/openclaw/skills/memory-triage/SKILL.md b/openclaw/skills/memory-triage/SKILL.md index 7f77bc652..102c1d17e 100644 --- a/openclaw/skills/memory-triage/SKILL.md +++ b/openclaw/skills/memory-triage/SKILL.md @@ -8,7 +8,7 @@ description: > Read this skill at the start of every session. user-invocable: false metadata: - {"openclaw": {"always": true, "emoji": "🧠"}} + {"openclaw": {"always": true, "emoji": "🧠", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}} --- # Memory Protocol diff --git a/openclaw/sqlite-resilience.test.ts b/openclaw/sqlite-resilience.test.ts index 3506d2a64..026e0b94d 100644 --- a/openclaw/sqlite-resilience.test.ts +++ b/openclaw/sqlite-resilience.test.ts @@ -4,9 +4,18 @@ * 2. initPromise poisoning fix (retry after failure) * 3. Graceful SQLite fallback in OSSProvider */ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { mem0ConfigSchema, createProvider } from "./index.ts"; +/** Stub vector-store classes required by OSSProvider._init's patching loop. */ +function vectorStubs() { + return { + PGVector: class { initialize() { return Promise.resolve(); } }, + RedisDB: class { initialize() { return Promise.resolve(); } }, + Qdrant: class { initialize() { return Promise.resolve(); } }, + }; +} + // --------------------------------------------------------------------------- // 1. Config: disableHistory passthrough // --------------------------------------------------------------------------- @@ -60,6 +69,7 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => { beforeEach(() => { capturedConfig = undefined; memoryCallCount = 0; + vi.resetModules(); vi.doMock("mem0ai/oss", () => ({ Memory: class MockMemory { @@ -81,9 +91,14 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => { } async delete() {} }, + ...vectorStubs(), })); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it("passes disableHistory: true to Memory when configured", async () => { const { createProvider } = await import("./index.ts"); const cfg = mem0ConfigSchema.parse({ @@ -130,6 +145,7 @@ describe("OSSProvider — initPromise retry after failure", () => { beforeEach(() => { callCount = 0; + vi.resetModules(); vi.doMock("mem0ai/oss", () => ({ Memory: class MockMemory { @@ -154,9 +170,14 @@ describe("OSSProvider — initPromise retry after failure", () => { } async delete() {} }, + ...vectorStubs(), })); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it("retries initialization after a transient failure", async () => { const { createProvider } = await import("./index.ts"); const cfg = mem0ConfigSchema.parse({ @@ -184,14 +205,21 @@ describe("OSSProvider — initPromise retry after failure", () => { // --------------------------------------------------------------------------- describe("OSSProvider — graceful SQLite fallback", () => { let capturedConfigs: Record[]; + /** When set, the mock Memory constructor always throws with this message. */ + let forceConstructorError: string | null; beforeEach(() => { capturedConfigs = []; + forceConstructorError = null; + vi.resetModules(); vi.doMock("mem0ai/oss", () => ({ Memory: class MockMemory { constructor(config: Record) { capturedConfigs.push({ ...config }); + if (forceConstructorError) { + throw new Error(forceConstructorError); + } if (!config.disableHistory) { throw new Error("Could not locate the bindings file"); } @@ -211,9 +239,14 @@ describe("OSSProvider — graceful SQLite fallback", () => { } async delete() {} }, + ...vectorStubs(), })); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it("retries with disableHistory: true when initial construction fails", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const { createProvider } = await import("./index.ts"); @@ -242,14 +275,8 @@ describe("OSSProvider — graceful SQLite fallback", () => { }); it("does not retry when disableHistory is already true", async () => { - vi.doMock("mem0ai/oss", () => ({ - Memory: class MockMemory { - constructor(config: Record) { - // Fail even with disableHistory (e.g. vector store issue) - throw new Error("vector store connection refused"); - } - }, - })); + // Force the constructor to always throw, regardless of disableHistory + forceConstructorError = "vector store connection refused"; const { createProvider } = await import("./index.ts"); const cfg = mem0ConfigSchema.parse({ @@ -274,6 +301,7 @@ describe("PlatformProvider — initPromise retry after failure", () => { beforeEach(() => { callCount = 0; + vi.resetModules(); vi.doMock("mem0ai", () => ({ default: class MockMemoryClient { @@ -300,6 +328,10 @@ describe("PlatformProvider — initPromise retry after failure", () => { })); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it("retries initialization after a transient failure", async () => { const { createProvider } = await import("./index.ts"); const cfg = mem0ConfigSchema.parse({ @@ -320,3 +352,349 @@ describe("PlatformProvider — initPromise retry after failure", () => { expect(callCount).toBe(2); }); }); + +// --------------------------------------------------------------------------- +// 6. OSSProvider: _buildConfig covers all branches +// --------------------------------------------------------------------------- +describe("OSSProvider — _buildConfig branch coverage", () => { + let capturedConfig: Record | undefined; + + beforeEach(() => { + capturedConfig = undefined; + vi.resetModules(); + + vi.doMock("mem0ai/oss", () => ({ + Memory: class MockMemory { + constructor(config: Record) { + capturedConfig = { ...config }; + } + async search() { return { results: [] }; } + async get() { return {}; } + async getAll() { return []; } + async add() { return { results: [] }; } + async delete() {} + }, + ...vectorStubs(), + })); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("builds config with custom embedder, llm, vectorStore, and historyDbPath", async () => { + const { createProvider } = await import("./index.ts"); + const cfg = mem0ConfigSchema.parse({ + mode: "open-source", + oss: { + embedder: { provider: "openai", config: { apiKey: "sk-e", model: "text-embedding-3-small" } }, + llm: { provider: "openai", config: { apiKey: "sk-l", model: "gpt-4" } }, + vectorStore: { provider: "qdrant", config: { host: "localhost", port: 6333 } }, + historyDbPath: "/tmp/history.db", + disableHistory: true, + }, + }); + const api = { resolvePath: (p: string) => `/resolved${p}` } as any; + const provider = createProvider(cfg, api); + + await provider.search("test", { user_id: "u1" }); + + expect(capturedConfig).toBeDefined(); + expect(capturedConfig!.embedder).toEqual({ + provider: "openai", + config: { model: "text-embedding-3-small", apiKey: "sk-e" }, + }); + expect(capturedConfig!.llm).toEqual({ + provider: "openai", + config: expect.objectContaining({ model: "gpt-4", apiKey: "sk-l" }), + }); + expect(capturedConfig!.vectorStore).toEqual({ provider: "qdrant", config: { host: "localhost", port: 6333 } }); + expect(capturedConfig!.historyDbPath).toBe("/resolved/tmp/history.db"); + expect(capturedConfig!.disableHistory).toBe(true); + }); + + it("strips empty-string values from embedder and llm config", async () => { + const { createProvider } = await import("./index.ts"); + const cfg = mem0ConfigSchema.parse({ + mode: "open-source", + oss: { + embedder: { provider: "openai", config: { apiKey: "", model: "custom-model" } }, + llm: { provider: "openai", config: { apiKey: "", model: "" } }, + disableHistory: true, + }, + }); + const api = { resolvePath: (p: string) => p } as any; + const provider = createProvider(cfg, api); + + await provider.search("test", { user_id: "u1" }); + + expect(capturedConfig).toBeDefined(); + // Empty apiKey should be stripped, leaving only the non-empty model + const embedderCfg = (capturedConfig!.embedder as any).config; + expect(embedderCfg.apiKey).toBeUndefined(); + expect(embedderCfg.model).toBe("custom-model"); + // Both empty keys in llm should be stripped, defaults applied + const llmCfg = (capturedConfig!.llm as any).config; + expect(llmCfg.apiKey).toBeUndefined(); + }); + + it("falls back to default provider when embedder/llm provider is empty", async () => { + const { createProvider } = await import("./index.ts"); + const cfg = mem0ConfigSchema.parse({ + mode: "open-source", + oss: { + embedder: { provider: "", config: { apiKey: "sk-e" } }, + llm: { provider: "", config: { apiKey: "sk-l" } }, + disableHistory: true, + }, + }); + const api = { resolvePath: (p: string) => p } as any; + const provider = createProvider(cfg, api); + + await provider.search("test", { user_id: "u1" }); + + expect(capturedConfig).toBeDefined(); + // Empty provider should fall back to "openai" default + expect((capturedConfig!.embedder as any).provider).toBe("openai"); + expect((capturedConfig!.llm as any).provider).toBe("openai"); + }); +}); + +// --------------------------------------------------------------------------- +// 7. OSSProvider: vector store dimension patching +// --------------------------------------------------------------------------- +describe("OSSProvider — vector store dimension patching", () => { + let capturedModule: any; + + beforeEach(() => { + vi.resetModules(); + + vi.doMock("mem0ai/oss", () => { + const mod = { + Memory: class MockMemory { + constructor() {} + async search() { return { results: [] }; } + async get() { return {}; } + async getAll() { return []; } + async add() { return { results: [] }; } + async delete() {} + }, + PGVector: class { + config: any; + dimension: any; + _initializePromise: any; + initialize() { return Promise.resolve("pg-initialized"); } + }, + RedisDB: class { + config: any; + _initializePromise: any; + initialize() { return Promise.resolve("redis-initialized"); } + }, + Qdrant: class { + config: any; + dimension: any; + _initializePromise: any; + initialize() { return Promise.resolve("qdrant-initialized"); } + }, + }; + capturedModule = mod; + return mod; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + async function triggerInit() { + const { createProvider } = await import("./index.ts"); + const cfg = mem0ConfigSchema.parse({ + mode: "open-source", + oss: { disableHistory: true }, + }); + const provider = createProvider(cfg, { resolvePath: (p: string) => p } as any); + await provider.search("test", { user_id: "u1" }); + } + + it("copies config.dimension to embeddingModelDims and this.dimension", async () => { + await triggerInit(); + + const pg = new capturedModule.PGVector(); + pg.config = { dimension: 1536 }; + await pg.initialize(); + + expect(pg.config.embeddingModelDims).toBe(1536); + expect(pg.dimension).toBe(1536); + }); + + it("returns resolved promise when no dimensions are known", async () => { + await triggerInit(); + + const pg = new capturedModule.PGVector(); + pg.config = {}; + const result = await pg.initialize(); + expect(result).toBeUndefined(); + }); + + it("runs original initialize only once via cached promise", async () => { + await triggerInit(); + + const q = new capturedModule.Qdrant(); + q.config = { dimension: 768 }; + + const first = await q.initialize(); + const second = await q.initialize(); + expect(first).toBe("qdrant-initialized"); + expect(second).toBe("qdrant-initialized"); + expect(q._initializePromise).toBeDefined(); + }); + + it("skips missing vector store classes without crashing", async () => { + // Override with a mock that omits PGVector entirely + vi.resetModules(); + vi.doMock("mem0ai/oss", () => ({ + Memory: class { + constructor() {} + async search() { return { results: [] }; } + async get() { return {}; } + async getAll() { return []; } + async add() { return { results: [] }; } + async delete() {} + }, + PGVector: undefined, // explicitly absent — tests the !VectorCls guard + RedisDB: class { initialize() { return Promise.resolve(); } }, + Qdrant: class { initialize() { return Promise.resolve(); } }, + })); + + const { createProvider } = await import("./index.ts"); + const cfg = mem0ConfigSchema.parse({ + mode: "open-source", + oss: { disableHistory: true }, + }); + const provider = createProvider(cfg, { resolvePath: (p: string) => p } as any); + + // Should not throw even though PGVector is missing + const results = await provider.search("test", { user_id: "u1" }); + expect(results).toBeDefined(); + }); +}); + +// --------------------------------------------------------------------------- +// 8. OSSProvider: history() error handler +// --------------------------------------------------------------------------- +describe("OSSProvider — history error handling", () => { + /** When set, the mock history() throws this value instead of an Error. */ + let historyThrowValue: unknown; + + beforeEach(() => { + historyThrowValue = new Error("history not available"); + vi.resetModules(); + + vi.doMock("mem0ai/oss", () => ({ + Memory: class MockMemory { + constructor() {} + async search() { return { results: [] }; } + async get() { return {}; } + async getAll() { return []; } + async add() { return { results: [] }; } + async delete() {} + async history() { throw historyThrowValue; } + }, + ...vectorStubs(), + })); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("returns empty array and warns when history() throws an Error", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { createProvider } = await import("./index.ts"); + const cfg = mem0ConfigSchema.parse({ + mode: "open-source", + oss: { disableHistory: true }, + }); + const api = { resolvePath: (p: string) => p } as any; + const provider = createProvider(cfg, api); + + await provider.search("test", { user_id: "u1" }); + + const result = await provider.history("mem-123"); + expect(result).toEqual([]); + expect(warnSpy).toHaveBeenCalledWith( + "[mem0] OSS history() failed:", + "history not available", + ); + warnSpy.mockRestore(); + }); + + it("handles non-Error thrown values in history()", async () => { + historyThrowValue = "raw string error"; + + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { createProvider } = await import("./index.ts"); + const cfg = mem0ConfigSchema.parse({ + mode: "open-source", + oss: { disableHistory: true }, + }); + const api = { resolvePath: (p: string) => p } as any; + const provider = createProvider(cfg, api); + + await provider.search("test", { user_id: "u1" }); + + const result = await provider.history("mem-456"); + expect(result).toEqual([]); + expect(warnSpy).toHaveBeenCalledWith( + "[mem0] OSS history() failed:", + "raw string error", + ); + warnSpy.mockRestore(); + }); +}); + +// --------------------------------------------------------------------------- +// 9. OSSProvider: customPrompt passthrough +// --------------------------------------------------------------------------- +describe("OSSProvider — customPrompt passthrough", () => { + let capturedConfig: Record | undefined; + + beforeEach(() => { + capturedConfig = undefined; + vi.resetModules(); + + vi.doMock("mem0ai/oss", () => ({ + Memory: class MockMemory { + constructor(config: Record) { + capturedConfig = { ...config }; + } + async search() { return { results: [] }; } + async get() { return {}; } + async getAll() { return []; } + async add() { return { results: [] }; } + async delete() {} + }, + ...vectorStubs(), + })); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("passes customPrompt to Memory config when provided", async () => { + const { createProvider } = await import("./index.ts"); + const cfg = mem0ConfigSchema.parse({ + mode: "open-source", + oss: { disableHistory: true }, + customPrompt: "Extract only user preferences.", + }); + const api = { resolvePath: (p: string) => p } as any; + const provider = createProvider(cfg, api); + + await provider.search("test", { user_id: "u1" }); + + expect(capturedConfig).toBeDefined(); + expect(capturedConfig!.customPrompt).toBe("Extract only user preferences."); + }); +}); diff --git a/openclaw/tests/cli-commands.test.ts b/openclaw/tests/cli-commands.test.ts index 9233f54d2..04d832b55 100644 --- a/openclaw/tests/cli-commands.test.ts +++ b/openclaw/tests/cli-commands.test.ts @@ -175,7 +175,6 @@ function createMockCfg() { apiKey: "m0-test-key-1234", baseUrl: "https://api.mem0.ai", topK: 5, - enableGraph: false, autoCapture: true, autoRecall: true, searchThreshold: 0.5, @@ -967,7 +966,7 @@ describe("registerCliCommands", () => { const configCmd = findCommand(mem0, "config")!; const getCmd = findCommand(configCmd, "get")!; - getCmd._action!("org_id"); + getCmd._action!("email"); expect(consoleSpy.log).toHaveBeenCalledWith("(not set)"); }); @@ -1033,18 +1032,6 @@ describe("registerCliCommands", () => { ); }); - it("coerces 'true' to boolean for boolean keys", () => { - const { mem0 } = setup(); - const configCmd = findCommand(mem0, "config")!; - const setCmd = findCommand(configCmd, "set")!; - - setCmd._action!("enable_graph", "true"); - - expect(writePluginAuth).toHaveBeenCalledWith( - expect.objectContaining({ enableGraph: true }), - ); - }); - it("coerces 'false' to boolean false for boolean keys", () => { const { mem0 } = setup(); const configCmd = findCommand(mem0, "config")!; @@ -1069,18 +1056,6 @@ describe("registerCliCommands", () => { ); }); - it("coerces 'yes' to boolean true for boolean keys", () => { - const { mem0 } = setup(); - const configCmd = findCommand(mem0, "config")!; - const setCmd = findCommand(configCmd, "set")!; - - setCmd._action!("enable_graph", "yes"); - - expect(writePluginAuth).toHaveBeenCalledWith( - expect.objectContaining({ enableGraph: true }), - ); - }); - it("coerces integer string for integer keys", () => { const { mem0 } = setup(); const configCmd = findCommand(mem0, "config")!; @@ -1412,5 +1387,73 @@ describe("registerCliCommands", () => { expect.stringContaining("Failed to get event"), ); }); + + it("event list returns early in open-source mode", async () => { + const provider = createMockProvider(); + const cfg = { ...createMockCfg(), mode: "open-source" as const }; + const mockApi = { + registerCli: vi.fn((cb: any) => { + const root = createMockCommand("root"); + cb({ program: root }); + const mem0 = findCommand(root, "mem0")!; + const eventCmd = findCommand(mem0, "event")!; + const listCmd = findCommand(eventCmd, "list")!; + listCmd._action!(); + }), + logger: { info: vi.fn(), warn: vi.fn() }, + } as any; + + registerCliCommands( + mockApi, + null as any, + provider as any, + cfg as any, + vi.fn().mockReturnValue("testuser"), + vi.fn((id: string) => `testuser:agent:${id}`), + vi.fn().mockReturnValue({ user_id: "testuser", top_k: 5 }), + vi.fn().mockReturnValue(undefined), + ); + + // Wait for async action + await new Promise((r) => setTimeout(r, 10)); + + expect(consoleSpy.log).toHaveBeenCalledWith( + "Event tracking is only available in platform mode.", + ); + }); + + it("event status returns early in open-source mode", async () => { + const provider = createMockProvider(); + const cfg = { ...createMockCfg(), mode: "open-source" as const }; + const mockApi = { + registerCli: vi.fn((cb: any) => { + const root = createMockCommand("root"); + cb({ program: root }); + const mem0 = findCommand(root, "mem0")!; + const eventCmd = findCommand(mem0, "event")!; + const statusCmd = findCommand(eventCmd, "status")!; + statusCmd._action!("evt-123"); + }), + logger: { info: vi.fn(), warn: vi.fn() }, + } as any; + + registerCliCommands( + mockApi, + null as any, + provider as any, + cfg as any, + vi.fn().mockReturnValue("testuser"), + vi.fn((id: string) => `testuser:agent:${id}`), + vi.fn().mockReturnValue({ user_id: "testuser", top_k: 5 }), + vi.fn().mockReturnValue(undefined), + ); + + // Wait for async action + await new Promise((r) => setTimeout(r, 10)); + + expect(consoleSpy.log).toHaveBeenCalledWith( + "Event tracking is only available in platform mode.", + ); + }); }); }); diff --git a/openclaw/tests/config-file.test.ts b/openclaw/tests/config-file.test.ts index 127a92415..f2d535757 100644 --- a/openclaw/tests/config-file.test.ts +++ b/openclaw/tests/config-file.test.ts @@ -69,10 +69,7 @@ describe("readPluginAuth", () => { apiKey: "sk-test-123", baseUrl: "https://custom.api.com", userId: "user-1", - orgId: "org-1", - projectId: "proj-1", mode: "platform", - enableGraph: true, autoRecall: true, autoCapture: false, topK: 10, @@ -87,17 +84,14 @@ describe("readPluginAuth", () => { apiKey: "sk-test-123", baseUrl: "https://custom.api.com", userId: "user-1", - orgId: "org-1", - projectId: "proj-1", mode: "platform", - enableGraph: true, autoRecall: true, autoCapture: false, topK: 10, }); }); - it("handles snake_case aliases (api_key, base_url, user_id, org_id, project_id)", () => { + it("handles snake_case aliases (api_key, base_url, user_id)", () => { setConfigFile({ plugins: { entries: { @@ -107,8 +101,6 @@ describe("readPluginAuth", () => { api_key: "sk-snake", base_url: "https://snake.api.com", user_id: "user-snake", - org_id: "org-snake", - project_id: "proj-snake", }, }, }, @@ -119,8 +111,6 @@ describe("readPluginAuth", () => { expect(auth.apiKey).toBe("sk-snake"); expect(auth.baseUrl).toBe("https://snake.api.com"); expect(auth.userId).toBe("user-snake"); - expect(auth.orgId).toBe("org-snake"); - expect(auth.projectId).toBe("proj-snake"); }); it("returns empty object when JSON is invalid", () => { diff --git a/openclaw/tests/config.test.ts b/openclaw/tests/config.test.ts index 188209311..7b420213a 100644 --- a/openclaw/tests/config.test.ts +++ b/openclaw/tests/config.test.ts @@ -64,11 +64,6 @@ describe("mem0ConfigSchema.parse() — defaults", () => { expect(cfg.searchThreshold).toBe(0.5); }); - it("enableGraph defaults to false", () => { - const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" }); - expect(cfg.enableGraph).toBe(false); - }); - it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => { const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" }); expect(cfg.customInstructions).toBe(DEFAULT_CUSTOM_INSTRUCTIONS); @@ -274,14 +269,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => { expect(cfg.autoRecall).toBe(false); }); - it("enableGraph can be set to true", () => { - const cfg = mem0ConfigSchema.parse({ - apiKey: "k", - enableGraph: true, - }); - expect(cfg.enableGraph).toBe(true); - }); - it("custom topK is used when provided", () => { const cfg = mem0ConfigSchema.parse({ apiKey: "k", topK: 20 }); expect(cfg.topK).toBe(20); @@ -330,18 +317,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => { expect(cfg.baseUrl).toBe("https://custom.api.com"); }); - it("orgId is passed through when provided", () => { - const cfg = mem0ConfigSchema.parse({ apiKey: "k", orgId: "org-123" }); - expect(cfg.orgId).toBe("org-123"); - }); - - it("projectId is passed through when provided", () => { - const cfg = mem0ConfigSchema.parse({ - apiKey: "k", - projectId: "proj-456", - }); - expect(cfg.projectId).toBe("proj-456"); - }); }); // --------------------------------------------------------------------------- @@ -359,22 +334,23 @@ describe("mem0ConfigSchema.parse() — oss config", () => { historyDbPath: "/tmp/history.db", disableHistory: false, }; - const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: ossConfig }); + const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: ossConfig }); + expect(cfg.mode).toBe("open-source"); expect(cfg.oss).toEqual(ossConfig); }); it("ignores oss when it is not a plain object", () => { - const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: "not-an-object" }); + const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: "not-an-object" }); expect(cfg.oss).toBeUndefined(); }); it("ignores oss when it is an array", () => { - const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: [1, 2, 3] }); + const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: [1, 2, 3] }); expect(cfg.oss).toBeUndefined(); }); it("ignores oss when it is null", () => { - const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: null }); + const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: null }); expect(cfg.oss).toBeUndefined(); }); }); @@ -388,7 +364,6 @@ describe("mem0ConfigSchema.parse() — skills config", () => { triage: { enabled: true, importanceThreshold: 3, - enableGraph: false, credentialPatterns: ["sk-", "ghp_"], }, recall: { diff --git a/openclaw/tests/providers.test.ts b/openclaw/tests/providers.test.ts index c4cc314ad..f586272e1 100644 --- a/openclaw/tests/providers.test.ts +++ b/openclaw/tests/providers.test.ts @@ -127,7 +127,6 @@ describe("providerToBackend — add", () => { immutable: true, infer: false, expires: "2027-01-01", - enableGraph: true, }); expect(provider.add).toHaveBeenCalledWith( @@ -139,7 +138,6 @@ describe("providerToBackend — add", () => { immutable: true, infer: false, expiration_date: "2027-01-01", - enable_graph: true, }), ); }); diff --git a/openclaw/tests/tools.test.ts b/openclaw/tests/tools.test.ts index 9515dbf50..bbddc29ce 100644 --- a/openclaw/tests/tools.test.ts +++ b/openclaw/tests/tools.test.ts @@ -32,7 +32,6 @@ function createMockToolDeps(overrides = {}): ToolDeps { mode: "platform", userId: "testuser", topK: 5, - enableGraph: false, autoCapture: true, autoRecall: true, searchThreshold: 0.5, diff --git a/openclaw/tools/memory-add.ts b/openclaw/tools/memory-add.ts index ae5af952f..46e2f20b2 100644 --- a/openclaw/tools/memory-add.ts +++ b/openclaw/tools/memory-add.ts @@ -67,7 +67,6 @@ export function createMemoryAddTool(deps: ToolDeps) { if (runId) addOpts.run_id = runId; if (cfg.mode === "platform") { addOpts.output_format = "v1.1"; - if (cfg.enableGraph || cfg.skills?.triage?.enableGraph) addOpts.enable_graph = true; } const result = await provider.add([{ role: "user", content: allFacts.join("\n") }], addOpts); diff --git a/openclaw/types.ts b/openclaw/types.ts index e9c32e793..2e375739f 100644 --- a/openclaw/types.ts +++ b/openclaw/types.ts @@ -9,11 +9,8 @@ export type Mem0Config = { // Platform-specific apiKey?: string; baseUrl?: string; - orgId?: string; - projectId?: string; customInstructions: string; customCategories: Record; - enableGraph: boolean; // OSS-specific customPrompt?: string; oss?: { @@ -40,7 +37,6 @@ export interface AddOptions { run_id?: string; custom_instructions?: string; custom_categories?: Array>; - enable_graph?: boolean; output_format?: string; source?: string; // Agentic harness additions @@ -79,7 +75,6 @@ export interface SkillsConfig { triage?: { enabled?: boolean; importanceThreshold?: number; - enableGraph?: boolean; credentialPatterns?: string[]; }; recall?: {