From ebb9bb2b1534c753bb97bd857d626ece565b1ceb Mon Sep 17 00:00:00 2001 From: Kartik Date: Wed, 29 Apr 2026 22:19:40 +0530 Subject: [PATCH] fix: adding skills config and updating the plugin the config (#4958) --- docs/integrations/openclaw.mdx | 49 +++++--- openclaw/README.md | 74 +++++++++--- openclaw/cli/commands.ts | 37 +++++- openclaw/cli/config-file.ts | 156 ++++++++++++------------- openclaw/cli/oss-wizard.ts | 16 ++- openclaw/config.ts | 6 +- openclaw/index.test.ts | 49 ++++++++ openclaw/index.ts | 66 +++++++++-- openclaw/openclaw.plugin.json | 24 ++-- openclaw/package.json | 12 +- openclaw/pnpm-lock.yaml | 10 +- openclaw/providers.ts | 26 ++++- openclaw/skills/memory-dream/SKILL.md | 43 +++++++ openclaw/skills/memory-triage/SKILL.md | 68 +++++++++-- openclaw/sqlite-resilience.test.ts | 56 ++++++++- openclaw/tests/cli-commands.test.ts | 4 +- openclaw/tests/config.test.ts | 12 +- openclaw/tests/oss-wizard.test.ts | 48 +++++++- openclaw/tests/tools.test.ts | 2 +- openclaw/tsup.config.ts | 3 +- 20 files changed, 585 insertions(+), 176 deletions(-) diff --git a/docs/integrations/openclaw.mdx b/docs/integrations/openclaw.mdx index 56e39fb70..f45b0da2e 100644 --- a/docs/integrations/openclaw.mdx +++ b/docs/integrations/openclaw.mdx @@ -1,6 +1,6 @@ --- 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. @@ -12,11 +12,12 @@ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents The plugin provides: -1. **Auto-Recall** — Before the agent responds, memories matching the current message are injected into context -2. **Auto-Capture** — After the agent responds, the exchange is sent to Mem0 which decides what's worth keeping -3. **Agent Tools** — Eight tools for explicit memory operations during conversations +1. **Triage** — The agent extracts durable facts from conversations using a structured protocol with importance gates and domain overlays +2. **Recall** — Before each turn, relevant memories are retrieved with reranking and injected into context +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 @@ -24,12 +25,12 @@ Check your OpenClaw version: ```bash openclaw --version -# OpenClaw 2026.4.15 (041266a) +# OpenClaw 2026.4.25 (aa36ee6) ``` | OpenClaw Version | Plugin Support | |------------------|----------------| -| `>= 2026.4.15` | Fully supported | +| `>= 2026.4.25` | Fully supported | ## Installation @@ -100,9 +101,9 @@ You no longer need manual config editing to get started. Everything happens insi -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. -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. +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. #### 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, "config": { "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 | | `userId` | `string` | OS username | Scope memories per user | -| `autoRecall` | `boolean` | `false` | Inject memories before each turn (opt-in) | -| `autoCapture` | `boolean` | `false` | Store facts after each turn (opt-in) | +| `autoRecall` | `boolean` | `true` | Inject memories before each turn. Ignored when `skills` is configured. | +| `autoCapture` | `boolean` | `true` | Store facts after each turn. Ignored when `skills` is configured. | | `topK` | `number` | `5` | Max memories per recall | | `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 | | **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 { @@ -436,8 +451,8 @@ Auto-capture and auto-recall are disabled by default (opt-in). To enable either "entries": { "openclaw-mem0": { "config": { - "autoCapture": true, // send conversations to Mem0 for extraction - "autoRecall": true // inject relevant memories into context + "autoCapture": false, // disable automatic fact extraction + "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 diff --git a/openclaw/README.md b/openclaw/README.md index c18f50e8d..4af44d1b9 100644 --- a/openclaw/README.md +++ b/openclaw/README.md @@ -2,7 +2,9 @@ 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 @@ -10,12 +12,12 @@ Check your OpenClaw version: ```bash openclaw --version -# OpenClaw 2026.4.15 (041266a) +# OpenClaw 2026.4.25 (aa36ee6) ``` | OpenClaw Version | Plugin Support | |------------------|----------------| -| `>= 2026.4.15` | Fully supported | +| `>= 2026.4.25` | Fully supported | ## Quick Start @@ -50,7 +52,19 @@ openclaw --version "enabled": true, "config": { "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 Architecture

-**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 @@ -260,10 +287,25 @@ openclaw mem0 help --json # discover all comma | --- | ---- | ------- | ----------- | | `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode | | `userId` | `string` | OS username | User identifier. All memories scoped to this value. | -| `autoRecall` | `boolean` | `false` | Inject relevant memories before each turn | -| `autoCapture` | `boolean` | `false` | Extract and store facts after each turn | +| `autoRecall` | `boolean` | `true` | Inject relevant memories before each turn. Ignored when `skills` is set. | +| `autoCapture` | `boolean` | `true` | Extract and store facts after each turn. Ignored when `skills` is set. | | `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 @@ -306,13 +348,15 @@ To avoid plaintext credentials: - Use env var references: `"apiKey": "${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: -- `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 +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. -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 diff --git a/openclaw/cli/commands.ts b/openclaw/cli/commands.ts index 18987ec29..ff7bd8b5a 100644 --- a/openclaw/cli/commands.ts +++ b/openclaw/cli/commands.ts @@ -43,6 +43,7 @@ import { readPluginAuth, writePluginAuth, writePluginConfigField, + enableSkillsConfig, OPENCLAW_CONFIG_FILE, } from "./config-file.ts"; import { jsonOut, jsonErr, redactSecrets } from "./json-helpers.ts"; @@ -50,6 +51,7 @@ import { LLM_PROVIDERS, EMBEDDER_PROVIDERS, VECTOR_PROVIDERS, buildOssLlmConfig, buildOssEmbedderConfig, buildOssVectorConfig, validateOssFlags, checkQdrantConnectivity, checkOllamaConnectivity, checkPgConnectivity, + collectionNameForDims, } from "./oss-wizard.ts"; // ============================================================================ @@ -213,10 +215,11 @@ function saveLoginConfig( const userId = resolveUserId(userIdFlag, existingAuth.userId); writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) }); + enableSkillsConfig(userId); if (!silent) { console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`); - console.log(` Mode: platform`); + console.log(` Mode: platform (skills enabled)`); console.log(` User ID: ${userId}`); } } @@ -226,10 +229,11 @@ function saveOssConfig(userIdFlag?: string, silent?: boolean): void { const userId = resolveUserId(userIdFlag, existingAuth.userId); writePluginAuth({ apiKey: "", userId, mode: "open-source" }); + enableSkillsConfig(userId); if (!silent) { 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}`); } } @@ -350,6 +354,17 @@ async function runOssWizardInteractive( } 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); // === Step 4: User ID === @@ -370,6 +385,8 @@ async function runOssWizardInteractive( console.log(` LLM: ${llmDef.id} (${llmCfg.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(` Dims: ${dims ?? "unknown"}`); + console.log(` Collection:${dims ? " " + collectionNameForDims(dims) : " (default)"}`); console.log(` User ID: ${userIdValue}`); console.log(""); 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", "embedder"], { provider: embCfg.provider, config: embCfg.config }); writePluginConfigField(["oss", "vectorStore"], vecCfg); @@ -550,9 +576,10 @@ export function registerCliCommands( mode: "open-source", config: { llm: { provider: llmCfg.provider, model: llmCfg.config.model }, - embedder: { provider: embCfg.provider, model: embCfg.config.model }, - vectorStore: { provider: vecCfg.provider, ...(vecId === "qdrant" ? { url: vecCfg.config.url } : { host: vecCfg.config.host }) }, + embedder: { provider: embCfg.provider, model: embCfg.config.model, dims: newDims }, + 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), message: "Open-source mode configured. Restart the gateway: openclaw gateway restart", }; @@ -889,7 +916,7 @@ export function registerCliCommands( runId?: string, ): SearchOptions => { const base = buildSearchOptions(userIdOverride, lim, runId); - base.threshold = 0.3; + base.threshold = 0.1; return base; }; diff --git a/openclaw/cli/config-file.ts b/openclaw/cli/config-file.ts index bba4a8dc3..2af2f36bd 100644 --- a/openclaw/cli/config-file.ts +++ b/openclaw/cli/config-file.ts @@ -19,7 +19,6 @@ export const OPENCLAW_CONFIG_FILE = join(OPENCLAW_CONFIG_DIR, "openclaw.json"); export const DEFAULT_BASE_URL = "https://api.mem0.ai"; const PLUGIN_ID = "openclaw-mem0"; -const NPM_PACKAGE = "@mem0/openclaw-mem0"; // ============================================================================ // Types @@ -76,11 +75,44 @@ function readFullConfig(): Record { } } -/** 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): void { if (!exists(OPENCLAW_CONFIG_DIR)) { 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; + const diskPlugins = disk.plugins as Record | undefined; + const ourPlugins = config.plugins as Record | 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( OPENCLAW_CONFIG_FILE, JSON.stringify(config, null, 2), @@ -122,82 +154,6 @@ export function writePluginAuth(auth: PluginAuthConfig): void { writeFullConfig(full); } -/** - * Ensure the plugin has a valid install record and is in plugins.allow. - * - * OpenClaw's `plugins update` command requires a `plugins.installs.` - * 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. */ function ensurePluginStructure(full: any): void { @@ -231,6 +187,50 @@ export function writePluginConfigField( 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 */ export function getBaseUrl(): string { const auth = readPluginAuth(); diff --git a/openclaw/cli/oss-wizard.ts b/openclaw/cli/oss-wizard.ts index f8d693fab..dd8c0ba27 100644 --- a/openclaw/cli/oss-wizard.ts +++ b/openclaw/cli/oss-wizard.ts @@ -56,8 +56,15 @@ export const KNOWN_EMBEDDER_DIMS: Record = { "text-embedding-3-large": 3072, "text-embedding-ada-002": 1536, "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 // ============================================================================ @@ -105,7 +112,8 @@ export function buildOssEmbedderConfig( 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 }; } @@ -138,7 +146,11 @@ export function buildOssVectorConfig( 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 }; } diff --git a/openclaw/config.ts b/openclaw/config.ts index e16ef3c7a..4e43c8c44 100644 --- a/openclaw/config.ts +++ b/openclaw/config.ts @@ -231,8 +231,8 @@ export const mem0ConfigSchema = { return "default"; } })(), - autoCapture: cfg.autoCapture === true, - autoRecall: cfg.autoRecall === true, + autoCapture: cfg.autoCapture !== false, + autoRecall: cfg.autoRecall !== false, // v3.0.0: customPrompt renamed to customInstructions (backwards-compat: accept either) customInstructions: typeof cfg.customInstructions === "string" @@ -247,7 +247,7 @@ export const mem0ConfigSchema = { ? (cfg.customCategories as Record) : DEFAULT_CUSTOM_CATEGORIES, 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, needsSetup, oss: ossConfig, diff --git a/openclaw/index.test.ts b/openclaw/index.test.ts index cd58a5086..3adf1666d 100644 --- a/openclaw/index.test.ts +++ b/openclaw/index.test.ts @@ -561,3 +561,52 @@ What is the deployment plan?`, 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); + }); +}); diff --git a/openclaw/index.ts b/openclaw/index.ts index 50e506c26..4b4c89041 100644 --- a/openclaw/index.ts +++ b/openclaw/index.ts @@ -54,15 +54,12 @@ import { import { PlatformBackend } from "./backend/platform.ts"; import type { Backend } from "./backend/base.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 type { ToolDeps } from "./tools/index.ts"; import { captureEvent } from "./telemetry.ts"; import { bootstrapTelemetryFlag } from "./fs-safe.ts"; -bootstrapTelemetryFlag(); -ensureInstallRecord(); - // ============================================================================ // 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", register(api: OpenClawPluginApi) { + bootstrapTelemetryFlag(); + // Read auth from openclaw.json plugin config (picks up post-startup login). // This is the single source of truth — set via `openclaw mem0 login`. const pluginAuth = readPluginAuth(); @@ -207,8 +206,57 @@ const memoryPlugin = definePluginEntry({ }, 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 @@ -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( - (r) => (r.score ?? 0) >= recallThreshold, + (r) => (r.score ?? 0) >= cfg.searchThreshold, ); // Dynamic thresholding: drop memories scoring less than 50% of @@ -709,7 +753,7 @@ function registerHooks( undefined, recallSessionKey, ); - broadOpts.threshold = 0.5; + broadOpts.threshold = cfg.searchThreshold; const broadResults = await provider.search( "recent decisions, preferences, active projects, and configuration", broadOpts, diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json index d0457fee3..2b867f004 100644 --- a/openclaw/openclaw.plugin.json +++ b/openclaw/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "openclaw-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.", - "version": "1.0.10", + "version": "1.0.11", "kind": "memory", "skills": ["skills"], "commandAliases": [ @@ -17,9 +17,17 @@ "memory_update", "memory_delete", "memory_event_list", "memory_event_status" ] }, - "providerAuthEnvVars": { - "mem0": ["MEM0_API_KEY"], - "openclaw-mem0-oss": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"] + "setup": { + "providers": [ + { + "id": "mem0", + "envVars": ["MEM0_API_KEY"] + }, + { + "id": "openclaw-mem0-oss", + "envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"] + } + ] }, "providerAuthChoices": [ { @@ -171,13 +179,13 @@ }, "autoCapture": { "type": "boolean", - "default": false, - "description": "Opt-in. When true, extracts durable facts after each agent turn. Disabled by default." + "default": true, + "description": "When true, extracts durable facts after each agent turn. Enabled by default. Ignored in skills mode." }, "autoRecall": { "type": "boolean", - "default": false, - "description": "Opt-in. When true, injects relevant memories before each agent turn. Disabled by default." + "default": true, + "description": "When true, injects relevant memories before each agent turn. Enabled by default. Ignored in skills mode." }, "customInstructions": { "type": "string" diff --git a/openclaw/package.json b/openclaw/package.json index 0b8a23219..14b4b670b 100644 --- a/openclaw/package.json +++ b/openclaw/package.json @@ -1,6 +1,6 @@ { "name": "@mem0/openclaw-mem0", - "version": "1.0.10", + "version": "1.0.11", "type": "module", "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source", "license": "Apache-2.0", @@ -35,19 +35,19 @@ }, "dependencies": { "@sinclair/typebox": "0.34.47", - "mem0ai": "3.0.1" + "mem0ai": "3.0.2" }, "openclaw": { "extensions": [ "./dist/index.js" ], "compat": { - "pluginApi": ">=2026.3.28", - "minGatewayVersion": ">=2026.3.28" + "pluginApi": ">=2026.4.24", + "minGatewayVersion": ">=2026.4.24" }, "build": { - "openclawVersion": "2026.4.1", - "pluginSdkVersion": "2026.4.1" + "openclawVersion": "2026.4.24", + "pluginSdkVersion": "2026.4.24" }, "install": { "npmSpec": "@mem0/openclaw-mem0" diff --git a/openclaw/pnpm-lock.yaml b/openclaw/pnpm-lock.yaml index f9ae8b872..68bcdc63d 100644 --- a/openclaw/pnpm-lock.yaml +++ b/openclaw/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: 0.34.47 version: 0.34.47 mem0ai: - specifier: 3.0.1 - 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) + specifier: 3.0.2 + 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: '@types/node': specifier: ^22.15.0 @@ -1508,8 +1508,8 @@ packages: md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - mem0ai@3.0.1: - resolution: {integrity: sha512-6phM544/3NRcCg7n5DBNRc9uUEMqGTe7sksULM2KM/FTihH27yTl5nPms8cnNpSKM5/ZKJ9jnkpzwGlvYYbtBQ==} + mem0ai@3.0.2: + resolution: {integrity: sha512-smB9q27jrJu2D5WZje65+zMVptdS/WsqALxd2kjiZhEpThd/qkS8T3bumatcTu6Zb+LsT28FRpeWjXGTJjyQXQ==} engines: {node: '>=18'} peerDependencies: '@anthropic-ai/sdk': ^0.40.1 @@ -3690,7 +3690,7 @@ snapshots: crypt: 0.0.2 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: '@anthropic-ai/sdk': 0.40.1 '@azure/identity': 4.13.0 diff --git a/openclaw/providers.ts b/openclaw/providers.ts index a29795cde..6911adedc 100644 --- a/openclaw/providers.ts +++ b/openclaw/providers.ts @@ -280,13 +280,29 @@ class OSSProvider implements Mem0Provider { config.llm = defaultLlm; } - if (this.ossConfig?.vectorStore) - config.vectorStore = { ...this.ossConfig.vectorStore }; + if (this.ossConfig?.vectorStore) { + const vs = { ...this.ossConfig.vectorStore } as Record; + const vsCfg = (vs.config ?? {}) as Record; + // 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) { - const dbPath = this.resolvePath - ? this.resolvePath(this.ossConfig.historyDbPath) - : this.ossConfig.historyDbPath; + const raw = this.ossConfig.historyDbPath; + const isAbsolute = raw.startsWith("/") || /^[A-Za-z]:[/\\]/.test(raw); + const dbPath = + isAbsolute || !this.resolvePath ? raw : this.resolvePath(raw); config.historyDbPath = dbPath; } diff --git a/openclaw/skills/memory-dream/SKILL.md b/openclaw/skills/memory-dream/SKILL.md index 984aa09dc..e6ccf0032 100644 --- a/openclaw/skills/memory-dream/SKILL.md +++ b/openclaw/skills/memory-dream/SKILL.md @@ -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. +## 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. ## Phase 1: Orient diff --git a/openclaw/skills/memory-triage/SKILL.md b/openclaw/skills/memory-triage/SKILL.md index 899a1210b..3487d7f1a 100644 --- a/openclaw/skills/memory-triage/SKILL.md +++ b/openclaw/skills/memory-triage/SKILL.md @@ -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. +## 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 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? - 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 - **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): 1. `memory_search` to find the existing memory -2. `memory_delete` on the old memory's ID -3. `memory_add` with the corrected/expanded fact +2. `memory_update` on the memory's ID with the corrected/expanded text + +`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. @@ -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 - 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" - 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. - 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"] 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_delete(memoryId: "mem-id-of-old") -→ 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") +→ 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") ``` ### 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"] 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_delete(memoryId: "id-1"), memory_delete(memoryId: "id-2"), memory_delete(memoryId: "id-3") -→ 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_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_delete(memoryId: "id-2"), memory_delete(memoryId: "id-3") ``` ### Example 12: NOOP — generic greeting, nothing to store diff --git a/openclaw/sqlite-resilience.test.ts b/openclaw/sqlite-resilience.test.ts index 80fb47872..98767416f 100644 --- a/openclaw/sqlite-resilience.test.ts +++ b/openclaw/sqlite-resilience.test.ts @@ -443,7 +443,7 @@ describe("OSSProvider — _buildConfig branch coverage", () => { config: expect.objectContaining({ model: "gpt-4", apiKey: "sk-l" }), }); expect(capturedConfig!.vectorStore).toEqual({ provider: "qdrant", config: { host: "localhost", port: 6333 } }); - expect(capturedConfig!.historyDbPath).toBe("/resolved/tmp/history.db"); + expect(capturedConfig!.historyDbPath).toBe("/tmp/history.db"); expect(capturedConfig!.disableHistory).toBe(true); }); @@ -732,4 +732,58 @@ describe("OSSProvider — customInstructions passthrough", () => { expect(capturedConfig).toBeDefined(); 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"); + }); }); diff --git a/openclaw/tests/cli-commands.test.ts b/openclaw/tests/cli-commands.test.ts index cf4bdfb4d..77cd52f49 100644 --- a/openclaw/tests/cli-commands.test.ts +++ b/openclaw/tests/cli-commands.test.ts @@ -16,6 +16,7 @@ vi.mock("../cli/config-file.ts", () => ({ readPluginAuth: vi.fn().mockReturnValue({}), writePluginAuth: vi.fn(), writePluginConfigField: vi.fn(), + enableSkillsConfig: vi.fn(), getBaseUrl: vi.fn().mockReturnValue("https://api.mem0.ai"), OPENCLAW_CONFIG_FILE: "/mock/.openclaw/openclaw.json", })); @@ -42,6 +43,7 @@ import { readPluginAuth, writePluginAuth, writePluginConfigField, + enableSkillsConfig, getBaseUrl, } from "../cli/config-file.ts"; import { loadDreamPrompt } from "../skill-loader.ts"; @@ -178,7 +180,7 @@ function createMockCfg() { topK: 5, autoCapture: true, autoRecall: true, - searchThreshold: 0.5, + searchThreshold: 0.1, customInstructions: "", customCategories: {}, skills: {}, diff --git a/openclaw/tests/config.test.ts b/openclaw/tests/config.test.ts index 712133be6..6eb7441ac 100644 --- a/openclaw/tests/config.test.ts +++ b/openclaw/tests/config.test.ts @@ -44,14 +44,14 @@ describe("mem0ConfigSchema.parse() — defaults", () => { expect(cfg.userId.length).toBeGreaterThan(0); }); - it("autoCapture defaults to false", () => { + it("autoCapture defaults to true", () => { 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" }); - expect(cfg.autoRecall).toBe(false); + expect(cfg.autoRecall).toBe(true); }); it("topK defaults to 5", () => { @@ -59,9 +59,9 @@ describe("mem0ConfigSchema.parse() — defaults", () => { expect(cfg.topK).toBe(5); }); - it("searchThreshold defaults to 0.5", () => { + it("searchThreshold defaults to 0.1", () => { 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", () => { diff --git a/openclaw/tests/oss-wizard.test.ts b/openclaw/tests/oss-wizard.test.ts index 185f9e197..00bf0b41b 100644 --- a/openclaw/tests/oss-wizard.test.ts +++ b/openclaw/tests/oss-wizard.test.ts @@ -7,6 +7,7 @@ import { buildOssLlmConfig, buildOssEmbedderConfig, buildOssVectorConfig, + collectionNameForDims, validateOssFlags, checkQdrantConnectivity, checkOllamaConnectivity, @@ -90,9 +91,9 @@ describe("buildOssEmbedderConfig", () => { 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" }); - 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", () => { it("returns error for unreachable host", async () => { const result = await checkQdrantConnectivity("http://localhost:19999"); diff --git a/openclaw/tests/tools.test.ts b/openclaw/tests/tools.test.ts index d28aa94b5..0ca1c63a9 100644 --- a/openclaw/tests/tools.test.ts +++ b/openclaw/tests/tools.test.ts @@ -34,7 +34,7 @@ function createMockToolDeps(overrides = {}): ToolDeps { topK: 5, autoCapture: true, autoRecall: true, - searchThreshold: 0.5, + searchThreshold: 0.1, customInstructions: "test", customCategories: {}, } as any, diff --git a/openclaw/tsup.config.ts b/openclaw/tsup.config.ts index 95b5bdfa0..d76cffef4 100644 --- a/openclaw/tsup.config.ts +++ b/openclaw/tsup.config.ts @@ -8,7 +8,8 @@ export default defineConfig({ dts: true, sourcemap: 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: { __OPENCLAW_PLUGIN_VERSION__: JSON.stringify(pkg.version), },