diff --git a/docs/docs.json b/docs/docs.json
index 302b151a3..775baf364 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -29,7 +29,9 @@
{
"group": "Start Here",
"icon": "home",
- "pages": ["introduction"]
+ "pages": [
+ "introduction"
+ ]
}
]
},
@@ -112,7 +114,9 @@
{
"group": "Support & Troubleshooting",
"icon": "life-buoy",
- "pages": ["platform/faqs"]
+ "pages": [
+ "platform/faqs"
+ ]
},
{
"group": "Migration Guide",
@@ -127,7 +131,9 @@
{
"group": "Contribute",
"icon": "clipboard-list",
- "pages": ["platform/contribute"]
+ "pages": [
+ "platform/contribute"
+ ]
}
]
},
@@ -282,7 +288,10 @@
{
"group": "Community & Support",
"icon": "users",
- "pages": ["contributing/development", "contributing/documentation"]
+ "pages": [
+ "contributing/development",
+ "contributing/documentation"
+ ]
}
]
},
@@ -306,7 +315,9 @@
{
"group": "Getting Started",
"icon": "lightbulb",
- "pages": ["cookbooks/overview"]
+ "pages": [
+ "cookbooks/overview"
+ ]
},
{
"group": "Essentials",
@@ -378,7 +389,9 @@
{
"group": "Overview",
"icon": "plug",
- "pages": ["integrations"]
+ "pages": [
+ "integrations"
+ ]
},
{
"group": "Agent Frameworks",
@@ -409,7 +422,9 @@
{
"group": "Cloud & Infrastructure",
"icon": "cloud",
- "pages": ["integrations/aws-bedrock"]
+ "pages": [
+ "integrations/aws-bedrock"
+ ]
},
{
"group": "Developer Tools",
@@ -431,7 +446,10 @@
{
"group": "Getting Started",
"icon": "rocket",
- "pages": ["api-reference", "api-reference/organizations-projects"]
+ "pages": [
+ "api-reference",
+ "api-reference/organizations-projects"
+ ]
},
{
"group": "Core Memory Operations",
@@ -461,7 +479,10 @@
{
"group": "Events APIs",
"icon": "clock",
- "pages": ["api-reference/events/get-events", "api-reference/events/get-event"]
+ "pages": [
+ "api-reference/events/get-events",
+ "api-reference/events/get-event"
+ ]
},
{
"group": "Entities APIs",
@@ -513,12 +534,16 @@
{
"group": "Changelog",
"icon": "rocket",
- "pages": ["changelog"]
+ "pages": [
+ "changelog"
+ ]
},
{
"group": "Legacy Docs",
"icon": "archive",
- "pages": ["v0x/introduction"]
+ "pages": [
+ "v0x/introduction"
+ ]
}
]
}
@@ -539,7 +564,11 @@
{
"group": "Getting Started",
"icon": "rocket",
- "pages": ["v0x/introduction", "v0x/quickstart", "v0x/faqs"]
+ "pages": [
+ "v0x/introduction",
+ "v0x/quickstart",
+ "v0x/faqs"
+ ]
},
{
"group": "Core Concepts",
@@ -1050,4 +1079,4 @@
"destination": "/platform/features/memory-export"
}
]
-}
+}
\ No newline at end of file
diff --git a/docs/images/openclaw-architecture.png b/docs/images/openclaw-architecture.png
new file mode 100644
index 000000000..2baec5a6c
Binary files /dev/null and b/docs/images/openclaw-architecture.png differ
diff --git a/docs/integrations/openclaw.mdx b/docs/integrations/openclaw.mdx
new file mode 100644
index 000000000..989b6b29a
--- /dev/null
+++ b/docs/integrations/openclaw.mdx
@@ -0,0 +1,172 @@
+---
+title: OpenClaw
+---
+
+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.
+
+## Overview
+
+
+
+
+
+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** — Five tools for explicit memory operations during conversations
+
+Both auto-recall and auto-capture run silently with no manual configuration required.
+
+## Installation
+
+```bash
+openclaw plugins install @mem0/openclaw-mem0
+```
+
+## Setup and Configuration
+
+### Platform Mode (Mem0 Cloud)
+
+Get your API key from [app.mem0.ai](https://app.mem0.ai).
+
+Add to your `openclaw.json`:
+
+```json5
+// plugins.entries
+"openclaw-mem0": {
+ "enabled": true,
+ "config": {
+ "apiKey": "${MEM0_API_KEY}",
+ "userId": "your-user-id"
+ }
+}
+```
+
+### Open-Source Mode (Self-hosted)
+
+No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings/LLM.
+
+```json5
+"openclaw-mem0": {
+ "enabled": true,
+ "config": {
+ "mode": "open-source",
+ "userId": "your-user-id"
+ }
+}
+```
+
+Sensible defaults work out of the box. To customize the embedder, vector store, or LLM:
+
+```json5
+"config": {
+ "mode": "open-source",
+ "userId": "your-user-id",
+ "oss": {
+ "embedder": { "provider": "openai", "config": { "model": "text-embedding-3-small" } },
+ "vectorStore": { "provider": "qdrant", "config": { "host": "localhost", "port": 6333 } },
+ "llm": { "provider": "openai", "config": { "model": "gpt-4o" } }
+ }
+}
+```
+
+All `oss` fields are optional. See [Mem0 OSS docs](/open-source/node-quickstart) for available providers.
+
+## Short-term vs Long-term Memory
+
+Memories are organized into two scopes:
+
+- **Session (short-term)** — Auto-capture stores memories scoped to the current session via Mem0's `run_id` / `runId` parameter. These are contextual to the ongoing conversation.
+
+- **User (long-term)** — The agent can explicitly store long-term memories using the `memory_store` tool (with `longTerm: true`, the default). These persist across all sessions for the user.
+
+During **auto-recall**, the plugin searches both scopes and presents them separately — long-term memories first, then session memories — so the agent has full context.
+
+## Agent Tools
+
+The agent gets five tools it can call during conversations:
+
+| Tool | Description |
+|------|-------------|
+| `memory_search` | Search memories by natural language |
+| `memory_list` | List all stored memories for a user |
+| `memory_store` | Explicitly save a fact |
+| `memory_get` | Retrieve a memory by ID |
+| `memory_forget` | Delete by ID or by query |
+
+The `memory_search` and `memory_list` tools accept a `scope` parameter (`"session"`, `"long-term"`, or `"all"`) to control which memories are queried. The `memory_store` tool accepts a `longTerm` boolean (default: `true`) to choose where to store.
+
+## CLI Commands
+
+```bash
+# Search all memories (long-term + session)
+openclaw mem0 search "what languages does the user know"
+
+# Search only long-term memories
+openclaw mem0 search "what languages does the user know" --scope long-term
+
+# Search only session/short-term memories
+openclaw mem0 search "what languages does the user know" --scope session
+
+# View stats
+openclaw mem0 stats
+```
+
+## Configuration Options
+
+### General Options
+
+| Key | Type | Default | Description |
+|-----|------|---------|-------------|
+| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use |
+| `userId` | `string` | `"default"` | Scope memories per user |
+| `autoRecall` | `boolean` | `true` | Inject memories before each turn |
+| `autoCapture` | `boolean` | `true` | Store facts after each turn |
+| `topK` | `number` | `5` | Max memories per recall |
+| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
+
+### Platform Mode Options
+
+| Key | Type | Default | Description |
+|-----|------|---------|-------------|
+| `apiKey` | `string` | — | **Required.** Mem0 API key (supports `${MEM0_API_KEY}`) |
+| `orgId` | `string` | — | Organization ID |
+| `projectId` | `string` | — | Project ID |
+| `enableGraph` | `boolean` | `false` | Entity graph for relationships |
+| `customInstructions` | `string` | *(built-in)* | Extraction rules — what to store, how to format |
+| `customCategories` | `object` | *(12 defaults)* | Category name → description map for tagging |
+
+### Open-Source Mode Options
+
+| Key | Type | Default | Description |
+|-----|------|---------|-------------|
+| `customPrompt` | `string` | *(built-in)* | Extraction prompt for memory processing |
+| `oss.embedder.provider` | `string` | `"openai"` | Embedding provider (`"openai"`, `"ollama"`, etc.) |
+| `oss.embedder.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL` |
+| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store (`"memory"`, `"qdrant"`, `"chroma"`, etc.) |
+| `oss.vectorStore.config` | `object` | — | Provider config: `host`, `port`, `collectionName`, `dimension` |
+| `oss.llm.provider` | `string` | `"openai"` | LLM provider (`"openai"`, `"anthropic"`, `"ollama"`, etc.) |
+| `oss.llm.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL`, `temperature` |
+| `oss.historyDbPath` | `string` | — | SQLite path for memory edit history |
+
+Everything inside `oss` is optional — defaults use OpenAI embeddings (`text-embedding-3-small`), in-memory vector store, and OpenAI LLM.
+
+## Key Features
+
+1. **Zero Configuration** — Auto-recall and auto-capture work out of the box with no prompting required
+2. **Dual Memory Scopes** — Session-scoped short-term and user-scoped long-term memories
+3. **Flexible Backend** — Use Mem0 Cloud for managed service or self-host with open-source mode
+4. **Rich Tool Suite** — Five agent tools for explicit memory operations when needed
+
+## Conclusion
+
+The `@mem0/openclaw-mem0` plugin gives OpenClaw agents persistent memory with minimal setup. Whether using Mem0 Cloud or self-hosting, your agents can now remember user preferences, facts, and context across sessions automatically.
+
+
+
+ Build agents with OpenAI's SDK and Mem0
+
+
+ Create stateful agent workflows with memory
+
+
diff --git a/openclaw/.gitignore b/openclaw/.gitignore
new file mode 100644
index 000000000..4913e17ad
--- /dev/null
+++ b/openclaw/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+package-lock.json
+*.db
diff --git a/openclaw/README.md b/openclaw/README.md
new file mode 100644
index 000000000..3f939d9de
--- /dev/null
+++ b/openclaw/README.md
@@ -0,0 +1,155 @@
+# @mem0/openclaw-mem0
+
+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 watches conversations, extracts what matters, and brings it back when relevant — automatically.
+
+## How it works
+
+
+
+
+
+**Auto-Recall** — Before the agent responds, the plugin searches Mem0 for memories that match the current message and injects them into context.
+
+**Auto-Capture** — After the agent responds, the plugin sends the exchange to Mem0. Mem0 decides what's worth keeping — new facts get stored, stale ones updated, duplicates merged.
+
+Both run silently. No prompting, no configuration, no manual calls.
+
+### Short-term vs long-term memory
+
+Memories are organized into two scopes:
+
+- **Session (short-term)** — Auto-capture stores memories scoped to the current session via Mem0's `run_id` / `runId` parameter. These are contextual to the ongoing conversation and automatically recalled alongside long-term memories.
+
+- **User (long-term)** — The agent can explicitly store long-term memories using the `memory_store` tool (with `longTerm: true`, the default). These persist across all sessions for the user.
+
+During **auto-recall**, the plugin searches both scopes and presents them separately — long-term memories first, then session memories — so the agent has full context.
+
+The agent tools (`memory_search`, `memory_list`) accept a `scope` parameter (`"session"`, `"long-term"`, or `"all"`) to control which memories are queried. The `memory_store` tool accepts a `longTerm` boolean (default: `true`) to choose where to store.
+
+All new parameters are optional and backward-compatible — existing configurations work without changes.
+
+## Setup
+
+```bash
+openclaw plugins install @mem0/openclaw-mem0
+```
+
+### Platform (Mem0 Cloud)
+
+Get an API key from [app.mem0.ai](https://app.mem0.ai), then add to your `openclaw.json`:
+
+```json5
+// plugins.entries
+"openclaw-mem0": {
+ "enabled": true,
+ "config": {
+ "apiKey": "${MEM0_API_KEY}",
+ "userId": "your-user-id"
+ }
+}
+```
+
+### Open-Source (Self-hosted)
+
+No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings/LLM.
+
+```json5
+"openclaw-mem0": {
+ "enabled": true,
+ "config": {
+ "mode": "open-source",
+ "userId": "your-user-id"
+ }
+}
+```
+
+Sensible defaults out of the box. To customize the embedder, vector store, or LLM:
+
+```json5
+"config": {
+ "mode": "open-source",
+ "userId": "your-user-id",
+ "oss": {
+ "embedder": { "provider": "openai", "config": { "model": "text-embedding-3-small" } },
+ "vectorStore": { "provider": "qdrant", "config": { "host": "localhost", "port": 6333 } },
+ "llm": { "provider": "openai", "config": { "model": "gpt-4o" } }
+ }
+}
+```
+
+All `oss` fields are optional. See [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for providers.
+
+## Agent tools
+
+The agent gets five tools it can call during conversations:
+
+| Tool | Description |
+|------|-------------|
+| `memory_search` | Search memories by natural language |
+| `memory_list` | List all stored memories for a user |
+| `memory_store` | Explicitly save a fact |
+| `memory_get` | Retrieve a memory by ID |
+| `memory_forget` | Delete by ID or by query |
+
+## CLI
+
+```bash
+# Search all memories (long-term + session)
+openclaw mem0 search "what languages does the user know"
+
+# Search only long-term memories
+openclaw mem0 search "what languages does the user know" --scope long-term
+
+# Search only session/short-term memories
+openclaw mem0 search "what languages does the user know" --scope session
+
+# Stats
+openclaw mem0 stats
+```
+
+## Options
+
+### General
+
+| Key | Type | Default | |
+|-----|------|---------|---|
+| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use |
+| `userId` | `string` | `"default"` | Scope memories per user |
+| `autoRecall` | `boolean` | `true` | Inject memories before each turn |
+| `autoCapture` | `boolean` | `true` | Store facts after each turn |
+| `topK` | `number` | `5` | Max memories per recall |
+| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
+
+### Platform mode
+
+| Key | Type | Default | |
+|-----|------|---------|---|
+| `apiKey` | `string` | — | **Required.** Mem0 API key (supports `${MEM0_API_KEY}`) |
+| `orgId` | `string` | — | Organization ID |
+| `projectId` | `string` | — | Project ID |
+| `enableGraph` | `boolean` | `false` | Entity graph for relationships |
+| `customInstructions` | `string` | *(built-in)* | Extraction rules — what to store, how to format |
+| `customCategories` | `object` | *(12 defaults)* | Category name → description map for tagging |
+
+### Open-source mode
+
+Works with zero extra config. The `oss` block lets you swap out any component:
+
+| Key | Type | Default | |
+|-----|------|---------|---|
+| `customPrompt` | `string` | *(built-in)* | Extraction prompt for memory processing |
+| `oss.embedder.provider` | `string` | `"openai"` | Embedding provider (`"openai"`, `"ollama"`, etc.) |
+| `oss.embedder.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL` |
+| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store (`"memory"`, `"qdrant"`, `"chroma"`, etc.) |
+| `oss.vectorStore.config` | `object` | — | Provider config: `host`, `port`, `collectionName`, `dimension` |
+| `oss.llm.provider` | `string` | `"openai"` | LLM provider (`"openai"`, `"anthropic"`, `"ollama"`, etc.) |
+| `oss.llm.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL`, `temperature` |
+| `oss.historyDbPath` | `string` | — | SQLite path for memory edit history |
+
+Everything inside `oss` is optional — defaults use OpenAI embeddings (`text-embedding-3-small`), in-memory vector store, and OpenAI LLM. Override only what you need.
+
+## License
+
+Apache 2.0
diff --git a/openclaw/index.ts b/openclaw/index.ts
new file mode 100644
index 000000000..09097761d
--- /dev/null
+++ b/openclaw/index.ts
@@ -0,0 +1,1381 @@
+/**
+ * OpenClaw Memory (Mem0) Plugin
+ *
+ * Long-term memory via Mem0 — supports both the Mem0 platform
+ * and the open-source self-hosted SDK. Uses the official `mem0ai` package.
+ *
+ * Features:
+ * - 5 tools: memory_search, memory_list, memory_store, memory_get, memory_forget
+ * (with session/long-term scope support via scope and longTerm parameters)
+ * - Short-term (session-scoped) and long-term (user-scoped) memory
+ * - Auto-recall: injects relevant memories (both scopes) before each agent turn
+ * - Auto-capture: stores key facts scoped to the current session after each agent turn
+ * - CLI: openclaw mem0 search, openclaw mem0 stats
+ * - Dual mode: platform or open-source (self-hosted)
+ */
+
+import { Type } from "@sinclair/typebox";
+import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
+
+// ============================================================================
+// Types
+// ============================================================================
+
+type Mem0Mode = "platform" | "open-source";
+
+type Mem0Config = {
+ mode: Mem0Mode;
+ // Platform-specific
+ apiKey?: string;
+ orgId?: string;
+ projectId?: string;
+ customInstructions: string;
+ customCategories: Record;
+ enableGraph: boolean;
+ // OSS-specific
+ customPrompt?: string;
+ oss?: {
+ embedder?: { provider: string; config: Record };
+ vectorStore?: { provider: string; config: Record };
+ llm?: { provider: string; config: Record };
+ historyDbPath?: string;
+ };
+ // Shared
+ userId: string;
+ autoCapture: boolean;
+ autoRecall: boolean;
+ searchThreshold: number;
+ topK: number;
+};
+
+// Unified types for the provider interface
+interface AddOptions {
+ user_id: string;
+ run_id?: string;
+ custom_instructions?: string;
+ custom_categories?: Array>;
+ enable_graph?: boolean;
+ output_format?: string;
+}
+
+interface SearchOptions {
+ user_id: string;
+ run_id?: string;
+ top_k?: number;
+ threshold?: number;
+ limit?: number;
+ keyword_search?: boolean;
+ reranking?: boolean;
+}
+
+interface ListOptions {
+ user_id: string;
+ run_id?: string;
+ page_size?: number;
+}
+
+interface MemoryItem {
+ id: string;
+ memory: string;
+ user_id?: string;
+ score?: number;
+ categories?: string[];
+ metadata?: Record;
+ created_at?: string;
+ updated_at?: string;
+}
+
+interface AddResultItem {
+ id: string;
+ memory: string;
+ event: "ADD" | "UPDATE" | "DELETE" | "NOOP";
+}
+
+interface AddResult {
+ results: AddResultItem[];
+}
+
+// ============================================================================
+// Unified Provider Interface
+// ============================================================================
+
+interface Mem0Provider {
+ add(
+ messages: Array<{ role: string; content: string }>,
+ options: AddOptions,
+ ): Promise;
+ search(query: string, options: SearchOptions): Promise;
+ get(memoryId: string): Promise;
+ getAll(options: ListOptions): Promise;
+ delete(memoryId: string): Promise;
+}
+
+// ============================================================================
+// Platform Provider (Mem0 Cloud)
+// ============================================================================
+
+class PlatformProvider implements Mem0Provider {
+ private client: any; // MemoryClient from mem0ai
+ private initPromise: Promise | null = null;
+
+ constructor(
+ private readonly apiKey: string,
+ private readonly orgId?: string,
+ private readonly projectId?: string,
+ ) { }
+
+ private async ensureClient(): Promise {
+ if (this.client) return;
+ if (this.initPromise) return this.initPromise;
+ this.initPromise = this._init();
+ return this.initPromise;
+ }
+
+ private async _init(): Promise {
+ const { default: MemoryClient } = await import("mem0ai");
+ const opts: Record = { apiKey: this.apiKey };
+ if (this.orgId) opts.org_id = this.orgId;
+ if (this.projectId) opts.project_id = this.projectId;
+ this.client = new MemoryClient(opts);
+ }
+
+ async add(
+ messages: Array<{ role: string; content: string }>,
+ options: AddOptions,
+ ): Promise {
+ await this.ensureClient();
+ const opts: Record = { user_id: options.user_id };
+ if (options.run_id) opts.run_id = options.run_id;
+ if (options.custom_instructions)
+ opts.custom_instructions = options.custom_instructions;
+ if (options.custom_categories)
+ opts.custom_categories = options.custom_categories;
+ if (options.enable_graph) opts.enable_graph = options.enable_graph;
+ if (options.output_format) opts.output_format = options.output_format;
+
+ const result = await this.client.add(messages, opts);
+ return normalizeAddResult(result);
+ }
+
+ async search(query: string, options: SearchOptions): Promise {
+ await this.ensureClient();
+ const opts: Record = { user_id: options.user_id };
+ if (options.run_id) opts.run_id = options.run_id;
+ if (options.top_k != null) opts.top_k = options.top_k;
+ if (options.threshold != null) opts.threshold = options.threshold;
+ if (options.keyword_search != null) opts.keyword_search = options.keyword_search;
+ if (options.reranking != null) opts.reranking = options.reranking;
+
+ const results = await this.client.search(query, opts);
+ return normalizeSearchResults(results);
+ }
+
+ async get(memoryId: string): Promise {
+ await this.ensureClient();
+ const result = await this.client.get(memoryId);
+ return normalizeMemoryItem(result);
+ }
+
+ async getAll(options: ListOptions): Promise {
+ await this.ensureClient();
+ const opts: Record = { user_id: options.user_id };
+ if (options.run_id) opts.run_id = options.run_id;
+ if (options.page_size != null) opts.page_size = options.page_size;
+
+ const results = await this.client.getAll(opts);
+ if (Array.isArray(results)) return results.map(normalizeMemoryItem);
+ // Some versions return { results: [...] }
+ if (results?.results && Array.isArray(results.results))
+ return results.results.map(normalizeMemoryItem);
+ return [];
+ }
+
+ async delete(memoryId: string): Promise {
+ await this.ensureClient();
+ await this.client.delete(memoryId);
+ }
+}
+
+// ============================================================================
+// Open-Source Provider (Self-hosted)
+// ============================================================================
+
+class OSSProvider implements Mem0Provider {
+ private memory: any; // Memory from mem0ai/oss
+ private initPromise: Promise | null = null;
+
+ constructor(
+ private readonly ossConfig?: Mem0Config["oss"],
+ private readonly customPrompt?: string,
+ private readonly resolvePath?: (p: string) => string,
+ ) { }
+
+ private async ensureMemory(): Promise {
+ if (this.memory) return;
+ if (this.initPromise) return this.initPromise;
+ this.initPromise = this._init();
+ return this.initPromise;
+ }
+
+ private async _init(): Promise {
+ const { Memory } = await import("mem0ai/oss");
+
+ const config: Record = { version: "v1.1" };
+
+ if (this.ossConfig?.embedder) config.embedder = this.ossConfig.embedder;
+ if (this.ossConfig?.vectorStore)
+ config.vectorStore = this.ossConfig.vectorStore;
+ if (this.ossConfig?.llm) config.llm = this.ossConfig.llm;
+
+ if (this.ossConfig?.historyDbPath) {
+ const dbPath = this.resolvePath
+ ? this.resolvePath(this.ossConfig.historyDbPath)
+ : this.ossConfig.historyDbPath;
+ config.historyDbPath = dbPath;
+ }
+
+ if (this.customPrompt) config.customPrompt = this.customPrompt;
+
+ this.memory = new Memory(config);
+ }
+
+ async add(
+ messages: Array<{ role: string; content: string }>,
+ options: AddOptions,
+ ): Promise {
+ await this.ensureMemory();
+ // OSS SDK uses camelCase: userId/runId, not user_id/run_id
+ const addOpts: Record = { userId: options.user_id };
+ if (options.run_id) addOpts.runId = options.run_id;
+ const result = await this.memory.add(messages, addOpts);
+ return normalizeAddResult(result);
+ }
+
+ async search(query: string, options: SearchOptions): Promise {
+ await this.ensureMemory();
+ // OSS SDK uses camelCase: userId/runId, not user_id/run_id
+ const opts: Record = { userId: options.user_id };
+ if (options.run_id) opts.runId = options.run_id;
+ if (options.limit != null) opts.limit = options.limit;
+ else if (options.top_k != null) opts.limit = options.top_k;
+ if (options.keyword_search != null) opts.keyword_search = options.keyword_search;
+ if (options.reranking != null) opts.reranking = options.reranking;
+
+ const results = await this.memory.search(query, opts);
+ return normalizeSearchResults(results);
+ }
+
+ async get(memoryId: string): Promise {
+ await this.ensureMemory();
+ const result = await this.memory.get(memoryId);
+ return normalizeMemoryItem(result);
+ }
+
+ async getAll(options: ListOptions): Promise {
+ await this.ensureMemory();
+ // OSS SDK uses camelCase: userId/runId, not user_id/run_id
+ const getAllOpts: Record = { userId: options.user_id };
+ if (options.run_id) getAllOpts.runId = options.run_id;
+ const results = await this.memory.getAll(getAllOpts);
+ if (Array.isArray(results)) return results.map(normalizeMemoryItem);
+ if (results?.results && Array.isArray(results.results))
+ return results.results.map(normalizeMemoryItem);
+ return [];
+ }
+
+ async delete(memoryId: string): Promise {
+ await this.ensureMemory();
+ await this.memory.delete(memoryId);
+ }
+}
+
+// ============================================================================
+// Result Normalizers
+// ============================================================================
+
+function normalizeMemoryItem(raw: any): MemoryItem {
+ return {
+ id: raw.id ?? raw.memory_id ?? "",
+ memory: raw.memory ?? raw.text ?? raw.content ?? "",
+ // Handle both platform (user_id, created_at) and OSS (userId, createdAt) field names
+ user_id: raw.user_id ?? raw.userId,
+ score: raw.score,
+ categories: raw.categories,
+ metadata: raw.metadata,
+ created_at: raw.created_at ?? raw.createdAt,
+ updated_at: raw.updated_at ?? raw.updatedAt,
+ };
+}
+
+function normalizeSearchResults(raw: any): MemoryItem[] {
+ // Platform API returns flat array, OSS returns { results: [...] }
+ if (Array.isArray(raw)) return raw.map(normalizeMemoryItem);
+ if (raw?.results && Array.isArray(raw.results))
+ return raw.results.map(normalizeMemoryItem);
+ return [];
+}
+
+function normalizeAddResult(raw: any): AddResult {
+ // Handle { results: [...] } shape (both platform and OSS)
+ if (raw?.results && Array.isArray(raw.results)) {
+ return {
+ results: raw.results.map((r: any) => ({
+ id: r.id ?? r.memory_id ?? "",
+ memory: r.memory ?? r.text ?? "",
+ // Platform API may return PENDING status (async processing)
+ // OSS stores event in metadata.event
+ event: r.event ?? r.metadata?.event ?? (r.status === "PENDING" ? "ADD" : "ADD"),
+ })),
+ };
+ }
+ // Platform API without output_format returns flat array
+ if (Array.isArray(raw)) {
+ return {
+ results: raw.map((r: any) => ({
+ id: r.id ?? r.memory_id ?? "",
+ memory: r.memory ?? r.text ?? "",
+ event: r.event ?? r.metadata?.event ?? (r.status === "PENDING" ? "ADD" : "ADD"),
+ })),
+ };
+ }
+ return { results: [] };
+}
+
+// ============================================================================
+// Config Parser
+// ============================================================================
+
+function resolveEnvVars(value: string): string {
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
+ const envValue = process.env[envVar];
+ if (!envValue) {
+ throw new Error(`Environment variable ${envVar} is not set`);
+ }
+ return envValue;
+ });
+}
+
+function resolveEnvVarsDeep(obj: Record): Record {
+ const result: Record = {};
+ for (const [key, value] of Object.entries(obj)) {
+ if (typeof value === "string") {
+ result[key] = resolveEnvVars(value);
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
+ result[key] = resolveEnvVarsDeep(value as Record);
+ } else {
+ result[key] = value;
+ }
+ }
+ return result;
+}
+
+// ============================================================================
+// Default Custom Instructions & Categories
+// ============================================================================
+
+const DEFAULT_CUSTOM_INSTRUCTIONS = `Your Task: Extract and maintain a structured, evolving profile of the user from their conversations with an AI assistant. Capture information that would help the assistant provide personalized, context-aware responses in future interactions.
+
+Information to Extract:
+
+1. Identity & Demographics:
+ - Name, age, location, timezone, language preferences
+ - Occupation, employer, job role, industry
+ - Education background
+
+2. Preferences & Opinions:
+ - Communication style preferences (formal/casual, verbose/concise)
+ - Tool and technology preferences (languages, frameworks, editors, OS)
+ - Content preferences (topics of interest, learning style)
+ - Strong opinions or values they've expressed
+ - Likes and dislikes they've explicitly stated
+
+3. Goals & Projects:
+ - Current projects they're working on (name, description, status)
+ - Short-term and long-term goals
+ - Deadlines and milestones mentioned
+ - Problems they're actively trying to solve
+
+4. Technical Context:
+ - Tech stack and tools they use
+ - Skill level in different areas (beginner/intermediate/expert)
+ - Development environment and setup details
+ - Recurring technical challenges
+
+5. Relationships & People:
+ - Names and roles of people they mention (colleagues, family, friends)
+ - Team structure and dynamics
+ - Key contacts and their relevance
+
+6. Decisions & Lessons:
+ - Important decisions made and their reasoning
+ - Lessons learned from past experiences
+ - Strategies that worked or failed
+ - Changed opinions or updated beliefs
+
+7. Routines & Habits:
+ - Daily routines and schedules mentioned
+ - Work patterns (when they're productive, how they organize work)
+ - Health and wellness habits if voluntarily shared
+
+8. Life Events:
+ - Significant events (new job, moving, milestones)
+ - Upcoming events or plans
+ - Changes in circumstances
+
+Guidelines:
+- Store memories as clear, self-contained statements (each memory should make sense on its own)
+- Use third person: "User prefers..." not "I prefer..."
+- Include temporal context when relevant: "As of [date], user is working on..."
+- When information updates, UPDATE the existing memory rather than creating duplicates
+- Merge related facts into single coherent memories when possible
+- Preserve specificity: "User uses Next.js 14 with App Router" is better than "User uses React"
+- Capture the WHY behind preferences when stated: "User prefers Vim because of keyboard-driven workflow"
+
+Exclude:
+- Passwords, API keys, tokens, or any authentication credentials
+- Exact financial amounts (account balances, salaries) unless the user explicitly asks to remember them
+- Temporary or ephemeral information (one-time questions, debugging sessions with no lasting insight)
+- Generic small talk with no informational content
+- The assistant's own responses unless they contain a commitment or promise to the user
+- Raw code snippets (capture the intent/decision, not the code itself)
+- Information the user explicitly asks not to remember`;
+
+const DEFAULT_CUSTOM_CATEGORIES: Record = {
+ identity:
+ "Personal identity information: name, age, location, timezone, occupation, employer, education, demographics",
+ preferences:
+ "Explicitly stated likes, dislikes, preferences, opinions, and values across any domain",
+ goals:
+ "Current and future goals, aspirations, objectives, targets the user is working toward",
+ projects:
+ "Specific projects, initiatives, or endeavors the user is working on, including status and details",
+ technical:
+ "Technical skills, tools, tech stack, development environment, programming languages, frameworks",
+ decisions:
+ "Important decisions made, reasoning behind choices, strategy changes, and their outcomes",
+ relationships:
+ "People mentioned by the user: colleagues, family, friends, their roles and relevance",
+ routines:
+ "Daily habits, work patterns, schedules, productivity routines, health and wellness habits",
+ life_events:
+ "Significant life events, milestones, transitions, upcoming plans and changes",
+ lessons:
+ "Lessons learned, insights gained, mistakes acknowledged, changed opinions or beliefs",
+ work:
+ "Work-related context: job responsibilities, workplace dynamics, career progression, professional challenges",
+ health:
+ "Health-related information voluntarily shared: conditions, medications, fitness, wellness goals",
+};
+
+// ============================================================================
+// Config Schema
+// ============================================================================
+
+const ALLOWED_KEYS = [
+ "mode",
+ "apiKey",
+ "userId",
+ "orgId",
+ "projectId",
+ "autoCapture",
+ "autoRecall",
+ "customInstructions",
+ "customCategories",
+ "customPrompt",
+ "enableGraph",
+ "searchThreshold",
+ "topK",
+ "oss",
+];
+
+function assertAllowedKeys(
+ value: Record,
+ allowed: string[],
+ label: string,
+) {
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
+ if (unknown.length === 0) return;
+ throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
+}
+
+const mem0ConfigSchema = {
+ parse(value: unknown): Mem0Config {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ throw new Error("openclaw-mem0 config required");
+ }
+ const cfg = value as Record;
+ assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-mem0 config");
+
+ // Accept both "open-source" and legacy "oss" as open-source mode; everything else is platform
+ const mode: Mem0Mode =
+ cfg.mode === "oss" || cfg.mode === "open-source" ? "open-source" : "platform";
+
+ // Platform mode requires apiKey
+ if (mode === "platform") {
+ if (typeof cfg.apiKey !== "string" || !cfg.apiKey) {
+ throw new Error(
+ "apiKey is required for platform mode (set mode: \"open-source\" for self-hosted)",
+ );
+ }
+ }
+
+ // Resolve env vars in oss config
+ let ossConfig: Mem0Config["oss"];
+ if (cfg.oss && typeof cfg.oss === "object" && !Array.isArray(cfg.oss)) {
+ ossConfig = resolveEnvVarsDeep(
+ cfg.oss as Record,
+ ) as unknown as Mem0Config["oss"];
+ }
+
+ return {
+ mode,
+ apiKey:
+ typeof cfg.apiKey === "string" ? resolveEnvVars(cfg.apiKey) : undefined,
+ userId:
+ typeof cfg.userId === "string" && cfg.userId ? cfg.userId : "default",
+ orgId: typeof cfg.orgId === "string" ? cfg.orgId : undefined,
+ projectId: typeof cfg.projectId === "string" ? cfg.projectId : undefined,
+ autoCapture: cfg.autoCapture !== false,
+ autoRecall: cfg.autoRecall !== false,
+ customInstructions:
+ typeof cfg.customInstructions === "string"
+ ? cfg.customInstructions
+ : DEFAULT_CUSTOM_INSTRUCTIONS,
+ customCategories:
+ cfg.customCategories &&
+ typeof cfg.customCategories === "object" &&
+ !Array.isArray(cfg.customCategories)
+ ? (cfg.customCategories as Record)
+ : DEFAULT_CUSTOM_CATEGORIES,
+ customPrompt:
+ typeof cfg.customPrompt === "string"
+ ? cfg.customPrompt
+ : DEFAULT_CUSTOM_INSTRUCTIONS,
+ enableGraph: cfg.enableGraph === true,
+ searchThreshold:
+ typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5,
+ topK: typeof cfg.topK === "number" ? cfg.topK : 5,
+ oss: ossConfig,
+ };
+ },
+};
+
+// ============================================================================
+// Provider Factory
+// ============================================================================
+
+function createProvider(
+ cfg: Mem0Config,
+ api: OpenClawPluginApi,
+): Mem0Provider {
+ if (cfg.mode === "open-source") {
+ return new OSSProvider(cfg.oss, cfg.customPrompt, (p) =>
+ api.resolvePath(p),
+ );
+ }
+
+ return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId);
+}
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+/** Convert Record categories to the array format mem0ai expects */
+function categoriesToArray(
+ cats: Record,
+): Array> {
+ return Object.entries(cats).map(([key, value]) => ({ [key]: value }));
+}
+
+// ============================================================================
+// Plugin Definition
+// ============================================================================
+
+const memoryPlugin = {
+ id: "openclaw-mem0",
+ name: "Memory (Mem0)",
+ description:
+ "Mem0 memory backend — Mem0 platform or self-hosted open-source",
+ kind: "memory" as const,
+ configSchema: mem0ConfigSchema,
+
+ register(api: OpenClawPluginApi) {
+ const cfg = mem0ConfigSchema.parse(api.pluginConfig);
+ const provider = createProvider(cfg, api);
+
+ // Track current session ID for tool-level session scoping
+ let currentSessionId: string | undefined;
+
+ api.logger.info(
+ `openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, graph: ${cfg.enableGraph}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
+ );
+
+ // Helper: build add options
+ function buildAddOptions(userIdOverride?: string, runId?: string): AddOptions {
+ const opts: AddOptions = {
+ user_id: userIdOverride || cfg.userId,
+ };
+ if (runId) opts.run_id = runId;
+ if (cfg.mode === "platform") {
+ opts.custom_instructions = cfg.customInstructions;
+ opts.custom_categories = categoriesToArray(cfg.customCategories);
+ opts.enable_graph = cfg.enableGraph;
+ opts.output_format = "v1.1";
+ }
+ return opts;
+ }
+
+ // Helper: build search options
+ function buildSearchOptions(
+ userIdOverride?: string,
+ limit?: number,
+ runId?: string,
+ ): SearchOptions {
+ const opts: SearchOptions = {
+ user_id: userIdOverride || cfg.userId,
+ top_k: limit ?? cfg.topK,
+ limit: limit ?? cfg.topK,
+ threshold: cfg.searchThreshold,
+ keyword_search: true,
+ reranking: true,
+ };
+ if (runId) opts.run_id = runId;
+ return opts;
+ }
+
+ // ========================================================================
+ // Tools
+ // ========================================================================
+
+ api.registerTool(
+ {
+ name: "memory_search",
+ label: "Memory Search",
+ description:
+ "Search through long-term memories stored in Mem0. Use when you need context about user preferences, past decisions, or previously discussed topics.",
+ parameters: Type.Object({
+ query: Type.String({ description: "Search query" }),
+ limit: Type.Optional(
+ Type.Number({
+ description: `Max results (default: ${cfg.topK})`,
+ }),
+ ),
+ userId: Type.Optional(
+ Type.String({
+ description:
+ "User ID to scope search (default: configured userId)",
+ }),
+ ),
+ scope: Type.Optional(
+ Type.Union([
+ Type.Literal("session"),
+ Type.Literal("long-term"),
+ Type.Literal("all"),
+ ], {
+ description:
+ 'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
+ }),
+ ),
+ }),
+ async execute(_toolCallId, params) {
+ const { query, limit, userId, scope = "all" } = params as {
+ query: string;
+ limit?: number;
+ userId?: string;
+ scope?: "session" | "long-term" | "all";
+ };
+
+ try {
+ let results: MemoryItem[] = [];
+
+ if (scope === "session") {
+ if (currentSessionId) {
+ results = await provider.search(
+ query,
+ buildSearchOptions(userId, limit, currentSessionId),
+ );
+ }
+ } else if (scope === "long-term") {
+ results = await provider.search(
+ query,
+ buildSearchOptions(userId, limit),
+ );
+ } else {
+ // "all" — search both scopes and combine
+ const longTermResults = await provider.search(
+ query,
+ buildSearchOptions(userId, limit),
+ );
+ let sessionResults: MemoryItem[] = [];
+ if (currentSessionId) {
+ sessionResults = await provider.search(
+ query,
+ buildSearchOptions(userId, limit, currentSessionId),
+ );
+ }
+ // Deduplicate by ID, preferring long-term
+ const seen = new Set(longTermResults.map((r) => r.id));
+ results = [
+ ...longTermResults,
+ ...sessionResults.filter((r) => !seen.has(r.id)),
+ ];
+ }
+
+ if (!results || results.length === 0) {
+ return {
+ content: [
+ { type: "text", text: "No relevant memories found." },
+ ],
+ details: { count: 0 },
+ };
+ }
+
+ const text = results
+ .map(
+ (r, i) =>
+ `${i + 1}. ${r.memory} (score: ${((r.score ?? 0) * 100).toFixed(0)}%, id: ${r.id})`,
+ )
+ .join("\n");
+
+ const sanitized = results.map((r) => ({
+ id: r.id,
+ memory: r.memory,
+ score: r.score,
+ categories: r.categories,
+ created_at: r.created_at,
+ }));
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Found ${results.length} memories:\n\n${text}`,
+ },
+ ],
+ details: { count: results.length, memories: sanitized },
+ };
+ } catch (err) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Memory search failed: ${String(err)}`,
+ },
+ ],
+ details: { error: String(err) },
+ };
+ }
+ },
+ },
+ { name: "memory_search" },
+ );
+
+ api.registerTool(
+ {
+ name: "memory_store",
+ label: "Memory Store",
+ description:
+ "Save important information in long-term memory via Mem0. Use for preferences, facts, decisions, and anything worth remembering.",
+ parameters: Type.Object({
+ text: Type.String({ description: "Information to remember" }),
+ userId: Type.Optional(
+ Type.String({
+ description: "User ID to scope this memory",
+ }),
+ ),
+ metadata: Type.Optional(
+ Type.Record(Type.String(), Type.Unknown(), {
+ description: "Optional metadata to attach to this memory",
+ }),
+ ),
+ longTerm: Type.Optional(
+ Type.Boolean({
+ description:
+ "Store as long-term (user-scoped) memory. Default: true. Set to false for session-scoped memory.",
+ }),
+ ),
+ }),
+ async execute(_toolCallId, params) {
+ const { text, userId, longTerm = true } = params as {
+ text: string;
+ userId?: string;
+ metadata?: Record;
+ longTerm?: boolean;
+ };
+
+ try {
+ const runId = !longTerm && currentSessionId ? currentSessionId : undefined;
+ const result = await provider.add(
+ [{ role: "user", content: text }],
+ buildAddOptions(userId, runId),
+ );
+
+ const added =
+ result.results?.filter((r) => r.event === "ADD") ?? [];
+ const updated =
+ result.results?.filter((r) => r.event === "UPDATE") ?? [];
+
+ const summary = [];
+ if (added.length > 0)
+ summary.push(
+ `${added.length} new memor${added.length === 1 ? "y" : "ies"} added`,
+ );
+ if (updated.length > 0)
+ summary.push(
+ `${updated.length} memor${updated.length === 1 ? "y" : "ies"} updated`,
+ );
+ if (summary.length === 0)
+ summary.push("No new memories extracted");
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Stored: ${summary.join(", ")}. ${result.results?.map((r) => `[${r.event}] ${r.memory}`).join("; ") ?? ""}`,
+ },
+ ],
+ details: {
+ action: "stored",
+ results: result.results,
+ },
+ };
+ } catch (err) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Memory store failed: ${String(err)}`,
+ },
+ ],
+ details: { error: String(err) },
+ };
+ }
+ },
+ },
+ { name: "memory_store" },
+ );
+
+ api.registerTool(
+ {
+ name: "memory_get",
+ label: "Memory Get",
+ description: "Retrieve a specific memory by its ID from Mem0.",
+ parameters: Type.Object({
+ memoryId: Type.String({ description: "The memory ID to retrieve" }),
+ }),
+ async execute(_toolCallId, params) {
+ const { memoryId } = params as { memoryId: string };
+
+ try {
+ const memory = await provider.get(memoryId);
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Memory ${memory.id}:\n${memory.memory}\n\nCreated: ${memory.created_at ?? "unknown"}\nUpdated: ${memory.updated_at ?? "unknown"}`,
+ },
+ ],
+ details: { memory },
+ };
+ } catch (err) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Memory get failed: ${String(err)}`,
+ },
+ ],
+ details: { error: String(err) },
+ };
+ }
+ },
+ },
+ { name: "memory_get" },
+ );
+
+ api.registerTool(
+ {
+ name: "memory_list",
+ label: "Memory List",
+ description:
+ "List all stored memories for a user. Use this when you want to see everything that's been remembered, rather than searching for something specific.",
+ parameters: Type.Object({
+ userId: Type.Optional(
+ Type.String({
+ description:
+ "User ID to list memories for (default: configured userId)",
+ }),
+ ),
+ scope: Type.Optional(
+ Type.Union([
+ Type.Literal("session"),
+ Type.Literal("long-term"),
+ Type.Literal("all"),
+ ], {
+ description:
+ 'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
+ }),
+ ),
+ }),
+ async execute(_toolCallId, params) {
+ const { userId, scope = "all" } = params as { userId?: string; scope?: "session" | "long-term" | "all" };
+
+ try {
+ let memories: MemoryItem[] = [];
+ const uid = userId || cfg.userId;
+
+ if (scope === "session") {
+ if (currentSessionId) {
+ memories = await provider.getAll({
+ user_id: uid,
+ run_id: currentSessionId,
+ });
+ }
+ } else if (scope === "long-term") {
+ memories = await provider.getAll({ user_id: uid });
+ } else {
+ // "all" — combine both scopes
+ const longTerm = await provider.getAll({ user_id: uid });
+ let session: MemoryItem[] = [];
+ if (currentSessionId) {
+ session = await provider.getAll({
+ user_id: uid,
+ run_id: currentSessionId,
+ });
+ }
+ const seen = new Set(longTerm.map((r) => r.id));
+ memories = [
+ ...longTerm,
+ ...session.filter((r) => !seen.has(r.id)),
+ ];
+ }
+
+ if (!memories || memories.length === 0) {
+ return {
+ content: [
+ { type: "text", text: "No memories stored yet." },
+ ],
+ details: { count: 0 },
+ };
+ }
+
+ const text = memories
+ .map(
+ (r, i) =>
+ `${i + 1}. ${r.memory} (id: ${r.id})`,
+ )
+ .join("\n");
+
+ const sanitized = memories.map((r) => ({
+ id: r.id,
+ memory: r.memory,
+ categories: r.categories,
+ created_at: r.created_at,
+ }));
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: `${memories.length} memories:\n\n${text}`,
+ },
+ ],
+ details: { count: memories.length, memories: sanitized },
+ };
+ } catch (err) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Memory list failed: ${String(err)}`,
+ },
+ ],
+ details: { error: String(err) },
+ };
+ }
+ },
+ },
+ { name: "memory_list" },
+ );
+
+ api.registerTool(
+ {
+ name: "memory_forget",
+ label: "Memory Forget",
+ description:
+ "Delete memories from Mem0. Provide a specific memoryId to delete directly, or a query to search and delete matching memories. GDPR-compliant.",
+ parameters: Type.Object({
+ query: Type.Optional(
+ Type.String({
+ description: "Search query to find memory to delete",
+ }),
+ ),
+ memoryId: Type.Optional(
+ Type.String({ description: "Specific memory ID to delete" }),
+ ),
+ }),
+ async execute(_toolCallId, params) {
+ const { query, memoryId } = params as {
+ query?: string;
+ memoryId?: string;
+ };
+
+ try {
+ if (memoryId) {
+ await provider.delete(memoryId);
+ return {
+ content: [
+ { type: "text", text: `Memory ${memoryId} forgotten.` },
+ ],
+ details: { action: "deleted", id: memoryId },
+ };
+ }
+
+ if (query) {
+ const results = await provider.search(
+ query,
+ buildSearchOptions(undefined, 5),
+ );
+
+ if (!results || results.length === 0) {
+ return {
+ content: [
+ { type: "text", text: "No matching memories found." },
+ ],
+ details: { found: 0 },
+ };
+ }
+
+ // If single high-confidence match, delete directly
+ if (
+ results.length === 1 ||
+ (results[0].score ?? 0) > 0.9
+ ) {
+ await provider.delete(results[0].id);
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Forgotten: "${results[0].memory}"`,
+ },
+ ],
+ details: { action: "deleted", id: results[0].id },
+ };
+ }
+
+ const list = results
+ .map(
+ (r) =>
+ `- [${r.id}] ${r.memory.slice(0, 80)}${r.memory.length > 80 ? "..." : ""} (score: ${((r.score ?? 0) * 100).toFixed(0)}%)`,
+ )
+ .join("\n");
+
+ const candidates = results.map((r) => ({
+ id: r.id,
+ memory: r.memory,
+ score: r.score,
+ }));
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Found ${results.length} candidates. Specify memoryId to delete:\n${list}`,
+ },
+ ],
+ details: { action: "candidates", candidates },
+ };
+ }
+
+ return {
+ content: [
+ { type: "text", text: "Provide a query or memoryId." },
+ ],
+ details: { error: "missing_param" },
+ };
+ } catch (err) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Memory forget failed: ${String(err)}`,
+ },
+ ],
+ details: { error: String(err) },
+ };
+ }
+ },
+ },
+ { name: "memory_forget" },
+ );
+
+ // ========================================================================
+ // CLI Commands
+ // ========================================================================
+
+ api.registerCli(
+ ({ program }) => {
+ const mem0 = program
+ .command("mem0")
+ .description("Mem0 memory plugin commands");
+
+ mem0
+ .command("search")
+ .description("Search memories in Mem0")
+ .argument("", "Search query")
+ .option("--limit ", "Max results", String(cfg.topK))
+ .option("--scope ", 'Memory scope: "session", "long-term", or "all"', "all")
+ .action(async (query: string, opts: { limit: string; scope: string }) => {
+ try {
+ const limit = parseInt(opts.limit, 10);
+ const scope = opts.scope as "session" | "long-term" | "all";
+
+ let allResults: MemoryItem[] = [];
+
+ if (scope === "session" || scope === "all") {
+ if (currentSessionId) {
+ const sessionResults = await provider.search(
+ query,
+ buildSearchOptions(undefined, limit, currentSessionId),
+ );
+ if (sessionResults?.length) {
+ allResults.push(...sessionResults.map((r) => ({ ...r, _scope: "session" as const })));
+ }
+ } else if (scope === "session") {
+ console.log("No active session ID available for session-scoped search.");
+ return;
+ }
+ }
+
+ if (scope === "long-term" || scope === "all") {
+ const longTermResults = await provider.search(
+ query,
+ buildSearchOptions(undefined, limit),
+ );
+ if (longTermResults?.length) {
+ allResults.push(...longTermResults.map((r) => ({ ...r, _scope: "long-term" as const })));
+ }
+ }
+
+ // Deduplicate by ID when searching "all"
+ if (scope === "all") {
+ const seen = new Set();
+ allResults = allResults.filter((r) => {
+ if (seen.has(r.id)) return false;
+ seen.add(r.id);
+ return true;
+ });
+ }
+
+ if (!allResults.length) {
+ console.log("No memories found.");
+ return;
+ }
+
+ const output = allResults.map((r) => ({
+ id: r.id,
+ memory: r.memory,
+ score: r.score,
+ scope: (r as any)._scope,
+ categories: r.categories,
+ created_at: r.created_at,
+ }));
+ console.log(JSON.stringify(output, null, 2));
+ } catch (err) {
+ console.error(`Search failed: ${String(err)}`);
+ }
+ });
+
+ mem0
+ .command("stats")
+ .description("Show memory statistics from Mem0")
+ .action(async () => {
+ try {
+ const memories = await provider.getAll({
+ user_id: cfg.userId,
+ });
+ console.log(`Mode: ${cfg.mode}`);
+ console.log(`User: ${cfg.userId}`);
+ console.log(
+ `Total memories: ${Array.isArray(memories) ? memories.length : "unknown"}`,
+ );
+ console.log(`Graph enabled: ${cfg.enableGraph}`);
+ console.log(
+ `Auto-recall: ${cfg.autoRecall}, Auto-capture: ${cfg.autoCapture}`,
+ );
+ } catch (err) {
+ console.error(`Stats failed: ${String(err)}`);
+ }
+ });
+ },
+ { commands: ["mem0"] },
+ );
+
+ // ========================================================================
+ // Lifecycle Hooks
+ // ========================================================================
+
+ // Auto-recall: inject relevant memories before agent starts
+ if (cfg.autoRecall) {
+ api.on("before_agent_start", async (event, ctx) => {
+ if (!event.prompt || event.prompt.length < 5) return;
+
+ // Track session ID
+ const sessionId = (ctx as any)?.sessionKey ?? undefined;
+ if (sessionId) currentSessionId = sessionId;
+
+ try {
+ // Search long-term memories (user-scoped)
+ const longTermResults = await provider.search(
+ event.prompt,
+ buildSearchOptions(),
+ );
+
+ // Search session memories (session-scoped) if we have a session ID
+ let sessionResults: MemoryItem[] = [];
+ if (currentSessionId) {
+ sessionResults = await provider.search(
+ event.prompt,
+ buildSearchOptions(undefined, undefined, currentSessionId),
+ );
+ }
+
+ // Deduplicate session results against long-term
+ const longTermIds = new Set(longTermResults.map((r) => r.id));
+ const uniqueSessionResults = sessionResults.filter(
+ (r) => !longTermIds.has(r.id),
+ );
+
+ if (longTermResults.length === 0 && uniqueSessionResults.length === 0) return;
+
+ // Build context with clear labels
+ let memoryContext = "";
+ if (longTermResults.length > 0) {
+ memoryContext += longTermResults
+ .map(
+ (r) =>
+ `- ${r.memory}${r.categories?.length ? ` [${r.categories.join(", ")}]` : ""}`,
+ )
+ .join("\n");
+ }
+ if (uniqueSessionResults.length > 0) {
+ if (memoryContext) memoryContext += "\n";
+ memoryContext += "\nSession memories:\n";
+ memoryContext += uniqueSessionResults
+ .map((r) => `- ${r.memory}`)
+ .join("\n");
+ }
+
+ const totalCount = longTermResults.length + uniqueSessionResults.length;
+ api.logger.info(
+ `openclaw-mem0: injecting ${totalCount} memories into context (${longTermResults.length} long-term, ${uniqueSessionResults.length} session)`,
+ );
+
+ return {
+ systemContext: `\nThe following memories may be relevant to this conversation:\n${memoryContext}\n`,
+ };
+ } catch (err) {
+ api.logger.warn(`openclaw-mem0: recall failed: ${String(err)}`);
+ }
+ });
+ }
+
+ // Auto-capture: store conversation context after agent ends
+ if (cfg.autoCapture) {
+ api.on("agent_end", async (event, ctx) => {
+ if (!event.success || !event.messages || event.messages.length === 0) {
+ return;
+ }
+
+ // Track session ID
+ const sessionId = (ctx as any)?.sessionKey ?? undefined;
+ if (sessionId) currentSessionId = sessionId;
+
+ try {
+ // Extract messages, limiting to last 10
+ const recentMessages = event.messages.slice(-10);
+ const formattedMessages: Array<{
+ role: string;
+ content: string;
+ }> = [];
+
+ for (const msg of recentMessages) {
+ if (!msg || typeof msg !== "object") continue;
+ const msgObj = msg as Record;
+
+ const role = msgObj.role;
+ if (role !== "user" && role !== "assistant") continue;
+
+ let textContent = "";
+ const content = msgObj.content;
+
+ if (typeof content === "string") {
+ textContent = content;
+ } else if (Array.isArray(content)) {
+ for (const block of content) {
+ if (
+ block &&
+ typeof block === "object" &&
+ "type" in block &&
+ (block as Record).type === "text" &&
+ "text" in block &&
+ typeof (block as Record).text === "string"
+ ) {
+ textContent +=
+ (textContent ? "\n" : "") +
+ ((block as Record).text as string);
+ }
+ }
+ }
+
+ if (!textContent) continue;
+ // Skip injected memory context
+ if (textContent.includes("")) continue;
+
+ formattedMessages.push({
+ role: role as string,
+ content: textContent,
+ });
+ }
+
+ if (formattedMessages.length === 0) return;
+
+ const addOpts = buildAddOptions(undefined, currentSessionId);
+ const result = await provider.add(
+ formattedMessages,
+ addOpts,
+ );
+
+ const capturedCount = result.results?.length ?? 0;
+ if (capturedCount > 0) {
+ api.logger.info(
+ `openclaw-mem0: auto-captured ${capturedCount} memories`,
+ );
+ }
+ } catch (err) {
+ api.logger.warn(`openclaw-mem0: capture failed: ${String(err)}`);
+ }
+ });
+ }
+
+ // ========================================================================
+ // Service
+ // ========================================================================
+
+ api.registerService({
+ id: "openclaw-mem0",
+ start: () => {
+ api.logger.info(
+ `openclaw-mem0: initialized (mode: ${cfg.mode}, user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
+ );
+ },
+ stop: () => {
+ api.logger.info("openclaw-mem0: stopped");
+ },
+ });
+ },
+};
+
+export default memoryPlugin;
diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json
new file mode 100644
index 000000000..75392b790
--- /dev/null
+++ b/openclaw/openclaw.plugin.json
@@ -0,0 +1,168 @@
+{
+ "id": "openclaw-mem0",
+ "kind": "memory",
+ "uiHints": {
+ "mode": {
+ "label": "Mode",
+ "help": "\"platform\" for Mem0 cloud, \"open-source\" for self-hosted"
+ },
+ "apiKey": {
+ "label": "Mem0 API Key",
+ "sensitive": true,
+ "placeholder": "m0-...",
+ "help": "API key from app.mem0.ai (or use ${MEM0_API_KEY}). Only needed for platform mode."
+ },
+ "userId": {
+ "label": "Default User ID",
+ "placeholder": "default",
+ "help": "User ID for scoping memories"
+ },
+ "orgId": {
+ "label": "Organization ID",
+ "placeholder": "org-...",
+ "advanced": true
+ },
+ "projectId": {
+ "label": "Project ID",
+ "placeholder": "proj-...",
+ "advanced": true
+ },
+ "autoCapture": {
+ "label": "Auto-Capture",
+ "help": "Automatically store conversation context after each agent turn"
+ },
+ "autoRecall": {
+ "label": "Auto-Recall",
+ "help": "Automatically inject relevant memories before each agent turn"
+ },
+ "customInstructions": {
+ "label": "Custom Instructions",
+ "placeholder": "Only store user preferences and important facts...",
+ "help": "Natural language rules for what Mem0 should store or exclude (platform mode)"
+ },
+ "customCategories": {
+ "label": "Custom Categories",
+ "advanced": true,
+ "help": "Map of category names to descriptions for memory tagging (platform mode only). Sensible defaults are built in."
+ },
+ "customPrompt": {
+ "label": "Custom Prompt (Open-Source)",
+ "advanced": true,
+ "help": "Custom prompt for open-source mode memory extraction."
+ },
+ "enableGraph": {
+ "label": "Enable Graph Memory",
+ "help": "Enable Mem0 graph memory for entity relationships (platform mode only)"
+ },
+ "searchThreshold": {
+ "label": "Search Threshold",
+ "placeholder": "0.5",
+ "help": "Minimum similarity score for search results (0-1). Default: 0.5"
+ },
+ "topK": {
+ "label": "Top K Results",
+ "placeholder": "5",
+ "help": "Maximum number of memories to retrieve"
+ },
+ "oss": {
+ "label": "Open-Source Configuration",
+ "advanced": true,
+ "help": "Optional. Configure custom embedder, vector store, LLM, or history DB for open-source mode. Has sensible defaults — only override what you need."
+ }
+ },
+ "configSchema": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "mode": {
+ "type": "string",
+ "enum": [
+ "platform",
+ "open-source",
+ "oss"
+ ]
+ },
+ "apiKey": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "string"
+ },
+ "orgId": {
+ "type": "string"
+ },
+ "projectId": {
+ "type": "string"
+ },
+ "autoCapture": {
+ "type": "boolean"
+ },
+ "autoRecall": {
+ "type": "boolean"
+ },
+ "customInstructions": {
+ "type": "string"
+ },
+ "customCategories": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "customPrompt": {
+ "type": "string"
+ },
+ "enableGraph": {
+ "type": "boolean"
+ },
+ "searchThreshold": {
+ "type": "number"
+ },
+ "topK": {
+ "type": "number"
+ },
+ "oss": {
+ "type": "object",
+ "properties": {
+ "embedder": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string"
+ },
+ "config": {
+ "type": "object"
+ }
+ }
+ },
+ "vectorStore": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string"
+ },
+ "config": {
+ "type": "object"
+ }
+ }
+ },
+ "llm": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string"
+ },
+ "config": {
+ "type": "object"
+ }
+ }
+ },
+ "historyDbPath": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "required": []
+ }
+}
\ No newline at end of file
diff --git a/openclaw/package.json b/openclaw/package.json
new file mode 100644
index 000000000..e2bdb87cc
--- /dev/null
+++ b/openclaw/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@mem0/openclaw-mem0",
+ "version": "0.1.0",
+ "type": "module",
+ "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
+ "license": "Apache-2.0",
+ "keywords": [
+ "openclaw",
+ "plugin",
+ "memory",
+ "mem0",
+ "long-term-memory"
+ ],
+ "dependencies": {
+ "@sinclair/typebox": "0.34.47",
+ "mem0ai": "^2.2.1"
+ },
+ "openclaw": {
+ "extensions": [
+ "./index.ts"
+ ]
+ }
+}
\ No newline at end of file