fix: adding skills config and updating the plugin the config (#4958)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: OpenClaw
|
title: OpenClaw
|
||||||
description: "Add long-term memory to OpenClaw agents using the Mem0 plugin with auto-recall and auto-capture support."
|
description: "Add long-term memory to OpenClaw agents using the Mem0 plugin with skills-based memory extraction and recall."
|
||||||
---
|
---
|
||||||
|
|
||||||
Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents with the `@mem0/openclaw-mem0` plugin. Your agent forgets everything between sessions — this plugin fixes that by automatically watching conversations, extracting what matters, and bringing it back when relevant.
|
Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents with the `@mem0/openclaw-mem0` plugin. Your agent forgets everything between sessions — this plugin fixes that by automatically watching conversations, extracting what matters, and bringing it back when relevant.
|
||||||
@@ -12,11 +12,12 @@ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents
|
|||||||
</Frame>
|
</Frame>
|
||||||
|
|
||||||
The plugin provides:
|
The plugin provides:
|
||||||
1. **Auto-Recall** — Before the agent responds, memories matching the current message are injected into context
|
1. **Triage** — The agent extracts durable facts from conversations using a structured protocol with importance gates and domain overlays
|
||||||
2. **Auto-Capture** — After the agent responds, the exchange is sent to Mem0 which decides what's worth keeping
|
2. **Recall** — Before each turn, relevant memories are retrieved with reranking and injected into context
|
||||||
3. **Agent Tools** — Eight tools for explicit memory operations during conversations
|
3. **Dream** — Periodic memory consolidation: merges duplicates, resolves conflicts, prunes stale entries
|
||||||
|
4. **Agent Tools** — Eight tools for explicit memory operations during conversations
|
||||||
|
|
||||||
Both auto-recall and auto-capture are opt-in (`autoRecall: true`, `autoCapture: true` in config). Once enabled, they run silently with no manual intervention required.
|
Skills mode, `autoRecall`, and `autoCapture` are all enabled by default during `openclaw mem0 init`.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -24,12 +25,12 @@ Check your OpenClaw version:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
openclaw --version
|
openclaw --version
|
||||||
# OpenClaw 2026.4.15 (041266a)
|
# OpenClaw 2026.4.25 (aa36ee6)
|
||||||
```
|
```
|
||||||
|
|
||||||
| OpenClaw Version | Plugin Support |
|
| OpenClaw Version | Plugin Support |
|
||||||
|------------------|----------------|
|
|------------------|----------------|
|
||||||
| `>= 2026.4.15` | Fully supported |
|
| `>= 2026.4.25` | Fully supported |
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -100,9 +101,9 @@ You no longer need manual config editing to get started. Everything happens insi
|
|||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
That's it. No API key, no config file editing, no environment variables. The plugin is now active and auto-capture and auto-recall are running on every turn.
|
That's it. No API key, no config file editing, no environment variables. The plugin is now active with skills-based memory (triage, recall, and dream) running automatically.
|
||||||
|
|
||||||
<Note>The chat flow uses the same underlying config as manual setup — it writes `apiKey` and `userId` into `openclaw.json` for you. You can still open the file to inspect or override values afterward.</Note>
|
<Note>The chat flow uses the same underlying config as manual setup — it writes `apiKey`, `userId`, and `skills` config into `openclaw.json` for you. You can still open the file to inspect or override values afterward.</Note>
|
||||||
|
|
||||||
#### Option 2: Manual Config
|
#### Option 2: Manual Config
|
||||||
|
|
||||||
@@ -131,7 +132,19 @@ That's it. No API key, no config file editing, no environment variables. The plu
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"config": {
|
"config": {
|
||||||
"apiKey": "${MEM0_API_KEY}",
|
"apiKey": "${MEM0_API_KEY}",
|
||||||
"userId": "alice" // any unique identifier you choose for this user
|
"userId": "alice", // any unique identifier you choose for this user
|
||||||
|
"skills": {
|
||||||
|
"triage": { "enabled": true },
|
||||||
|
"recall": {
|
||||||
|
"enabled": true,
|
||||||
|
"tokenBudget": 1500,
|
||||||
|
"rerank": true,
|
||||||
|
"keywordSearch": true,
|
||||||
|
"identityAlwaysInclude": true
|
||||||
|
},
|
||||||
|
"dream": { "enabled": true },
|
||||||
|
"domain": "companion"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,8 +341,8 @@ openclaw mem0 status --json
|
|||||||
|-----|------|---------|-------------|
|
|-----|------|---------|-------------|
|
||||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use |
|
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use |
|
||||||
| `userId` | `string` | OS username | Scope memories per user |
|
| `userId` | `string` | OS username | Scope memories per user |
|
||||||
| `autoRecall` | `boolean` | `false` | Inject memories before each turn (opt-in) |
|
| `autoRecall` | `boolean` | `true` | Inject memories before each turn. Ignored when `skills` is configured. |
|
||||||
| `autoCapture` | `boolean` | `false` | Store facts after each turn (opt-in) |
|
| `autoCapture` | `boolean` | `true` | Store facts after each turn. Ignored when `skills` is configured. |
|
||||||
| `topK` | `number` | `5` | Max memories per recall |
|
| `topK` | `number` | `5` | Max memories per recall |
|
||||||
| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
|
| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
|
||||||
|
|
||||||
@@ -426,9 +439,11 @@ If `openclaw plugins update` fails:
|
|||||||
| **Platform** | Conversations sent to `api.mem0.ai` for extraction and storage | Mem0 cloud |
|
| **Platform** | Conversations sent to `api.mem0.ai` for extraction and storage | Mem0 cloud |
|
||||||
| **Open-source** | Embeddings generated via configured provider (default: OpenAI API). Vectors stored locally. | `~/.mem0/vector_store.db` (SQLite) |
|
| **Open-source** | Embeddings generated via configured provider (default: OpenAI API). Vectors stored locally. | `~/.mem0/vector_store.db` (SQLite) |
|
||||||
|
|
||||||
### Enabling Auto-Capture and Auto-Recall
|
### Auto-Capture and Auto-Recall
|
||||||
|
|
||||||
Auto-capture and auto-recall are disabled by default (opt-in). To enable either or both:
|
Auto-capture and auto-recall are **enabled by default**. When skills mode is configured (the default after `openclaw mem0 init`), these are ignored in favor of the skills-based triage/recall/dream protocol.
|
||||||
|
|
||||||
|
To disable either:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
@@ -436,8 +451,8 @@ Auto-capture and auto-recall are disabled by default (opt-in). To enable either
|
|||||||
"entries": {
|
"entries": {
|
||||||
"openclaw-mem0": {
|
"openclaw-mem0": {
|
||||||
"config": {
|
"config": {
|
||||||
"autoCapture": true, // send conversations to Mem0 for extraction
|
"autoCapture": false, // disable automatic fact extraction
|
||||||
"autoRecall": true // inject relevant memories into context
|
"autoRecall": false // disable automatic memory injection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,7 +460,7 @@ Auto-capture and auto-recall are disabled by default (opt-in). To enable either
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Without these enabled, the agent can still use memory tools (`memory_add`, `memory_search`, etc.) explicitly — only the automatic background behavior is off.
|
The agent can always use memory tools (`memory_add`, `memory_search`, etc.) explicitly regardless of these settings.
|
||||||
|
|
||||||
### Credential Protection
|
### Credential Protection
|
||||||
|
|
||||||
|
|||||||
+59
-15
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
Long-term memory for [OpenClaw](https://github.com/openclaw/openclaw) agents, powered by [Mem0](https://mem0.ai).
|
Long-term memory for [OpenClaw](https://github.com/openclaw/openclaw) agents, powered by [Mem0](https://mem0.ai).
|
||||||
|
|
||||||
Your agent forgets everything between sessions. This plugin fixes that — it stores conversations, extracts what matters, and brings it back when relevant. Enable `autoRecall` and `autoCapture` in config to run this automatically, or use agent tools for explicit control.
|
Your agent forgets everything between sessions. This plugin fixes that — it stores conversations, extracts what matters, and brings it back when relevant.
|
||||||
|
|
||||||
|
By default, the plugin runs in **skills mode**: the agent controls what to remember (triage), how to recall (recall), and periodic cleanup (dream). Skills mode, `autoRecall`, and `autoCapture` are all enabled by default during `openclaw mem0 init`.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -10,12 +12,12 @@ Check your OpenClaw version:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
openclaw --version
|
openclaw --version
|
||||||
# OpenClaw 2026.4.15 (041266a)
|
# OpenClaw 2026.4.25 (aa36ee6)
|
||||||
```
|
```
|
||||||
|
|
||||||
| OpenClaw Version | Plugin Support |
|
| OpenClaw Version | Plugin Support |
|
||||||
|------------------|----------------|
|
|------------------|----------------|
|
||||||
| `>= 2026.4.15` | Fully supported |
|
| `>= 2026.4.25` | Fully supported |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -50,7 +52,19 @@ openclaw --version
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"config": {
|
"config": {
|
||||||
"apiKey": "${MEM0_API_KEY}",
|
"apiKey": "${MEM0_API_KEY}",
|
||||||
"userId": "alice"
|
"userId": "alice",
|
||||||
|
"skills": {
|
||||||
|
"triage": { "enabled": true },
|
||||||
|
"recall": {
|
||||||
|
"enabled": true,
|
||||||
|
"tokenBudget": 1500,
|
||||||
|
"rerank": true,
|
||||||
|
"keywordSearch": true,
|
||||||
|
"identityAlwaysInclude": true
|
||||||
|
},
|
||||||
|
"dream": { "enabled": true },
|
||||||
|
"domain": "companion"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,11 +196,24 @@ All `oss` fields are optional. See the [Mem0 OSS docs](https://docs.mem0.ai/open
|
|||||||
<img src="https://raw.githubusercontent.com/mem0ai/mem0/main/docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
|
<img src="https://raw.githubusercontent.com/mem0ai/mem0/main/docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Auto-Recall** (`autoRecall: true`) — Before the agent responds, the plugin searches Mem0 for relevant memories and injects them into context.
|
### Skills Mode (Default)
|
||||||
|
|
||||||
**Auto-Capture** (`autoCapture: true`) — After the agent responds, the conversation is filtered through a noise-removal pipeline and sent to Mem0. New facts get stored, stale ones updated, duplicates merged.
|
Enabled automatically during `openclaw mem0 init`. The agent controls memory through three skills:
|
||||||
|
|
||||||
Both are opt-in. Once enabled, they run silently — no prompting, no manual calls required. Without them, the agent can still use memory tools (`memory_add`, `memory_search`, etc.) explicitly.
|
- **Triage** — Extracts durable facts from conversations using a structured protocol. Categories, importance gates, and domain overlays control what gets stored.
|
||||||
|
- **Recall** — Before each turn, rewrites the user message into search queries, retrieves relevant memories with reranking, and injects them into context.
|
||||||
|
- **Dream** — Periodic memory consolidation: merges duplicates, resolves conflicts, and prunes stale entries.
|
||||||
|
|
||||||
|
When skills mode is active, the skills handle memory operations. `autoRecall` and `autoCapture` remain `true` by default alongside skills mode. The built-in `session-memory` hook is disabled to avoid conflicts.
|
||||||
|
|
||||||
|
### Auto-Recall & Auto-Capture
|
||||||
|
|
||||||
|
When skills mode is not configured, the plugin uses `autoRecall` and `autoCapture` (both enabled by default):
|
||||||
|
|
||||||
|
- **Auto-Recall** — Before the agent responds, the plugin searches Mem0 for relevant memories and injects them into context.
|
||||||
|
- **Auto-Capture** — After the agent responds, the conversation is filtered through a noise-removal pipeline and sent to Mem0. New facts get stored, stale ones updated, duplicates merged.
|
||||||
|
|
||||||
|
Set `autoRecall: false` or `autoCapture: false` to disable individually. The agent can also use memory tools (`memory_add`, `memory_search`, etc.) explicitly regardless of these settings.
|
||||||
|
|
||||||
### Memory Scopes
|
### Memory Scopes
|
||||||
|
|
||||||
@@ -260,10 +287,25 @@ openclaw mem0 help --json # discover all comma
|
|||||||
| --- | ---- | ------- | ----------- |
|
| --- | ---- | ------- | ----------- |
|
||||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
|
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
|
||||||
| `userId` | `string` | OS username | User identifier. All memories scoped to this value. |
|
| `userId` | `string` | OS username | User identifier. All memories scoped to this value. |
|
||||||
| `autoRecall` | `boolean` | `false` | Inject relevant memories before each turn |
|
| `autoRecall` | `boolean` | `true` | Inject relevant memories before each turn. Ignored when `skills` is set. |
|
||||||
| `autoCapture` | `boolean` | `false` | Extract and store facts after each turn |
|
| `autoCapture` | `boolean` | `true` | Extract and store facts after each turn. Ignored when `skills` is set. |
|
||||||
| `topK` | `number` | `5` | Max memories returned per recall |
|
| `topK` | `number` | `5` | Max memories returned per recall |
|
||||||
| `searchThreshold` | `number` | `0.3` | Minimum similarity score (0-1) |
|
| `searchThreshold` | `number` | `0.1` | Minimum similarity score (0-1) |
|
||||||
|
|
||||||
|
### Skills Mode (Recommended)
|
||||||
|
|
||||||
|
Enabled by default during `openclaw mem0 init`. `autoRecall` and `autoCapture` are also `true` by default and work alongside skills mode.
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --- | ---- | ------- | ----------- |
|
||||||
|
| `skills.triage.enabled` | `boolean` | `true` | Enable fact extraction from conversations |
|
||||||
|
| `skills.recall.enabled` | `boolean` | `true` | Enable memory recall before each turn |
|
||||||
|
| `skills.recall.tokenBudget` | `number` | `1500` | Max tokens for injected memories |
|
||||||
|
| `skills.recall.rerank` | `boolean` | `true` | Rerank search results for relevance |
|
||||||
|
| `skills.recall.keywordSearch` | `boolean` | `true` | Augment with keyword-based search |
|
||||||
|
| `skills.recall.identityAlwaysInclude` | `boolean` | `true` | Always include identity memories |
|
||||||
|
| `skills.dream.enabled` | `boolean` | `true` | Enable periodic memory consolidation |
|
||||||
|
| `skills.domain` | `string` | `"companion"` | Domain overlay for triage rules |
|
||||||
|
|
||||||
### Platform Mode
|
### Platform Mode
|
||||||
|
|
||||||
@@ -306,13 +348,15 @@ To avoid plaintext credentials:
|
|||||||
- Use env var references: `"apiKey": "${MEM0_API_KEY}"`
|
- Use env var references: `"apiKey": "${MEM0_API_KEY}"`
|
||||||
- Use SecretRef: `"apiKey": {"source": "env", "provider": "default", "id": "MEM0_API_KEY"}`
|
- Use SecretRef: `"apiKey": {"source": "env", "provider": "default", "id": "MEM0_API_KEY"}`
|
||||||
|
|
||||||
### Auto-Capture & Auto-Recall
|
### Memory Processing
|
||||||
|
|
||||||
Both are **disabled by default** (`false`). When enabled:
|
In **skills mode** (default after `openclaw mem0 init`), the agent uses structured protocols (triage, recall, dream) to decide what to store and recall. The built-in `session-memory` hook is disabled to avoid conflicts.
|
||||||
- `autoCapture`: sends conversation content to your configured backend (cloud or local) after each agent turn
|
|
||||||
- `autoRecall`: queries your memory store before each agent turn and injects results into agent context
|
|
||||||
|
|
||||||
Do not enable `autoCapture` in platform mode if your conversations contain sensitive data you do not want stored on Mem0 cloud.
|
Without skills, `autoCapture` and `autoRecall` are both enabled by default:
|
||||||
|
- `autoCapture`: sends conversation content to your configured backend after each agent turn
|
||||||
|
- `autoRecall`: queries your memory store before each agent turn and injects results into context
|
||||||
|
|
||||||
|
In platform mode, conversation content is sent to `api.mem0.ai` for processing. Do not use with sensitive data you do not want stored on Mem0 cloud.
|
||||||
|
|
||||||
### Persistence Locations
|
### Persistence Locations
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
readPluginAuth,
|
readPluginAuth,
|
||||||
writePluginAuth,
|
writePluginAuth,
|
||||||
writePluginConfigField,
|
writePluginConfigField,
|
||||||
|
enableSkillsConfig,
|
||||||
OPENCLAW_CONFIG_FILE,
|
OPENCLAW_CONFIG_FILE,
|
||||||
} from "./config-file.ts";
|
} from "./config-file.ts";
|
||||||
import { jsonOut, jsonErr, redactSecrets } from "./json-helpers.ts";
|
import { jsonOut, jsonErr, redactSecrets } from "./json-helpers.ts";
|
||||||
@@ -50,6 +51,7 @@ import {
|
|||||||
LLM_PROVIDERS, EMBEDDER_PROVIDERS, VECTOR_PROVIDERS,
|
LLM_PROVIDERS, EMBEDDER_PROVIDERS, VECTOR_PROVIDERS,
|
||||||
buildOssLlmConfig, buildOssEmbedderConfig, buildOssVectorConfig,
|
buildOssLlmConfig, buildOssEmbedderConfig, buildOssVectorConfig,
|
||||||
validateOssFlags, checkQdrantConnectivity, checkOllamaConnectivity, checkPgConnectivity,
|
validateOssFlags, checkQdrantConnectivity, checkOllamaConnectivity, checkPgConnectivity,
|
||||||
|
collectionNameForDims,
|
||||||
} from "./oss-wizard.ts";
|
} from "./oss-wizard.ts";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -213,10 +215,11 @@ function saveLoginConfig(
|
|||||||
const userId = resolveUserId(userIdFlag, existingAuth.userId);
|
const userId = resolveUserId(userIdFlag, existingAuth.userId);
|
||||||
|
|
||||||
writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) });
|
writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) });
|
||||||
|
enableSkillsConfig(userId);
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
|
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
|
||||||
console.log(` Mode: platform`);
|
console.log(` Mode: platform (skills enabled)`);
|
||||||
console.log(` User ID: ${userId}`);
|
console.log(` User ID: ${userId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,10 +229,11 @@ function saveOssConfig(userIdFlag?: string, silent?: boolean): void {
|
|||||||
const userId = resolveUserId(userIdFlag, existingAuth.userId);
|
const userId = resolveUserId(userIdFlag, existingAuth.userId);
|
||||||
|
|
||||||
writePluginAuth({ apiKey: "", userId, mode: "open-source" });
|
writePluginAuth({ apiKey: "", userId, mode: "open-source" });
|
||||||
|
enableSkillsConfig(userId);
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
|
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
|
||||||
console.log(` Mode: open-source`);
|
console.log(` Mode: open-source (skills enabled)`);
|
||||||
console.log(` User ID: ${userId}`);
|
console.log(` User ID: ${userId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,6 +354,17 @@ async function runOssWizardInteractive(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const vecCfg = buildOssVectorConfig(vecDef.id, vecInput as any);
|
const vecCfg = buildOssVectorConfig(vecDef.id, vecInput as any);
|
||||||
|
|
||||||
|
// Warn if switching embedder dimensions — old collection will have wrong vector size
|
||||||
|
const existingVecCfg = existingAuth as any;
|
||||||
|
const oldDims = existingVecCfg?.oss?.vectorStore?.config?.dimension as number | undefined;
|
||||||
|
if (oldDims && dims && oldDims !== dims) {
|
||||||
|
console.log(`\n ⚠ Dimension change detected: ${oldDims} → ${dims}`);
|
||||||
|
console.log(` Old collection had ${oldDims}-dim vectors. New embedder produces ${dims}-dim vectors.`);
|
||||||
|
console.log(` A new collection "${collectionNameForDims(dims)}" will be created.`);
|
||||||
|
console.log(` Old memories in the previous collection will NOT be accessible with the new embedder.\n`);
|
||||||
|
}
|
||||||
|
|
||||||
writePluginConfigField(["oss", "vectorStore"], vecCfg);
|
writePluginConfigField(["oss", "vectorStore"], vecCfg);
|
||||||
|
|
||||||
// === Step 4: User ID ===
|
// === Step 4: User ID ===
|
||||||
@@ -370,6 +385,8 @@ async function runOssWizardInteractive(
|
|||||||
console.log(` LLM: ${llmDef.id} (${llmCfg.config.model})`);
|
console.log(` LLM: ${llmDef.id} (${llmCfg.config.model})`);
|
||||||
console.log(` Embedder: ${embDef.id} (${embCfg.config.model})`);
|
console.log(` Embedder: ${embDef.id} (${embCfg.config.model})`);
|
||||||
console.log(` Vector: ${vecDef.id} (${vecDef.id === "qdrant" ? vecCfg.config.url : vecCfg.config.host})`);
|
console.log(` Vector: ${vecDef.id} (${vecDef.id === "qdrant" ? vecCfg.config.url : vecCfg.config.host})`);
|
||||||
|
console.log(` Dims: ${dims ?? "unknown"}`);
|
||||||
|
console.log(` Collection:${dims ? " " + collectionNameForDims(dims) : " (default)"}`);
|
||||||
console.log(` User ID: ${userIdValue}`);
|
console.log(` User ID: ${userIdValue}`);
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log(" Run: openclaw gateway restart");
|
console.log(" Run: openclaw gateway restart");
|
||||||
@@ -539,6 +556,15 @@ export function registerCliCommands(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warn on dimension change
|
||||||
|
const prevAuth = readPluginAuth() as any;
|
||||||
|
const prevDims = prevAuth?.oss?.vectorStore?.config?.dimension as number | undefined;
|
||||||
|
const newDims = dims;
|
||||||
|
let dimWarning: string | undefined;
|
||||||
|
if (prevDims && newDims && prevDims !== newDims) {
|
||||||
|
dimWarning = `Dimension change: ${prevDims} → ${newDims}. New collection "${collectionNameForDims(newDims)}" will be used. Old memories not accessible with new embedder.`;
|
||||||
|
}
|
||||||
|
|
||||||
writePluginConfigField(["oss", "llm"], llmCfg);
|
writePluginConfigField(["oss", "llm"], llmCfg);
|
||||||
writePluginConfigField(["oss", "embedder"], { provider: embCfg.provider, config: embCfg.config });
|
writePluginConfigField(["oss", "embedder"], { provider: embCfg.provider, config: embCfg.config });
|
||||||
writePluginConfigField(["oss", "vectorStore"], vecCfg);
|
writePluginConfigField(["oss", "vectorStore"], vecCfg);
|
||||||
@@ -550,9 +576,10 @@ export function registerCliCommands(
|
|||||||
mode: "open-source",
|
mode: "open-source",
|
||||||
config: {
|
config: {
|
||||||
llm: { provider: llmCfg.provider, model: llmCfg.config.model },
|
llm: { provider: llmCfg.provider, model: llmCfg.config.model },
|
||||||
embedder: { provider: embCfg.provider, model: embCfg.config.model },
|
embedder: { provider: embCfg.provider, model: embCfg.config.model, dims: newDims },
|
||||||
vectorStore: { provider: vecCfg.provider, ...(vecId === "qdrant" ? { url: vecCfg.config.url } : { host: vecCfg.config.host }) },
|
vectorStore: { provider: vecCfg.provider, ...(vecId === "qdrant" ? { url: vecCfg.config.url } : { host: vecCfg.config.host }), collectionName: newDims ? collectionNameForDims(newDims) : undefined },
|
||||||
},
|
},
|
||||||
|
...(dimWarning && { warning: dimWarning }),
|
||||||
userId: resolveUserId(opts.userId, existingAuth.userId),
|
userId: resolveUserId(opts.userId, existingAuth.userId),
|
||||||
message: "Open-source mode configured. Restart the gateway: openclaw gateway restart",
|
message: "Open-source mode configured. Restart the gateway: openclaw gateway restart",
|
||||||
};
|
};
|
||||||
@@ -889,7 +916,7 @@ export function registerCliCommands(
|
|||||||
runId?: string,
|
runId?: string,
|
||||||
): SearchOptions => {
|
): SearchOptions => {
|
||||||
const base = buildSearchOptions(userIdOverride, lim, runId);
|
const base = buildSearchOptions(userIdOverride, lim, runId);
|
||||||
base.threshold = 0.3;
|
base.threshold = 0.1;
|
||||||
return base;
|
return base;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+78
-78
@@ -19,7 +19,6 @@ export const OPENCLAW_CONFIG_FILE = join(OPENCLAW_CONFIG_DIR, "openclaw.json");
|
|||||||
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
|
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
|
||||||
|
|
||||||
const PLUGIN_ID = "openclaw-mem0";
|
const PLUGIN_ID = "openclaw-mem0";
|
||||||
const NPM_PACKAGE = "@mem0/openclaw-mem0";
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@@ -76,11 +75,44 @@ function readFullConfig(): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Write the full ~/.openclaw/openclaw.json (preserves all non-plugin config) */
|
/**
|
||||||
|
* Write the full ~/.openclaw/openclaw.json.
|
||||||
|
*
|
||||||
|
* Re-reads the file immediately before writing and deep-merges the
|
||||||
|
* `plugins` section so that fields written by other processes (e.g.
|
||||||
|
* OpenClaw gateway adding `installs`, `slots`) are not clobbered.
|
||||||
|
*/
|
||||||
function writeFullConfig(config: Record<string, unknown>): void {
|
function writeFullConfig(config: Record<string, unknown>): void {
|
||||||
if (!exists(OPENCLAW_CONFIG_DIR)) {
|
if (!exists(OPENCLAW_CONFIG_DIR)) {
|
||||||
mkdirp(OPENCLAW_CONFIG_DIR, 0o700);
|
mkdirp(OPENCLAW_CONFIG_DIR, 0o700);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exists(OPENCLAW_CONFIG_FILE)) {
|
||||||
|
try {
|
||||||
|
const diskText = readText(OPENCLAW_CONFIG_FILE);
|
||||||
|
if (diskText.trim()) {
|
||||||
|
const disk = JSON.parse(diskText) as Record<string, unknown>;
|
||||||
|
const diskPlugins = disk.plugins as Record<string, unknown> | undefined;
|
||||||
|
const ourPlugins = config.plugins as Record<string, unknown> | undefined;
|
||||||
|
if (diskPlugins && ourPlugins) {
|
||||||
|
const OPENCLAW_MANAGED = ["installs", "slots"];
|
||||||
|
for (const key of OPENCLAW_MANAGED) {
|
||||||
|
if (key in diskPlugins) {
|
||||||
|
ourPlugins[key] = diskPlugins[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(diskPlugins)) {
|
||||||
|
if (!(key in ourPlugins)) {
|
||||||
|
ourPlugins[key] = diskPlugins[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// disk unreadable — write our version as-is
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeText(
|
writeText(
|
||||||
OPENCLAW_CONFIG_FILE,
|
OPENCLAW_CONFIG_FILE,
|
||||||
JSON.stringify(config, null, 2),
|
JSON.stringify(config, null, 2),
|
||||||
@@ -122,82 +154,6 @@ export function writePluginAuth(auth: PluginAuthConfig): void {
|
|||||||
writeFullConfig(full);
|
writeFullConfig(full);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the plugin has a valid install record and is in plugins.allow.
|
|
||||||
*
|
|
||||||
* OpenClaw's `plugins update` command requires a `plugins.installs.<id>`
|
|
||||||
* record with `source: "npm"` and `spec` to know how to update. Without
|
|
||||||
* this, `openclaw plugins update` prints "No install record" and skips.
|
|
||||||
*
|
|
||||||
* Similarly, if `plugins.allow` exists as an array, the plugin ID must
|
|
||||||
* be in it or OpenClaw treats the plugin as untrusted.
|
|
||||||
*
|
|
||||||
* This is safe to call multiple times — it only writes missing fields.
|
|
||||||
*/
|
|
||||||
export function ensureInstallRecord(): void {
|
|
||||||
try {
|
|
||||||
const full = readFullConfig() as any;
|
|
||||||
|
|
||||||
const entry = full?.plugins?.entries?.[PLUGIN_ID];
|
|
||||||
const record = full?.plugins?.installs?.[PLUGIN_ID];
|
|
||||||
const allow = full?.plugins?.allow;
|
|
||||||
const specPinned = record?.spec && /\d+\.\d+\.\d+/.test(record.spec);
|
|
||||||
if (
|
|
||||||
entry?.enabled === true &&
|
|
||||||
record?.source &&
|
|
||||||
record?.spec &&
|
|
||||||
!specPinned &&
|
|
||||||
Array.isArray(allow) &&
|
|
||||||
allow.includes(PLUGIN_ID)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ensurePluginStructure(full);
|
|
||||||
|
|
||||||
let changed = false;
|
|
||||||
|
|
||||||
// Ensure install record exists for `openclaw plugins update` support
|
|
||||||
if (!full.plugins.installs) full.plugins.installs = {};
|
|
||||||
if (!full.plugins.installs[PLUGIN_ID]) {
|
|
||||||
full.plugins.installs[PLUGIN_ID] = {
|
|
||||||
source: "npm",
|
|
||||||
spec: `${NPM_PACKAGE}@latest`,
|
|
||||||
resolvedName: NPM_PACKAGE,
|
|
||||||
installedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
changed = true;
|
|
||||||
} else {
|
|
||||||
const record = full.plugins.installs[PLUGIN_ID];
|
|
||||||
if (!record.source) {
|
|
||||||
record.source = "npm";
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (!record.spec || /\d+\.\d+\.\d+/.test(record.spec)) {
|
|
||||||
record.spec = record.source === "clawhub"
|
|
||||||
? `clawhub:${NPM_PACKAGE}`
|
|
||||||
: `${NPM_PACKAGE}@latest`;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (!record.resolvedName) {
|
|
||||||
record.resolvedName = NPM_PACKAGE;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(full.plugins.allow)) {
|
|
||||||
full.plugins.allow = [PLUGIN_ID];
|
|
||||||
changed = true;
|
|
||||||
} else if (!full.plugins.allow.includes(PLUGIN_ID)) {
|
|
||||||
full.plugins.allow.push(PLUGIN_ID);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) writeFullConfig(full);
|
|
||||||
} catch {
|
|
||||||
// Best-effort — don't break plugin loading if config is unreadable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ensure the nested plugin entry structure exists in the config object. */
|
/** Ensure the nested plugin entry structure exists in the config object. */
|
||||||
function ensurePluginStructure(full: any): void {
|
function ensurePluginStructure(full: any): void {
|
||||||
@@ -231,6 +187,50 @@ export function writePluginConfigField(
|
|||||||
writeFullConfig(full);
|
writeFullConfig(full);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default skills configuration — matches configure.py output.
|
||||||
|
* Enables triage, recall (with reranking), and dream consolidation.
|
||||||
|
*/
|
||||||
|
const DEFAULT_SKILLS_CONFIG = {
|
||||||
|
triage: { enabled: true },
|
||||||
|
recall: {
|
||||||
|
enabled: true,
|
||||||
|
tokenBudget: 1500,
|
||||||
|
rerank: true,
|
||||||
|
keywordSearch: true,
|
||||||
|
identityAlwaysInclude: true,
|
||||||
|
},
|
||||||
|
dream: { enabled: true },
|
||||||
|
domain: "companion",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable skills-mode config after onboarding.
|
||||||
|
*
|
||||||
|
* Sets skills config on the plugin entry, tools.profile = "full",
|
||||||
|
* and disables the built-in session-memory hook to avoid conflicts.
|
||||||
|
* Preserves any existing skills config if already set.
|
||||||
|
*/
|
||||||
|
export function enableSkillsConfig(userId: string): void {
|
||||||
|
const full = readFullConfig() as any;
|
||||||
|
ensurePluginStructure(full);
|
||||||
|
|
||||||
|
const cfg = full.plugins.entries[PLUGIN_ID].config;
|
||||||
|
if (!cfg.skills) {
|
||||||
|
cfg.skills = { ...DEFAULT_SKILLS_CONFIG };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!full.tools) full.tools = {};
|
||||||
|
full.tools.profile = "full";
|
||||||
|
|
||||||
|
if (!full.hooks) full.hooks = {};
|
||||||
|
if (!full.hooks.internal) full.hooks.internal = {};
|
||||||
|
if (!full.hooks.internal.entries) full.hooks.internal.entries = {};
|
||||||
|
full.hooks.internal.entries["session-memory"] = { enabled: false };
|
||||||
|
|
||||||
|
writeFullConfig(full);
|
||||||
|
}
|
||||||
|
|
||||||
/** Get the configured base URL from openclaw.json or default */
|
/** Get the configured base URL from openclaw.json or default */
|
||||||
export function getBaseUrl(): string {
|
export function getBaseUrl(): string {
|
||||||
const auth = readPluginAuth();
|
const auth = readPluginAuth();
|
||||||
|
|||||||
@@ -56,8 +56,15 @@ export const KNOWN_EMBEDDER_DIMS: Record<string, number> = {
|
|||||||
"text-embedding-3-large": 3072,
|
"text-embedding-3-large": 3072,
|
||||||
"text-embedding-ada-002": 1536,
|
"text-embedding-ada-002": 1536,
|
||||||
"nomic-embed-text": 768,
|
"nomic-embed-text": 768,
|
||||||
|
"mxbai-embed-large": 1024,
|
||||||
|
"all-minilm": 384,
|
||||||
|
"snowflake-arctic-embed": 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function collectionNameForDims(dims: number): string {
|
||||||
|
return `mem0_${dims}d`;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Config builders
|
// Config builders
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -105,7 +112,8 @@ export function buildOssEmbedderConfig(
|
|||||||
config.url = input.url || def.defaultUrl;
|
config.url = input.url || def.defaultUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dims = KNOWN_EMBEDDER_DIMS[model] ?? undefined;
|
const dims = KNOWN_EMBEDDER_DIMS[model] ?? def.defaultDims;
|
||||||
|
if (dims) config.embeddingDims = dims;
|
||||||
return { provider: providerId, config, dims };
|
return { provider: providerId, config, dims };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +146,11 @@ export function buildOssVectorConfig(
|
|||||||
config.dbname = input.dbname || "postgres";
|
config.dbname = input.dbname || "postgres";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.dims) config.dimension = input.dims;
|
if (input.dims) {
|
||||||
|
config.dimension = input.dims;
|
||||||
|
config.embeddingModelDims = input.dims;
|
||||||
|
config.collectionName = collectionNameForDims(input.dims);
|
||||||
|
}
|
||||||
return { provider: providerId, config };
|
return { provider: providerId, config };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -231,8 +231,8 @@ export const mem0ConfigSchema = {
|
|||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
autoCapture: cfg.autoCapture === true,
|
autoCapture: cfg.autoCapture !== false,
|
||||||
autoRecall: cfg.autoRecall === true,
|
autoRecall: cfg.autoRecall !== false,
|
||||||
// v3.0.0: customPrompt renamed to customInstructions (backwards-compat: accept either)
|
// v3.0.0: customPrompt renamed to customInstructions (backwards-compat: accept either)
|
||||||
customInstructions:
|
customInstructions:
|
||||||
typeof cfg.customInstructions === "string"
|
typeof cfg.customInstructions === "string"
|
||||||
@@ -247,7 +247,7 @@ export const mem0ConfigSchema = {
|
|||||||
? (cfg.customCategories as Record<string, string>)
|
? (cfg.customCategories as Record<string, string>)
|
||||||
: DEFAULT_CUSTOM_CATEGORIES,
|
: DEFAULT_CUSTOM_CATEGORIES,
|
||||||
searchThreshold:
|
searchThreshold:
|
||||||
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5,
|
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.1,
|
||||||
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
|
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
|
||||||
needsSetup,
|
needsSetup,
|
||||||
oss: ossConfig,
|
oss: ossConfig,
|
||||||
|
|||||||
@@ -561,3 +561,52 @@ What is the deployment plan?`,
|
|||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Auto-recall threshold filtering
|
||||||
|
// The recall hook in index.ts filters search results using cfg.searchThreshold.
|
||||||
|
// These tests verify the threshold is honored and no hardcoded floor overrides it.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe("auto-recall threshold respects cfg.searchThreshold", () => {
|
||||||
|
const typicalV3Results = [
|
||||||
|
{ id: "1", score: 0.553, memory: "User prefers dark mode" },
|
||||||
|
{ id: "2", score: 0.496, memory: "User works on mem0 project" },
|
||||||
|
{ id: "3", score: 0.471, memory: "User likes TypeScript" },
|
||||||
|
{ id: "4", score: 0.45, memory: "User's timezone is PST" },
|
||||||
|
{ id: "5", score: 0.42, memory: "User uses VS Code" },
|
||||||
|
{ id: "6", score: 0.35, memory: "User mentioned family trip" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function applyThresholdFilter(
|
||||||
|
results: typeof typicalV3Results,
|
||||||
|
searchThreshold: number,
|
||||||
|
) {
|
||||||
|
return results.filter((r) => (r.score ?? 0) >= searchThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("default 0.5 threshold returns results scoring >= 0.5", () => {
|
||||||
|
const filtered = applyThresholdFilter(typicalV3Results, 0.5);
|
||||||
|
expect(filtered).toHaveLength(1);
|
||||||
|
expect(filtered[0].id).toBe("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("threshold 0.4 returns results scoring >= 0.4", () => {
|
||||||
|
const filtered = applyThresholdFilter(typicalV3Results, 0.4);
|
||||||
|
expect(filtered).toHaveLength(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("threshold 0.3 returns all results", () => {
|
||||||
|
const filtered = applyThresholdFilter(typicalV3Results, 0.3);
|
||||||
|
expect(filtered).toHaveLength(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("threshold 0.6 correctly filters everything below", () => {
|
||||||
|
const filtered = applyThresholdFilter(typicalV3Results, 0.6);
|
||||||
|
expect(filtered).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("threshold 0 returns all results", () => {
|
||||||
|
const filtered = applyThresholdFilter(typicalV3Results, 0);
|
||||||
|
expect(filtered).toHaveLength(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+55
-11
@@ -54,15 +54,12 @@ import {
|
|||||||
import { PlatformBackend } from "./backend/platform.ts";
|
import { PlatformBackend } from "./backend/platform.ts";
|
||||||
import type { Backend } from "./backend/base.ts";
|
import type { Backend } from "./backend/base.ts";
|
||||||
import { registerCliCommands } from "./cli/commands.ts";
|
import { registerCliCommands } from "./cli/commands.ts";
|
||||||
import { readPluginAuth, ensureInstallRecord } from "./cli/config-file.ts";
|
import { readPluginAuth } from "./cli/config-file.ts";
|
||||||
import { registerAllTools } from "./tools/index.ts";
|
import { registerAllTools } from "./tools/index.ts";
|
||||||
import type { ToolDeps } from "./tools/index.ts";
|
import type { ToolDeps } from "./tools/index.ts";
|
||||||
import { captureEvent } from "./telemetry.ts";
|
import { captureEvent } from "./telemetry.ts";
|
||||||
import { bootstrapTelemetryFlag } from "./fs-safe.ts";
|
import { bootstrapTelemetryFlag } from "./fs-safe.ts";
|
||||||
|
|
||||||
bootstrapTelemetryFlag();
|
|
||||||
ensureInstallRecord();
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Re-exports (for tests and external consumers)
|
// Re-exports (for tests and external consumers)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -100,6 +97,8 @@ const memoryPlugin = definePluginEntry({
|
|||||||
description: "Mem0 memory backend — Mem0 platform or self-hosted open-source",
|
description: "Mem0 memory backend — Mem0 platform or self-hosted open-source",
|
||||||
|
|
||||||
register(api: OpenClawPluginApi) {
|
register(api: OpenClawPluginApi) {
|
||||||
|
bootstrapTelemetryFlag();
|
||||||
|
|
||||||
// Read auth from openclaw.json plugin config (picks up post-startup login).
|
// Read auth from openclaw.json plugin config (picks up post-startup login).
|
||||||
// This is the single source of truth — set via `openclaw mem0 login`.
|
// This is the single source of truth — set via `openclaw mem0 login`.
|
||||||
const pluginAuth = readPluginAuth();
|
const pluginAuth = readPluginAuth();
|
||||||
@@ -207,8 +206,57 @@ const memoryPlugin = definePluginEntry({
|
|||||||
},
|
},
|
||||||
effectiveUserId: _effectiveUserId,
|
effectiveUserId: _effectiveUserId,
|
||||||
}),
|
}),
|
||||||
|
runtime: {
|
||||||
|
async getMemorySearchManager(_params: any) {
|
||||||
|
try {
|
||||||
|
const userId = _effectiveUserId();
|
||||||
|
let memoryCount = 0;
|
||||||
|
try {
|
||||||
|
const memories = await provider.getAll({
|
||||||
|
user_id: userId,
|
||||||
|
page_size: 1,
|
||||||
|
source: "OPENCLAW",
|
||||||
|
});
|
||||||
|
memoryCount = Array.isArray(memories) ? memories.length : 0;
|
||||||
|
} catch {
|
||||||
|
// Non-fatal: status still works without count
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
manager: {
|
||||||
|
status() {
|
||||||
|
return {
|
||||||
|
backend: cfg.mode,
|
||||||
|
files: 0,
|
||||||
|
chunks: memoryCount,
|
||||||
|
dirty: false,
|
||||||
|
workspaceDir: pluginStateDir ?? "",
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async probeEmbeddingAvailability() {
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
async close() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
manager: null,
|
||||||
|
error: `mem0 ${cfg.mode} backend unavailable: ${String(err)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolveMemoryBackendConfig(_params: any) {
|
||||||
|
return {
|
||||||
|
backend: cfg.mode,
|
||||||
|
baseUrl: cfg.baseUrl ?? "https://api.mem0.ai",
|
||||||
|
userId: cfg.userId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async closeAllMemorySearchManagers() {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
api.logger.debug("openclaw-mem0: publicArtifacts capability registered");
|
api.logger.debug("openclaw-mem0: memory capability + runtime registered");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: build add options
|
// Helper: build add options
|
||||||
@@ -681,12 +729,8 @@ function registerHooks(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Client-side threshold filter for auto-recall — use a stricter
|
|
||||||
// threshold (0.6) than explicit tool searches (0.5) to avoid
|
|
||||||
// injecting irrelevant memories into agent context
|
|
||||||
const recallThreshold = Math.max(cfg.searchThreshold, 0.6);
|
|
||||||
longTermResults = longTermResults.filter(
|
longTermResults = longTermResults.filter(
|
||||||
(r) => (r.score ?? 0) >= recallThreshold,
|
(r) => (r.score ?? 0) >= cfg.searchThreshold,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Dynamic thresholding: drop memories scoring less than 50% of
|
// Dynamic thresholding: drop memories scoring less than 50% of
|
||||||
@@ -709,7 +753,7 @@ function registerHooks(
|
|||||||
undefined,
|
undefined,
|
||||||
recallSessionKey,
|
recallSessionKey,
|
||||||
);
|
);
|
||||||
broadOpts.threshold = 0.5;
|
broadOpts.threshold = cfg.searchThreshold;
|
||||||
const broadResults = await provider.search(
|
const broadResults = await provider.search(
|
||||||
"recent decisions, preferences, active projects, and configuration",
|
"recent decisions, preferences, active projects, and configuration",
|
||||||
broadOpts,
|
broadOpts,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "openclaw-mem0",
|
"id": "openclaw-mem0",
|
||||||
"name": "Memory (Mem0)",
|
"name": "Memory (Mem0)",
|
||||||
"description": "Mem0 memory backend for OpenClaw — platform (mem0.ai cloud) or self-hosted open-source. Auto-recall and auto-capture are opt-in (disabled by default). Supports OpenAI, Anthropic, Ollama (fully local), Qdrant, and PGVector providers.",
|
"description": "Mem0 memory backend for OpenClaw — platform (mem0.ai cloud) or self-hosted open-source. Auto-recall and auto-capture are opt-in (disabled by default). Supports OpenAI, Anthropic, Ollama (fully local), Qdrant, and PGVector providers.",
|
||||||
"version": "1.0.10",
|
"version": "1.0.11",
|
||||||
"kind": "memory",
|
"kind": "memory",
|
||||||
"skills": ["skills"],
|
"skills": ["skills"],
|
||||||
"commandAliases": [
|
"commandAliases": [
|
||||||
@@ -17,9 +17,17 @@
|
|||||||
"memory_update", "memory_delete", "memory_event_list", "memory_event_status"
|
"memory_update", "memory_delete", "memory_event_list", "memory_event_status"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"providerAuthEnvVars": {
|
"setup": {
|
||||||
"mem0": ["MEM0_API_KEY"],
|
"providers": [
|
||||||
"openclaw-mem0-oss": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]
|
{
|
||||||
|
"id": "mem0",
|
||||||
|
"envVars": ["MEM0_API_KEY"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "openclaw-mem0-oss",
|
||||||
|
"envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"providerAuthChoices": [
|
"providerAuthChoices": [
|
||||||
{
|
{
|
||||||
@@ -171,13 +179,13 @@
|
|||||||
},
|
},
|
||||||
"autoCapture": {
|
"autoCapture": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": true,
|
||||||
"description": "Opt-in. When true, extracts durable facts after each agent turn. Disabled by default."
|
"description": "When true, extracts durable facts after each agent turn. Enabled by default. Ignored in skills mode."
|
||||||
},
|
},
|
||||||
"autoRecall": {
|
"autoRecall": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": true,
|
||||||
"description": "Opt-in. When true, injects relevant memories before each agent turn. Disabled by default."
|
"description": "When true, injects relevant memories before each agent turn. Enabled by default. Ignored in skills mode."
|
||||||
},
|
},
|
||||||
"customInstructions": {
|
"customInstructions": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mem0/openclaw-mem0",
|
"name": "@mem0/openclaw-mem0",
|
||||||
"version": "1.0.10",
|
"version": "1.0.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -35,19 +35,19 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sinclair/typebox": "0.34.47",
|
"@sinclair/typebox": "0.34.47",
|
||||||
"mem0ai": "3.0.1"
|
"mem0ai": "3.0.2"
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"./dist/index.js"
|
"./dist/index.js"
|
||||||
],
|
],
|
||||||
"compat": {
|
"compat": {
|
||||||
"pluginApi": ">=2026.3.28",
|
"pluginApi": ">=2026.4.24",
|
||||||
"minGatewayVersion": ">=2026.3.28"
|
"minGatewayVersion": ">=2026.4.24"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"openclawVersion": "2026.4.1",
|
"openclawVersion": "2026.4.24",
|
||||||
"pluginSdkVersion": "2026.4.1"
|
"pluginSdkVersion": "2026.4.24"
|
||||||
},
|
},
|
||||||
"install": {
|
"install": {
|
||||||
"npmSpec": "@mem0/openclaw-mem0"
|
"npmSpec": "@mem0/openclaw-mem0"
|
||||||
|
|||||||
Generated
+5
-5
@@ -15,8 +15,8 @@ importers:
|
|||||||
specifier: 0.34.47
|
specifier: 0.34.47
|
||||||
version: 0.34.47
|
version: 0.34.47
|
||||||
mem0ai:
|
mem0ai:
|
||||||
specifier: 3.0.1
|
specifier: 3.0.2
|
||||||
version: 3.0.1(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0)
|
version: 3.0.2(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.15.0
|
specifier: ^22.15.0
|
||||||
@@ -1508,8 +1508,8 @@ packages:
|
|||||||
md5@2.3.0:
|
md5@2.3.0:
|
||||||
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
||||||
|
|
||||||
mem0ai@3.0.1:
|
mem0ai@3.0.2:
|
||||||
resolution: {integrity: sha512-6phM544/3NRcCg7n5DBNRc9uUEMqGTe7sksULM2KM/FTihH27yTl5nPms8cnNpSKM5/ZKJ9jnkpzwGlvYYbtBQ==}
|
resolution: {integrity: sha512-smB9q27jrJu2D5WZje65+zMVptdS/WsqALxd2kjiZhEpThd/qkS8T3bumatcTu6Zb+LsT28FRpeWjXGTJjyQXQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@anthropic-ai/sdk': ^0.40.1
|
'@anthropic-ai/sdk': ^0.40.1
|
||||||
@@ -3690,7 +3690,7 @@ snapshots:
|
|||||||
crypt: 0.0.2
|
crypt: 0.0.2
|
||||||
is-buffer: 1.1.6
|
is-buffer: 1.1.6
|
||||||
|
|
||||||
mem0ai@3.0.1(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0):
|
mem0ai@3.0.2(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.40.1
|
'@anthropic-ai/sdk': 0.40.1
|
||||||
'@azure/identity': 4.13.0
|
'@azure/identity': 4.13.0
|
||||||
|
|||||||
+21
-5
@@ -280,13 +280,29 @@ class OSSProvider implements Mem0Provider {
|
|||||||
config.llm = defaultLlm;
|
config.llm = defaultLlm;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.ossConfig?.vectorStore)
|
if (this.ossConfig?.vectorStore) {
|
||||||
config.vectorStore = { ...this.ossConfig.vectorStore };
|
const vs = { ...this.ossConfig.vectorStore } as Record<string, unknown>;
|
||||||
|
const vsCfg = (vs.config ?? {}) as Record<string, unknown>;
|
||||||
|
// Resolve dims from embedder config if vector store doesn't have them
|
||||||
|
const embedderDims = (config.embedder as any)?.config?.embeddingDims;
|
||||||
|
if (!vsCfg.dimension && embedderDims) {
|
||||||
|
vsCfg.dimension = embedderDims;
|
||||||
|
}
|
||||||
|
// Sync both dimension fields — Qdrant reads dimension, PGVector reads embeddingModelDims
|
||||||
|
if (vsCfg.dimension && !vsCfg.embeddingModelDims) {
|
||||||
|
vsCfg.embeddingModelDims = vsCfg.dimension;
|
||||||
|
} else if (vsCfg.embeddingModelDims && !vsCfg.dimension) {
|
||||||
|
vsCfg.dimension = vsCfg.embeddingModelDims;
|
||||||
|
}
|
||||||
|
vs.config = vsCfg;
|
||||||
|
config.vectorStore = vs;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.ossConfig?.historyDbPath) {
|
if (this.ossConfig?.historyDbPath) {
|
||||||
const dbPath = this.resolvePath
|
const raw = this.ossConfig.historyDbPath;
|
||||||
? this.resolvePath(this.ossConfig.historyDbPath)
|
const isAbsolute = raw.startsWith("/") || /^[A-Za-z]:[/\\]/.test(raw);
|
||||||
: this.ossConfig.historyDbPath;
|
const dbPath =
|
||||||
|
isAbsolute || !this.resolvePath ? raw : this.resolvePath(raw);
|
||||||
config.historyDbPath = dbPath;
|
config.historyDbPath = dbPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,49 @@ metadata:
|
|||||||
|
|
||||||
You are performing a memory consolidation pass. Your goal is to review all stored memories for this user and improve their overall quality. Think of this as compressing raw observations into clean, durable knowledge.
|
You are performing a memory consolidation pass. Your goal is to review all stored memories for this user and improve their overall quality. Think of this as compressing raw observations into clean, durable knowledge.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### memory_search
|
||||||
|
Semantic search across stored memories.
|
||||||
|
- `query` (required): search query
|
||||||
|
- `limit`: max results
|
||||||
|
- `userId`, `agentId`: scope overrides
|
||||||
|
- `scope`: `"all"` (default), `"session"`, or `"long-term"`
|
||||||
|
- `categories`: filter by category array
|
||||||
|
|
||||||
|
### memory_add
|
||||||
|
Store new facts in long-term memory.
|
||||||
|
- `facts` (required): array of facts — ALL must share the same category
|
||||||
|
- `category`: `"identity"`, `"preference"`, `"decision"`, `"rule"`, `"project"`, `"configuration"`, `"technical"`, `"relationship"`
|
||||||
|
- `importance`: 0.0–1.0
|
||||||
|
|
||||||
|
### memory_get
|
||||||
|
Retrieve a single memory by ID.
|
||||||
|
- `memoryId` (required): the memory ID
|
||||||
|
|
||||||
|
### memory_list
|
||||||
|
List all stored memories for a user or agent.
|
||||||
|
- `userId`, `agentId`: scope overrides
|
||||||
|
- `scope`: `"all"` (default), `"session"`, or `"long-term"`
|
||||||
|
|
||||||
|
### memory_update
|
||||||
|
Update an existing memory's text in place. Atomic and preserves edit history.
|
||||||
|
- `memoryId` (required): the memory ID to update
|
||||||
|
- `text` (required): the new text (replaces old)
|
||||||
|
|
||||||
|
### memory_delete
|
||||||
|
Delete memories by ID, query, or bulk.
|
||||||
|
- `memoryId`: specific memory ID to delete
|
||||||
|
- `all`: delete ALL memories (requires `confirm: true`)
|
||||||
|
- `userId`, `agentId`: scope overrides
|
||||||
|
|
||||||
|
### memory_event_list
|
||||||
|
List recent background processing events (platform mode only).
|
||||||
|
|
||||||
|
### memory_event_status
|
||||||
|
Get status of a specific background event.
|
||||||
|
- `event_id` (required): the event ID to check
|
||||||
|
|
||||||
Follow these four phases in order. Do not skip phases.
|
Follow these four phases in order. Do not skip phases.
|
||||||
|
|
||||||
## Phase 1: Orient
|
## Phase 1: Orient
|
||||||
|
|||||||
@@ -18,6 +18,56 @@ Your primary role is to extract relevant pieces of information from the conversa
|
|||||||
|
|
||||||
**The core question**: "Would a new agent — with no prior context — benefit from knowing this?" If no → do nothing. Most turns produce zero memory operations. That is correct and expected.
|
**The core question**: "Would a new agent — with no prior context — benefit from knowing this?" If no → do nothing. Most turns produce zero memory operations. That is correct and expected.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### memory_search
|
||||||
|
Semantic search across stored memories.
|
||||||
|
- `query` (required): search query
|
||||||
|
- `limit`: max results (default: configured topK)
|
||||||
|
- `userId`, `agentId`: scope overrides
|
||||||
|
- `scope`: `"all"` (default), `"session"`, or `"long-term"`
|
||||||
|
- `categories`: filter by category array
|
||||||
|
- `filters`: advanced filter object
|
||||||
|
|
||||||
|
### memory_add
|
||||||
|
Store new facts in long-term memory.
|
||||||
|
- `facts` (required): array of facts to store — ALL must share the same category
|
||||||
|
- `text`: alternative single-fact string
|
||||||
|
- `category`: `"identity"`, `"preference"`, `"decision"`, `"rule"`, `"project"`, `"configuration"`, `"technical"`, `"relationship"`
|
||||||
|
- `importance`: 0.0–1.0 (omit for category default)
|
||||||
|
- `userId`, `agentId`: scope overrides
|
||||||
|
- `metadata`: additional key-value metadata
|
||||||
|
- `longTerm`: true (default) for persistent, false for session-scoped
|
||||||
|
|
||||||
|
### memory_get
|
||||||
|
Retrieve a single memory by ID.
|
||||||
|
- `memoryId` (required): the memory ID
|
||||||
|
|
||||||
|
### memory_list
|
||||||
|
List all stored memories for a user or agent.
|
||||||
|
- `userId`, `agentId`: scope overrides
|
||||||
|
- `scope`: `"all"` (default), `"session"`, or `"long-term"`
|
||||||
|
|
||||||
|
### memory_update
|
||||||
|
Update an existing memory's text in place. Atomic and preserves edit history.
|
||||||
|
- `memoryId` (required): the memory ID to update
|
||||||
|
- `text` (required): the new text (replaces old)
|
||||||
|
|
||||||
|
### memory_delete
|
||||||
|
Delete memories by ID, query, or bulk.
|
||||||
|
- `memoryId`: specific memory ID to delete
|
||||||
|
- `query`: search query to find and delete matching memories
|
||||||
|
- `all`: delete ALL memories (requires `confirm: true`)
|
||||||
|
- `confirm`: safety gate for bulk operations
|
||||||
|
- `userId`, `agentId`: scope overrides
|
||||||
|
|
||||||
|
### memory_event_list
|
||||||
|
List recent background processing events (platform mode only).
|
||||||
|
|
||||||
|
### memory_event_status
|
||||||
|
Get status of a specific background event.
|
||||||
|
- `event_id` (required): the event ID to check
|
||||||
|
|
||||||
## Decision Gate
|
## Decision Gate
|
||||||
|
|
||||||
Every candidate fact must pass ALL four gates:
|
Every candidate fact must pass ALL four gates:
|
||||||
@@ -28,7 +78,7 @@ Every candidate fact must pass ALL four gates:
|
|||||||
|
|
||||||
**Gate 2 — NOVELTY**: Check your recalled memories below — is this already known?
|
**Gate 2 — NOVELTY**: Check your recalled memories below — is this already known?
|
||||||
- Already known and unchanged → SKIP
|
- Already known and unchanged → SKIP
|
||||||
- Known but materially changed → UPDATE (find old → forget → store new)
|
- Known but materially changed → UPDATE (find old → update in place)
|
||||||
- Genuinely new → proceed
|
- Genuinely new → proceed
|
||||||
- **Material difference test**: Only UPDATE if new information adds real context, details, or changes meaning. Cosmetic differences (synonyms, rephrasing, punctuation) are NOT updates. "Loves daily walks" vs "enjoys daily walks" = no material change = SKIP.
|
- **Material difference test**: Only UPDATE if new information adds real context, details, or changes meaning. Cosmetic differences (synonyms, rephrasing, punctuation) are NOT updates. "Loves daily walks" vs "enjoys daily walks" = no material change = SKIP.
|
||||||
|
|
||||||
@@ -196,8 +246,9 @@ Categories: `identity`, `configuration`, `rule`, `preference`, `decision`, `tech
|
|||||||
|
|
||||||
When a recalled memory needs updating (fact changed, status changed, new detail added):
|
When a recalled memory needs updating (fact changed, status changed, new detail added):
|
||||||
1. `memory_search` to find the existing memory
|
1. `memory_search` to find the existing memory
|
||||||
2. `memory_delete` on the old memory's ID
|
2. `memory_update` on the memory's ID with the corrected/expanded text
|
||||||
3. `memory_add` with the corrected/expanded fact
|
|
||||||
|
`memory_update` is preferred over delete+add because it is **atomic and preserves edit history**.
|
||||||
|
|
||||||
**Choose the MORE COMPLETE version.** When both old and new have unique context, COMBINE them into a unified memory using the user's stated words.
|
**Choose the MORE COMPLETE version.** When both old and new have unique context, COMBINE them into a unified memory using the user's stated words.
|
||||||
|
|
||||||
@@ -206,10 +257,10 @@ When a recalled memory needs updating (fact changed, status changed, new detail
|
|||||||
- "User likes Python" → "User enjoys Python" = NOT material = SKIP
|
- "User likes Python" → "User enjoys Python" = NOT material = SKIP
|
||||||
- When both have unique context, combine: Old "Trip to Paris in September with Jack" + New "User can't wait to visit Eiffel Tower" → "Trip to Paris in September 2025 with friend Jack, user says they can't wait to visit the Eiffel Tower and try authentic French pastries"
|
- When both have unique context, combine: Old "Trip to Paris in September with Jack" + New "User can't wait to visit Eiffel Tower" → "Trip to Paris in September 2025 with friend Jack, user says they can't wait to visit the Eiffel Tower and try authentic French pastries"
|
||||||
|
|
||||||
**Consolidation**: When a rich new fact encompasses multiple existing memories, update one to the comprehensive version and forget the others.
|
**Consolidation**: When a rich new fact encompasses multiple existing memories, `memory_update` the best one to the comprehensive version and `memory_delete` the rest.
|
||||||
- Old: "User has a dog" + "Dog's name is Poppy" + "User walks dog daily"
|
- Old: "User has a dog" + "Dog's name is Poppy" + "User walks dog daily"
|
||||||
- New: "User has a dog named Poppy and says taking him for walks is the best part of their day"
|
- New: "User has a dog named Poppy and says taking him for walks is the best part of their day"
|
||||||
- Action: forget all three old memories, store one consolidated memory
|
- Action: `memory_update` the best version with consolidated text, `memory_delete` the redundant ones
|
||||||
|
|
||||||
**Temporary vs permanent changes**: A temporary constraint (e.g., injury pausing a hobby) does NOT contradict the underlying preference. Store the constraint as a new memory; don't delete the preference.
|
**Temporary vs permanent changes**: A temporary constraint (e.g., injury pausing a hobby) does NOT contradict the underlying preference. Store the constraint as a new memory; don't delete the preference.
|
||||||
- Old: "User enjoys hiking on weekends"
|
- Old: "User enjoys hiking on weekends"
|
||||||
@@ -267,8 +318,7 @@ User: "Never use Docker for local dev, it ate 40GB of disk last time and my Mac
|
|||||||
Recalled: ["As of 2026-03-15, user is planning trip to Paris in September with friend Jack"]
|
Recalled: ["As of 2026-03-15, user is planning trip to Paris in September with friend Jack"]
|
||||||
User: "Can't wait for the Paris trip, definitely want to hit the Eiffel Tower and try authentic French pastries"
|
User: "Can't wait for the Paris trip, definitely want to hit the Eiffel Tower and try authentic French pastries"
|
||||||
→ memory_search("Paris trip planning")
|
→ memory_search("Paris trip planning")
|
||||||
→ memory_delete(memoryId: "mem-id-of-old")
|
→ memory_update(memoryId: "mem-id-of-old", text: "As of 2026-03-30, user is planning trip to Paris in September 2025 with friend Jack, says they can't wait to visit the Eiffel Tower and try authentic French pastries")
|
||||||
→ memory_add(facts: ["As of 2026-03-30, user is planning trip to Paris in September 2025 with friend Jack, says they can't wait to visit the Eiffel Tower and try authentic French pastries"], category: "project")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example 6: Outcome over intent
|
### Example 6: Outcome over intent
|
||||||
@@ -327,8 +377,8 @@ Agent: "Hello! How can I help?"
|
|||||||
Recalled: ["User has a dog", "Dog's name is Poppy", "User walks dog daily"]
|
Recalled: ["User has a dog", "Dog's name is Poppy", "User walks dog daily"]
|
||||||
User: "Poppy learned fetch! Our walks are even better now, honestly it's the best part of my day"
|
User: "Poppy learned fetch! Our walks are even better now, honestly it's the best part of my day"
|
||||||
→ memory_search("dog Poppy walks") → find all three old memory IDs
|
→ memory_search("dog Poppy walks") → find all three old memory IDs
|
||||||
→ memory_delete(memoryId: "id-1"), memory_delete(memoryId: "id-2"), memory_delete(memoryId: "id-3")
|
→ memory_update(memoryId: "id-1", text: "User has a dog named Poppy and says taking him for walks is the best part of their day. Poppy recently learned fetch, making walks more enjoyable.")
|
||||||
→ memory_add(facts: ["User has a dog named Poppy and says taking him for walks is the best part of their day. Poppy recently learned fetch, making walks more enjoyable."], category: "preference")
|
→ memory_delete(memoryId: "id-2"), memory_delete(memoryId: "id-3")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example 12: NOOP — generic greeting, nothing to store
|
### Example 12: NOOP — generic greeting, nothing to store
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ describe("OSSProvider — _buildConfig branch coverage", () => {
|
|||||||
config: expect.objectContaining({ model: "gpt-4", apiKey: "sk-l" }),
|
config: expect.objectContaining({ model: "gpt-4", apiKey: "sk-l" }),
|
||||||
});
|
});
|
||||||
expect(capturedConfig!.vectorStore).toEqual({ provider: "qdrant", config: { host: "localhost", port: 6333 } });
|
expect(capturedConfig!.vectorStore).toEqual({ provider: "qdrant", config: { host: "localhost", port: 6333 } });
|
||||||
expect(capturedConfig!.historyDbPath).toBe("/resolved/tmp/history.db");
|
expect(capturedConfig!.historyDbPath).toBe("/tmp/history.db");
|
||||||
expect(capturedConfig!.disableHistory).toBe(true);
|
expect(capturedConfig!.disableHistory).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -732,4 +732,58 @@ describe("OSSProvider — customInstructions passthrough", () => {
|
|||||||
expect(capturedConfig).toBeDefined();
|
expect(capturedConfig).toBeDefined();
|
||||||
expect(capturedConfig!.customInstructions).toBe("Extract only user preferences.");
|
expect(capturedConfig!.customInstructions).toBe("Extract only user preferences.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves absolute Unix historyDbPath without resolvePath mangling", async () => {
|
||||||
|
const { createProvider } = await import("./index.ts");
|
||||||
|
const cfg = mem0ConfigSchema.parse({
|
||||||
|
mode: "open-source",
|
||||||
|
oss: {
|
||||||
|
historyDbPath: "/home/user/.myapp/history.db",
|
||||||
|
disableHistory: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const api = { resolvePath: (p: string) => `/stateDir/${p}` } as any;
|
||||||
|
const provider = createProvider(cfg, api);
|
||||||
|
|
||||||
|
await provider.search("test", { user_id: "u1" });
|
||||||
|
|
||||||
|
expect(capturedConfig).toBeDefined();
|
||||||
|
expect(capturedConfig!.historyDbPath).toBe("/home/user/.myapp/history.db");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves absolute Windows historyDbPath without resolvePath mangling", async () => {
|
||||||
|
const { createProvider } = await import("./index.ts");
|
||||||
|
const cfg = mem0ConfigSchema.parse({
|
||||||
|
mode: "open-source",
|
||||||
|
oss: {
|
||||||
|
historyDbPath: "C:\\Users\\me\\history.db",
|
||||||
|
disableHistory: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const api = { resolvePath: (p: string) => `/stateDir/${p}` } as any;
|
||||||
|
const provider = createProvider(cfg, api);
|
||||||
|
|
||||||
|
await provider.search("test", { user_id: "u1" });
|
||||||
|
|
||||||
|
expect(capturedConfig).toBeDefined();
|
||||||
|
expect(capturedConfig!.historyDbPath).toBe("C:\\Users\\me\\history.db");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still resolves relative historyDbPath via resolvePath", async () => {
|
||||||
|
const { createProvider } = await import("./index.ts");
|
||||||
|
const cfg = mem0ConfigSchema.parse({
|
||||||
|
mode: "open-source",
|
||||||
|
oss: {
|
||||||
|
historyDbPath: "data/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!.historyDbPath).toBe("/resolved/data/history.db");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ vi.mock("../cli/config-file.ts", () => ({
|
|||||||
readPluginAuth: vi.fn().mockReturnValue({}),
|
readPluginAuth: vi.fn().mockReturnValue({}),
|
||||||
writePluginAuth: vi.fn(),
|
writePluginAuth: vi.fn(),
|
||||||
writePluginConfigField: vi.fn(),
|
writePluginConfigField: vi.fn(),
|
||||||
|
enableSkillsConfig: vi.fn(),
|
||||||
getBaseUrl: vi.fn().mockReturnValue("https://api.mem0.ai"),
|
getBaseUrl: vi.fn().mockReturnValue("https://api.mem0.ai"),
|
||||||
OPENCLAW_CONFIG_FILE: "/mock/.openclaw/openclaw.json",
|
OPENCLAW_CONFIG_FILE: "/mock/.openclaw/openclaw.json",
|
||||||
}));
|
}));
|
||||||
@@ -42,6 +43,7 @@ import {
|
|||||||
readPluginAuth,
|
readPluginAuth,
|
||||||
writePluginAuth,
|
writePluginAuth,
|
||||||
writePluginConfigField,
|
writePluginConfigField,
|
||||||
|
enableSkillsConfig,
|
||||||
getBaseUrl,
|
getBaseUrl,
|
||||||
} from "../cli/config-file.ts";
|
} from "../cli/config-file.ts";
|
||||||
import { loadDreamPrompt } from "../skill-loader.ts";
|
import { loadDreamPrompt } from "../skill-loader.ts";
|
||||||
@@ -178,7 +180,7 @@ function createMockCfg() {
|
|||||||
topK: 5,
|
topK: 5,
|
||||||
autoCapture: true,
|
autoCapture: true,
|
||||||
autoRecall: true,
|
autoRecall: true,
|
||||||
searchThreshold: 0.5,
|
searchThreshold: 0.1,
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
customCategories: {},
|
customCategories: {},
|
||||||
skills: {},
|
skills: {},
|
||||||
|
|||||||
@@ -44,14 +44,14 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
|
|||||||
expect(cfg.userId.length).toBeGreaterThan(0);
|
expect(cfg.userId.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("autoCapture defaults to false", () => {
|
it("autoCapture defaults to true", () => {
|
||||||
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
||||||
expect(cfg.autoCapture).toBe(false);
|
expect(cfg.autoCapture).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("autoRecall defaults to false", () => {
|
it("autoRecall defaults to true", () => {
|
||||||
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
||||||
expect(cfg.autoRecall).toBe(false);
|
expect(cfg.autoRecall).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("topK defaults to 5", () => {
|
it("topK defaults to 5", () => {
|
||||||
@@ -59,9 +59,9 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
|
|||||||
expect(cfg.topK).toBe(5);
|
expect(cfg.topK).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("searchThreshold defaults to 0.5", () => {
|
it("searchThreshold defaults to 0.1", () => {
|
||||||
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
||||||
expect(cfg.searchThreshold).toBe(0.5);
|
expect(cfg.searchThreshold).toBe(0.1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => {
|
it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
buildOssLlmConfig,
|
buildOssLlmConfig,
|
||||||
buildOssEmbedderConfig,
|
buildOssEmbedderConfig,
|
||||||
buildOssVectorConfig,
|
buildOssVectorConfig,
|
||||||
|
collectionNameForDims,
|
||||||
validateOssFlags,
|
validateOssFlags,
|
||||||
checkQdrantConnectivity,
|
checkQdrantConnectivity,
|
||||||
checkOllamaConnectivity,
|
checkOllamaConnectivity,
|
||||||
@@ -90,9 +91,9 @@ describe("buildOssEmbedderConfig", () => {
|
|||||||
expect(result.dims).toBe(768);
|
expect(result.dims).toBe(768);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns unknown dims for custom model", () => {
|
it("falls back to provider default dims for custom model", () => {
|
||||||
const result = buildOssEmbedderConfig("ollama", { model: "custom-embed" });
|
const result = buildOssEmbedderConfig("ollama", { model: "custom-embed" });
|
||||||
expect(result.dims).toBeUndefined();
|
expect(result.dims).toBe(768);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,6 +148,49 @@ describe("buildOssVectorConfig", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("collectionNameForDims", () => {
|
||||||
|
it("generates dimension-based collection name", () => {
|
||||||
|
expect(collectionNameForDims(1536)).toBe("mem0_1536d");
|
||||||
|
expect(collectionNameForDims(768)).toBe("mem0_768d");
|
||||||
|
expect(collectionNameForDims(384)).toBe("mem0_384d");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildOssVectorConfig dimension safety", () => {
|
||||||
|
it("sets both dimension and embeddingModelDims when dims provided", () => {
|
||||||
|
const result = buildOssVectorConfig("qdrant", { dims: 768 });
|
||||||
|
expect(result.config.dimension).toBe(768);
|
||||||
|
expect(result.config.embeddingModelDims).toBe(768);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets collectionName based on dims", () => {
|
||||||
|
const result = buildOssVectorConfig("qdrant", { dims: 768 });
|
||||||
|
expect(result.config.collectionName).toBe("mem0_768d");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses different collection names for different dims", () => {
|
||||||
|
const r1 = buildOssVectorConfig("qdrant", { dims: 1536 });
|
||||||
|
const r2 = buildOssVectorConfig("qdrant", { dims: 768 });
|
||||||
|
expect(r1.config.collectionName).not.toBe(r2.config.collectionName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits dimension fields when dims not provided", () => {
|
||||||
|
const result = buildOssVectorConfig("qdrant", {});
|
||||||
|
expect(result.config.dimension).toBeUndefined();
|
||||||
|
expect(result.config.embeddingModelDims).toBeUndefined();
|
||||||
|
expect(result.config.collectionName).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works with pgvector too", () => {
|
||||||
|
const result = buildOssVectorConfig("pgvector", {
|
||||||
|
host: "localhost", port: "5432", user: "me", password: "pw", dbname: "test", dims: 768,
|
||||||
|
});
|
||||||
|
expect(result.config.dimension).toBe(768);
|
||||||
|
expect(result.config.embeddingModelDims).toBe(768);
|
||||||
|
expect(result.config.collectionName).toBe("mem0_768d");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("checkQdrantConnectivity", () => {
|
describe("checkQdrantConnectivity", () => {
|
||||||
it("returns error for unreachable host", async () => {
|
it("returns error for unreachable host", async () => {
|
||||||
const result = await checkQdrantConnectivity("http://localhost:19999");
|
const result = await checkQdrantConnectivity("http://localhost:19999");
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function createMockToolDeps(overrides = {}): ToolDeps {
|
|||||||
topK: 5,
|
topK: 5,
|
||||||
autoCapture: true,
|
autoCapture: true,
|
||||||
autoRecall: true,
|
autoRecall: true,
|
||||||
searchThreshold: 0.5,
|
searchThreshold: 0.1,
|
||||||
customInstructions: "test",
|
customInstructions: "test",
|
||||||
customCategories: {},
|
customCategories: {},
|
||||||
} as any,
|
} as any,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export default defineConfig({
|
|||||||
dts: true,
|
dts: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module"],
|
external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module",
|
||||||
|
"mem0ai", /^mem0ai\//, "better-sqlite3", "@sinclair/typebox"],
|
||||||
define: {
|
define: {
|
||||||
__OPENCLAW_PLUGIN_VERSION__: JSON.stringify(pkg.version),
|
__OPENCLAW_PLUGIN_VERSION__: JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user