fix: adding skills config and updating the plugin the config (#4958)

This commit is contained in:
Kartik
2026-04-29 22:19:40 +05:30
committed by GitHub
parent 594b4e65d6
commit ebb9bb2b15
20 changed files with 585 additions and 176 deletions
+32 -17
View File
@@ -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
View File
@@ -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
+32 -5
View File
@@ -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
View File
@@ -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();
+14 -2
View File
@@ -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
View File
@@ -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,
+49
View File
@@ -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
View File
@@ -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,
+16 -8
View File
@@ -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"
+6 -6
View File
@@ -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"
+5 -5
View File
@@ -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
View File
@@ -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;
} }
+43
View File
@@ -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
+59 -9
View File
@@ -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
+55 -1
View File
@@ -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");
});
}); });
+3 -1
View File
@@ -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: {},
+6 -6
View File
@@ -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", () => {
+46 -2
View File
@@ -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");
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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),
}, },