Compare commits

...

20 Commits

Author SHA1 Message Date
Saket Aryan 3882af7450 fix(cli): persistent anonymous telemetry ID + pass source=CLI in all API calls (#4789) 2026-04-11 21:00:05 +05:30
Saket Aryan d39ebad09f fix(openclaw): persistent anonymous telemetry ID, flush fix, and email resolution (#4790)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:57:14 +05:30
Gabriel Stein 9d82e2329d refactor(telemetry): sample OSS hot-path events at 10% to reduce PostHog volume (#4771) 2026-04-11 16:25:38 +05:30
szinvas 789cc9d607 docs - replace session_id with run_id (#4742) 2026-04-10 20:06:59 +05:30
Jared Diaz e59e3d5f0c Add deepseek.ts to src/llms with corresponding unit tests. Updates fa… (#4613)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-10 20:06:22 +05:30
Kartik d926f3697c fix(docs): eliminate ~222 SEO redirect chains on docs.mem0.ai (#4768) 2026-04-10 19:33:45 +05:30
Kartik c996b0e7fa docs: removing the changelog.mdx file adn replacing it with new changelog system (#4750) 2026-04-10 19:07:43 +05:30
Kartik 78ca85a260 refactor: update OpenClaw plugin config, hook logic, and documentation (#4764) 2026-04-09 17:36:05 +05:30
Kartik 88f696a60a refactor: drop orgId, projectId, enableGraph config options, update CLI prompts, and clean up related code (#4734) 2026-04-09 14:57:33 +05:30
Ignazio De Santis 081eca6d8f fix: guard temp_uuid_mapping lookups against LLM-hallucinated IDs (fixes #3931) (#4674)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-08 21:55:52 +05:30
Kartik 2434b9d550 docs: add ChatDev integration guide and update integrations list (#4751) 2026-04-08 20:16:04 +05:30
Rakhee Singh 3ffea554bc fix(azure_openai): forward response_format to Azure OpenAI API (#4689) 2026-04-08 19:22:02 +05:30
Rakhee Singh 1ad8a59b0c fix(deepseek): forward response_format to OpenAI-compatible API (#4688) 2026-04-08 19:21:11 +05:30
Saket Aryan a670333d67 feat: add AGENTS.md for AI coding agent instructions (#4726) 2026-04-06 21:32:43 +05:30
Saket Aryan 4c2db3e68b feat(skills): introduce Mem0 skill graph with dedicated CLI and Vercel AI SDK skills (#4725) 2026-04-06 20:41:29 +05:30
Saket Aryan 07f0d4f1e0 fix: use npx npm@latest for OIDC trusted publishing (#4724)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:22:00 +05:30
Saket Aryan 3565404eef fix: remove npm self-upgrade from CD workflows (#4723) 2026-04-06 17:11:28 +05:30
Kartik 144627c4ce fix(docs): change the position of the openclaw to agnet plugin and fix the integrations overview and sidebar list view (#4722) 2026-04-06 16:57:24 +05:30
Kartik 6984958138 chore: sat release (#4702) 2026-04-06 16:55:08 +05:30
Kartik b13748c446 feat: add import and event commands, refactor CLI, remove baseUrl config, update docs (#4704) 2026-04-06 16:54:27 +05:30
132 changed files with 8809 additions and 1310 deletions
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: mem0-ts/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+580
View File
@@ -0,0 +1,580 @@
# AGENTS.md
This file provides context for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, etc.) working with the Mem0 repository.
## Project Overview
**Mem0** ("mem-zero") is an intelligent memory layer for AI agents and assistants. It provides persistent, personalized memory via both a hosted platform API and self-hosted open-source SDKs.
- **Repository**: https://github.com/mem0ai/mem0
- **Documentation**: https://docs.mem0.ai
- **License**: Apache-2.0
## Repository Structure
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
### Key Directories
| Directory | Description |
|-----------|-------------|
| `mem0/` | Core Python SDK (`mem0ai` on PyPI) — memory, LLMs, embeddings, vector stores, graphs, rerankers |
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
| `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
| `skills/` | Claude Code skill definitions — `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/` |
| `docs/` | Documentation site (Mintlify) |
| `tests/` | Python SDK tests (pytest) |
| `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
| `examples/` | Sample projects — demo apps, Chrome extension, multi-agent patterns |
| `cookbooks/` | Jupyter notebooks — customer support chatbot, AutoGen integration |
| `embedchain/` | Legacy Embedchain RAG framework (maintained separately, Poetry-based) |
| `pr-reviews/` | Pull request review materials |
### Core Package Dependencies
```
mem0 (Python SDK) mem0-ts (TypeScript SDK)
├── mem0/memory/ ├── src/client/ (MemoryClient — hosted)
├── mem0/llms/ └── src/oss/ (Memory — self-hosted)
├── mem0/embeddings/ ├── src/llms/
├── mem0/vector_stores/ ├── src/embeddings/
├── mem0/graphs/ ├── src/vector_stores/
└── mem0/reranker/ └── src/graphs/
cli/python/ ──▶ mem0ai (optional, for OSS mode)
cli/node/ ──▶ mem0ai (npm, for API calls)
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
openclaw/ ──▶ mem0ai (npm)
```
## Development Setup
### Requirements
- **Python**: 3.9+ (3.10+ for CLI)
- **Node.js**: v18+ (v20 or v22 recommended)
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
- **Hatch**: Python build/environment tool (`pip install hatch`)
- **Docker**: Required for `server/` and `openmemory/` development
### Initial Setup
```bash
# Python SDK
hatch shell dev_py_3_11 # creates environment with all deps
pre-commit install # install git hooks
# TypeScript packages
cd mem0-ts && pnpm install # TS SDK
cd cli/node && pnpm install # Node CLI
cd vercel-ai-sdk && pnpm install # Vercel AI provider
cd openclaw && pnpm install # OpenClaw plugin
```
## Build, Lint, and Test Commands
### Python SDK (`mem0/`)
```bash
# Environment setup (uses Hatch)
hatch shell dev_py_3_11 # or dev_py_3_9, dev_py_3_10, dev_py_3_12
# Linting and formatting
make lint # ruff check
make format # ruff format
make sort # isort mem0/
# Tests
make test # pytest tests/
make test-py-3.9 # test specific Python version (3.9–3.12)
# Build and publish
make build # hatch build
make publish # hatch publish
```
- **Python:** 3.9, 3.10, 3.11, 3.12
- **Linter/formatter:** Ruff (line length **120**)
- **Import sorting:** isort (`profile = "black"`)
- **Test framework:** pytest (with pytest-mock, pytest-asyncio)
- **Pre-commit hooks:** ruff + isort — run `pre-commit install` before committing
### TypeScript SDK (`mem0-ts/`)
```bash
cd mem0-ts
pnpm install
pnpm run build # tsup
pnpm run test # jest (all tests)
pnpm run test:unit # jest --coverage (unit tests only)
pnpm run test:integration # jest (integration tests, needs MEM0_API_KEY)
pnpm run test:ci # jest --coverage --ci (CI mode)
pnpm run test:watch # jest watch mode
```
- **Node:** 20, 22 (CI-tested)
- **Build:** tsup (CJS + ESM)
- **Test:** jest
- **Formatter:** prettier
### Python CLI (`cli/python/`)
```bash
cd cli/python
pip install -e ".[dev]" # dev install with ruff + pytest
ruff check . # lint
ruff format . # format
pytest # test
hatch build # build
```
- **Python:** 3.10+ (not 3.9)
- **Linter/formatter:** Ruff (line length **100** — different from root SDK)
- **Ruff rules:** E, F, I, W, UP, B, SIM, RUF (ignores E501, B008 for Typer patterns, SIM108)
- **Framework:** Typer + Rich + httpx
- **Entry point:** `mem0 = "mem0_cli.app:main"`
- **Source layout:** `src/mem0_cli/`
- **Optional dependency:** `mem0ai` (for OSS mode, via `[oss]` extra)
### Node CLI (`cli/node/`)
```bash
cd cli/node
pnpm install
pnpm run build # tsup
pnpm run lint # biome check src/
pnpm run lint:fix # biome check --write src/
pnpm run typecheck # tsc --noEmit
pnpm run test # vitest run
pnpm run test:watch # vitest (watch mode)
pnpm run dev # tsx src/index.ts (development)
```
- **Node:** 18+ required
- **Build:** tsup (ESM)
- **Linter:** Biome (not ESLint, not Ruff)
- **Test:** vitest (not jest)
- **Framework:** Commander + Chalk + ora + cli-table3
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
```bash
cd vercel-ai-sdk
pnpm install
pnpm run build # tsup
pnpm run lint # eslint
pnpm run type-check # tsc --noEmit
pnpm run prettier-check # prettier --check
pnpm run test # jest
pnpm run test:edge # vitest (edge runtime)
pnpm run test:node # vitest (node runtime)
```
- **Build:** tsup (CJS + ESM)
- **Lint:** ESLint + Prettier
- **Test:** jest + vitest (edge/node configs)
### OpenClaw Plugin (`openclaw/`)
```bash
cd openclaw
pnpm install
pnpm run build # tsup
pnpm run test # vitest run
```
- **Build:** tsup (ESM)
- **Test:** vitest (with Codecov in CI)
- **Plugin manifest:** `openclaw.plugin.json`
### Server (`server/`)
```bash
# Docker production build
cd server
make build # docker build -t mem0-api-server .
make run_local # docker run -p 8000:8000 with .env
# Docker Compose development (FastAPI + PostgreSQL/pgvector + Neo4j)
cd server
docker-compose up # starts all 3 services
# mem0 API: localhost:8888
# PostgreSQL: localhost:8432
# Neo4j HTTP: localhost:8474, Bolt: localhost:8687
```
- **Framework:** FastAPI with uvicorn (auto-reload in dev)
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
### OpenMemory (`openmemory/`)
```bash
# Full stack via Docker Compose
cd openmemory
docker-compose up
# Qdrant: localhost:6333
# API (MCP): localhost:8765
# UI: localhost:3000
# Individual development
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
cd openmemory/ui && npm run dev # Next.js frontend
# Tests
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
```
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
- **Vector store:** Qdrant
### Documentation (`docs/`)
```bash
make docs # or: cd docs && mintlify dev
```
- **Framework:** Mintlify
- **API spec:** `docs/openapi.json`
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
### Evaluation (`evaluation/`)
```bash
cd evaluation
make run-mem0-add # Run mem0 add experiments
make run-mem0-search # Run mem0 search experiments
make run-mem0-plus-add # With graph memory
make run-mem0-plus-search # With graph memory
make run-rag # RAG baseline
make run-full-context # Full context baseline
make run-langmem # LangMem comparison
make run-openai # OpenAI comparison
```
## Core APIs
### Python
| Function / Class | Purpose | Import |
|-----------------|---------|--------|
| `Memory` | Self-hosted memory (sync) | `from mem0 import Memory` |
| `AsyncMemory` | Self-hosted memory (async) | `from mem0 import AsyncMemory` |
| `MemoryClient` | Hosted platform client (sync) | `from mem0 import MemoryClient` |
| `AsyncMemoryClient` | Hosted platform client (async) | `from mem0 import AsyncMemoryClient` |
**Key `Memory` / `MemoryClient` methods:**
| Method | Purpose |
|--------|---------|
| `add(messages, *, user_id, agent_id, run_id, metadata)` | Store a new memory |
| `search(query, *, user_id, agent_id, run_id, limit, filters)` | Search memories |
| `get(memory_id)` | Retrieve a single memory by ID |
| `get_all(*, user_id, agent_id, run_id, limit)` | List all memories |
| `update(memory_id, data)` | Update a memory |
| `delete(memory_id)` | Delete a memory |
| `delete_all(*, user_id, agent_id, run_id)` | Delete all memories |
| `history(memory_id)` | Get change history for a memory |
### TypeScript
| Export | Purpose | Import |
|--------|---------|--------|
| `MemoryClient` | Hosted platform client | `import { MemoryClient } from 'mem0ai'` |
| `Memory` | Self-hosted OSS memory | `import { Memory } from 'mem0ai/oss'` |
## Import Patterns
### Python
| What | Import |
|------|--------|
| Core memory classes | `from mem0 import Memory, AsyncMemory` |
| Platform client | `from mem0 import MemoryClient, AsyncMemoryClient` |
| Configuration | `from mem0.configs.base import MemoryConfig` |
| LLM providers | `from mem0.llms.<provider> import <ProviderLLM>` |
| Embedding providers | `from mem0.embeddings.<provider> import <ProviderEmbedding>` |
| Vector store providers | `from mem0.vector_stores.<provider> import <ProviderVectorStore>` |
### TypeScript
| What | Import |
|------|--------|
| Hosted client | `import { MemoryClient } from 'mem0ai'` |
| OSS memory | `import { Memory } from 'mem0ai/oss'` |
| Specific providers (OSS) | `import { OpenAIEmbedding } from 'mem0ai/oss'` |
## Coding Standards
### File Naming Conventions
- **Python source files:** `snake_case.py` (e.g., `azure_openai.py`, `cohere_reranker.py`)
- **Python test files:** `test_<module>.py` (e.g., `test_memory.py`, `test_main.py`)
- **TypeScript source files:** `snake_case.ts` (e.g., `azure_ai_search.ts`)
- **TypeScript test files:** `<module>.test.ts` (e.g., `memory.test.ts`)
- **Config/manifest files:** `kebab-case` (e.g., `openclaw.plugin.json`, `jest.config.js`)
### Python Conventions
- **Provider pattern:** All providers (LLMs, embeddings, vector stores, graphs, rerankers) inherit from a `base.py` abstract class in their directory. Config classes live in `configs.py`.
- **Pydantic v2** for all data models and configuration.
- **Ruff** is the single linting and formatting tool — no black, no flake8.
- Root SDK: line length **120**
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
- **isort** with `profile = "black"` for import sorting.
- Ruff excludes `embedchain/` and `openmemory/` from root config.
### TypeScript Conventions
- **Build:** tsup across all packages.
- **Package manager:** pnpm everywhere (no npm, no yarn).
- **TypeScript strict mode** across all packages.
- **Linting varies by package:**
| Package | Linter | Formatter | Test Framework |
|---------|--------|-----------|---------------|
| `mem0-ts/` | — | Prettier | jest |
| `cli/node/` | Biome | Biome | vitest |
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
| `openclaw/` | — | — | vitest |
### Type Checking
Always run type checking after modifying TypeScript code:
```bash
cd <package> && pnpm run typecheck # or: tsc --noEmit
```
## Architecture
### Provider Pattern
The SDK uses a consistent plugin architecture across 5 categories. Each category has a `base.py` abstract class and concrete provider implementations:
| Category | Count | Examples |
|----------|-------|---------|
| **LLMs** | 24 | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Gemini, Groq, Ollama, Together, DeepSeek, vLLM, LiteLLM, LM Studio, xAI |
| **Vector Stores** | 30 | Qdrant, Pinecone, Chroma, Weaviate, Milvus, MongoDB, Redis, Elasticsearch, pgvector, Supabase, Faiss, S3 Vectors |
| **Embeddings** | 15 | OpenAI, Azure OpenAI, Gemini, HuggingFace, FastEmbed, Together, AWS Bedrock, Ollama, Vertex AI |
| **Graph Stores** | 4 | Neo4j, Memgraph, Kuzu, Apache AGE |
| **Rerankers** | 5 | Cohere, HuggingFace, LLM-based, Sentence Transformer, Zero Entropy |
### Two Usage Modes
Self-hosted `Memory` / `AsyncMemory` classes and hosted-platform `MemoryClient` — both in Python and TypeScript.
### Graph Memory
Optional layer on top of vector memory for relationship-aware retrieval. Configured via the `graph` section of `MemoryConfig`.
### MCP Integration
Model Context Protocol support in multiple places:
- **Remote:** MCP server at `mcp.mem0.ai`
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
- **Plugin:** MCP tools in `mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
### Plugin & Skills System
- `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
- `skills/` contains structured skill definitions for AI agents, covering SDK usage, CLI workflows, and Vercel AI SDK patterns.
### Adding a New Provider
To add a new LLM, embedding, vector store, or reranker provider:
1. Create `mem0/<category>/<provider_name>.py`
2. Inherit from the abstract base class in `mem0/<category>/base.py`
3. Add configuration to `mem0/<category>/configs.py` (if the category uses one)
4. Register the provider in `mem0/<category>/__init__.py`
5. Add tests in `tests/<category>/<provider_name>/`
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
## CI/CD
### CI Workflows (automated testing)
| Workflow | File | Triggers | Tests |
|----------|------|----------|-------|
| Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
| Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| Embedchain | `ci.yml` (shared) | PRs on `embedchain/` | Ruff + pytest + coverage on Python 3.9–3.12 |
### CD Workflows (automated publishing)
| Workflow | File | Tag Prefix | Target |
|----------|------|------------|--------|
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
### Utility Workflows
| Workflow | File | Purpose |
|----------|------|---------|
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
## Task Completion Guidelines
These guidelines outline typical artifacts for different task types. Use judgment to adapt based on scope and context.
### Bug Fixes
1. **Unit tests**: Add tests that would fail without the fix (regression tests)
2. **Implementation**: Fix the bug
3. **Manual verification**: Run the relevant test suite to confirm the fix
4. **Lint**: Run the appropriate linter for the package you modified
### New Features
1. **Implementation**: Build the feature following existing patterns
2. **Unit tests**: Comprehensive test coverage for new functionality
3. **Documentation**: Update relevant docs in `docs/` for public APIs
4. **Examples**: Add usage examples if the feature introduces new user-facing behavior
### New Provider (LLM / Embedding / Vector Store / Reranker)
1. **Implementation**: Follow the "Adding a New Provider" steps above
2. **Tests**: Add unit tests matching the pattern of existing providers
3. **Configuration**: Add to the appropriate `configs.py` and `__init__.py`
4. **Dependencies**: Add to the correct optional group in `pyproject.toml`
5. **Documentation**: Add an integration guide in `docs/integrations/`
### Refactoring / Internal Changes
- Unit tests for any changed behavior
- No documentation needed for internal-only changes
- Ensure all existing tests still pass
### When to Deviate
These are guidelines, not rigid rules. Adjust based on:
- **Scope**: Trivial fixes (typos, comments) may not need tests
- **Visibility**: Internal changes may not need documentation
- **Context**: Some changes span multiple categories — use judgment
When uncertain about expected artifacts, ask for clarification.
## Contributing Guidelines
### Workflow
1. Fork and clone the repository.
2. Create a feature branch from `main` (e.g., `feature/my-new-feature`).
3. Make your changes — add tests, docs, and examples as appropriate.
4. Run linting and tests for every package you modified (see commands above).
5. Run `pre-commit install` on first setup — hooks run ruff + isort automatically.
6. Commit with a clear message following [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`, `docs:`, `refactor:`).
7. Push and open a Pull Request against `main`.
### Pull Request Requirements
Every PR must follow the repo's PR template (`.github/PULL_REQUEST_TEMPLATE.md`):
1. **Linked Issue** — Reference the issue with `Closes #<number>`. If no issue exists, create one first or explain why in the description.
2. **Description** — Explain what the PR does and why it's needed.
3. **Type of Change** — Check the appropriate box:
- Bug fix / New feature / Breaking change / Refactor / Documentation update
4. **Breaking Changes** — If applicable, describe what breaks and the migration path.
5. **Test Coverage** — Check what applies:
- Added/updated unit tests
- Added/updated integration tests
- Tested manually (describe how)
- No tests needed (explain why)
6. **Checklist** — All must be checked before merge:
- [ ] Code follows the project's style guidelines
- [ ] Self-review performed
- [ ] Tests added that prove the fix/feature works
- [ ] New and existing tests pass locally
- [ ] Documentation updated if needed
### PR Description Template
```markdown
## Linked Issue
Closes #<!-- issue number -->
## Description
<!-- What does this PR do? Why is it needed? -->
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Refactor (no functional changes)
- [ ] Documentation update
## Breaking Changes
N/A
## Test Coverage
- [ ] I added/updated unit tests
- [ ] I added/updated integration tests
- [ ] I tested manually (describe below)
- [ ] No tests needed (explain why)
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have added tests that prove my fix/feature works
- [ ] New and existing tests pass locally
- [ ] I have updated documentation if needed
```
### General Rules
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
- For `server/` and `openmemory/` work, use Docker Compose for local development.
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
### Contributing Guides
| Task | Guide |
|------|-------|
| Code contributions | `docs/contributing/development.mdx` |
| Documentation contributions | `docs/contributing/documentation.mdx` |
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
| Bug reports | `.github/ISSUE_TEMPLATE/bug_report.yml` |
| Feature requests | `.github/ISSUE_TEMPLATE/feature_request.yml` |
| Documentation issues | `.github/ISSUE_TEMPLATE/documentation_issue.yml` |
## Do NOT
- Modify CI/CD workflows without explicit approval.
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
- Commit `.env` files, API keys, or credentials.
- Modify `embedchain/` unless specifically working on that package — it has its own build system (Poetry).
- Skip pre-commit hooks.
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
- Change public APIs without updating documentation in `docs/`.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/cli",
"version": "0.2.2",
"version": "0.2.3",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
+12 -9
View File
@@ -116,6 +116,7 @@ export class PlatformBackend implements Backend {
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
payload.source = "CLI";
return (await this._request("POST", "/v1/memories/", {
json: payload,
@@ -176,6 +177,7 @@ export class PlatformBackend implements Backend {
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
payload.source = "CLI";
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
@@ -186,10 +188,9 @@ export class PlatformBackend implements Backend {
}
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
string,
unknown
>;
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
params: { source: "CLI" },
})) as Record<string, unknown>;
}
async listMemories(
@@ -227,6 +228,7 @@ export class PlatformBackend implements Backend {
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
payload.source = "CLI";
const result = (await this._request("POST", "/v2/memories/", {
json: payload,
@@ -245,6 +247,7 @@ export class PlatformBackend implements Backend {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
payload.source = "CLI";
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
@@ -255,7 +258,7 @@ export class PlatformBackend implements Backend {
opts: DeleteOptions = {},
): Promise<Record<string, unknown>> {
if (opts.all) {
const params: Record<string, string> = {};
const params: Record<string, string> = { source: "CLI" };
if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId;
@@ -265,10 +268,9 @@ export class PlatformBackend implements Backend {
})) as Record<string, unknown>;
}
if (memoryId) {
return (await this._request(
"DELETE",
`/v1/memories/${memoryId}/`,
)) as Record<string, unknown>;
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
params: { source: "CLI" },
})) as Record<string, unknown>;
}
throw new Error("Either memoryId or --all is required");
}
@@ -291,6 +293,7 @@ export class PlatformBackend implements Backend {
result = (await this._request(
"DELETE",
`/v2/entities/${entityType}/${entityId}/`,
{ params: { source: "CLI" } },
)) as Record<string, unknown>;
}
return result;
+14
View File
@@ -31,10 +31,15 @@ export interface DefaultsConfig {
enableGraph: boolean;
}
export interface TelemetryConfig {
anonymousId: string;
}
export interface Mem0Config {
version: number;
defaults: DefaultsConfig;
platform: PlatformConfig;
telemetry: TelemetryConfig;
}
export function createDefaultConfig(): Mem0Config {
@@ -52,6 +57,9 @@ export function createDefaultConfig(): Mem0Config {
baseUrl: DEFAULT_BASE_URL,
userEmail: "",
},
telemetry: {
anonymousId: "",
},
};
}
@@ -80,6 +88,9 @@ export function loadConfig(): Mem0Config {
config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false;
const telemetry = data.telemetry ?? {};
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
}
// Environment variable overrides
@@ -119,6 +130,9 @@ export function saveConfig(config: Mem0Config): void {
base_url: config.platform.baseUrl,
user_email: config.platform.userEmail,
},
telemetry: {
anonymous_id: config.telemetry.anonymousId,
},
};
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
+52 -5
View File
@@ -9,10 +9,10 @@
*/
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { CONFIG_FILE, loadConfig } from "./config.js";
import { CONFIG_FILE, loadConfig, saveConfig } from "./config.js";
import { CLI_VERSION } from "./version.js";
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
@@ -29,11 +29,34 @@ function isTelemetryEnabled(): boolean {
}
}
/**
* Return a persistent per-machine anonymous ID, generating one if needed.
*
* Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
* repeat runs on the same machine share one PostHog identity instead of
* collapsing into a single shared fallback string.
*/
function getOrCreateAnonymousId(): string {
const config = loadConfig();
if (config.telemetry.anonymousId) {
return config.telemetry.anonymousId;
}
const newId = `cli-anon-${randomUUID().replace(/-/g, "")}`;
config.telemetry.anonymousId = newId;
try {
saveConfig(config);
} catch {
/* ignore persistence failure — still return the generated ID */
}
return newId;
}
/**
* Return a stable anonymous identifier for the current user.
*
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
* Matches the SDK pattern in mem0-ts/src/client/mem0.ts.
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
* persistent per-machine anonymous ID.
*/
function getDistinctId(): string {
try {
@@ -47,7 +70,11 @@ function getDistinctId(): string {
} catch {
/* ignore */
}
return "anonymous-cli";
try {
return getOrCreateAnonymousId();
} catch {
return `cli-anon-${randomUUID().replace(/-/g, "")}`;
}
}
/**
@@ -69,6 +96,25 @@ export function captureEvent(
const config = loadConfig();
const distinctId = preResolvedEmail || getDistinctId();
// Detect anonymous → identified transition. If a stored anonymous_id
// exists and we just resolved to a real identity, fire a one-shot
// $identify event so PostHog stitches the pre-signup history onto
// the authenticated profile. Clear the stored id so we don't re-alias.
let anonIdToAlias: string | null = null;
if (
distinctId &&
!distinctId.startsWith("cli-anon-") &&
config.telemetry.anonymousId
) {
anonIdToAlias = config.telemetry.anonymousId;
config.telemetry.anonymousId = "";
try {
saveConfig(config);
} catch {
/* ignore — alias may double-fire next run, harmless */
}
}
const payload = {
api_key: POSTHOG_API_KEY,
distinct_id: distinctId,
@@ -92,6 +138,7 @@ export function captureEvent(
mem0ApiKey: config.platform.apiKey || "",
mem0BaseUrl: config.platform.baseUrl || "https://api.mem0.ai",
configPath: CONFIG_FILE,
anonDistinctIdToAlias: anonIdToAlias,
};
const child = spawn(
+21
View File
@@ -94,6 +94,19 @@ async function sendPosthogEvent(posthogHost, payload) {
}
}
async function sendIdentifyEvent(ctx, payload, anonId) {
const identifyPayload = {
api_key: payload.api_key,
event: "$identify",
distinct_id: payload.distinct_id,
properties: {
$anon_distinct_id: anonId,
$lib: (payload.properties && payload.properties.$lib) || "posthog-node",
},
};
await sendPosthogEvent(ctx.posthogHost, identifyPayload);
}
async function main() {
const ctx = JSON.parse(process.argv[2]);
const payload = ctx.payload;
@@ -102,6 +115,14 @@ async function main() {
await resolveAndCacheEmail(ctx, payload);
}
// Fire $identify *after* email resolution so PostHog links the stored
// anonymous id directly to the final identity (email, not the api-key
// hash). The regular event is sent next so it lands under the merged
// profile.
if (ctx.anonDistinctIdToAlias) {
await sendIdentifyEvent(ctx, payload, ctx.anonDistinctIdToAlias);
}
await sendPosthogEvent(ctx.posthogHost, payload);
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.2.2"
version = "0.2.3"
description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md"
license = "Apache-2.0"
+1 -1
View File
@@ -1,3 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.2.2"
__version__ = "0.2.3"
+10 -4
View File
@@ -93,6 +93,7 @@ class PlatformBackend(Backend):
payload["categories"] = categories
if enable_graph:
payload["enable_graph"] = True
payload["source"] = "CLI"
return self._request("POST", "/v1/memories/", json=payload)
@@ -172,6 +173,7 @@ class PlatformBackend(Backend):
payload["fields"] = fields
if enable_graph:
payload["enable_graph"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v2/memories/search/", json=payload)
return (
@@ -181,7 +183,7 @@ class PlatformBackend(Backend):
)
def get(self, memory_id: str) -> dict:
return self._request("GET", f"/v1/memories/{memory_id}/")
return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
def list_memories(
self,
@@ -220,6 +222,7 @@ class PlatformBackend(Backend):
payload["filters"] = api_filters
if enable_graph:
payload["enable_graph"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v2/memories/", json=payload, params=params)
return (
@@ -236,6 +239,7 @@ class PlatformBackend(Backend):
payload["text"] = content
if metadata:
payload["metadata"] = metadata
payload["source"] = "CLI"
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
def delete(
@@ -249,7 +253,7 @@ class PlatformBackend(Backend):
run_id: str | None = None,
) -> dict:
if all:
params: dict[str, str] = {}
params: dict[str, str] = {"source": "CLI"}
if user_id:
params["user_id"] = user_id
if agent_id:
@@ -260,7 +264,7 @@ class PlatformBackend(Backend):
params["run_id"] = run_id
return self._request("DELETE", "/v1/memories/", params=params)
elif memory_id:
return self._request("DELETE", f"/v1/memories/{memory_id}/")
return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
else:
raise ValueError("Either memory_id or --all is required")
@@ -285,7 +289,9 @@ class PlatformBackend(Backend):
# Delete each provided entity via the v2 path-based endpoint
result: dict = {}
for entity_type, entity_id in entities.items():
result = self._request("DELETE", f"/v2/entities/{entity_type}/{entity_id}/")
result = self._request(
"DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
)
return result
def ping(self, timeout: float | None = None) -> dict:
+12
View File
@@ -39,11 +39,17 @@ class DefaultsConfig:
enable_graph: bool = False
@dataclass
class TelemetryConfig:
anonymous_id: str = ""
@dataclass
class Mem0Config:
version: int = CONFIG_VERSION
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
platform: PlatformConfig = field(default_factory=PlatformConfig)
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
SHORT_KEY_ALIASES: dict[str, str] = {
@@ -87,6 +93,9 @@ def load_config() -> Mem0Config:
config.defaults.run_id = defaults.get("run_id", "")
config.defaults.enable_graph = defaults.get("enable_graph", False)
telemetry = data.get("telemetry", {})
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
# Environment variable overrides
env_key = os.environ.get("MEM0_API_KEY")
if env_key:
@@ -137,6 +146,9 @@ def save_config(config: Mem0Config) -> None:
"base_url": config.platform.base_url,
"user_email": config.platform.user_email,
},
"telemetry": {
"anonymous_id": config.telemetry.anonymous_id,
},
}
with open(CONFIG_FILE, "w") as f:
+45 -4
View File
@@ -9,12 +9,14 @@ Disable with: MEM0_TELEMETRY=false
from __future__ import annotations
import contextlib
import hashlib
import json
import os
import platform
import subprocess
import sys
import uuid
from typing import Any
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
@@ -26,11 +28,31 @@ def _is_telemetry_enabled() -> bool:
return val not in ("false", "0", "no")
def _get_or_create_anonymous_id() -> str:
"""Return a persistent per-machine anonymous ID, generating one if needed.
Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
repeat runs on the same machine share one PostHog identity instead of
collapsing into a single shared fallback string.
"""
from mem0_cli.config import load_config, save_config
config = load_config()
if config.telemetry.anonymous_id:
return config.telemetry.anonymous_id
new_id = f"cli-anon-{uuid.uuid4().hex}"
config.telemetry.anonymous_id = new_id
with contextlib.suppress(Exception):
save_config(config)
return new_id
def _get_distinct_id() -> str:
"""Return a stable anonymous identifier for the current user.
Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
Matches the SDK pattern in mem0/client/main.py.
Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
persistent per-machine anonymous ID.
"""
try:
from mem0_cli.config import load_config
@@ -42,7 +64,10 @@ def _get_distinct_id() -> str:
return hashlib.md5(config.platform.api_key.encode()).hexdigest()
except Exception:
pass
return "anonymous-cli"
try:
return _get_or_create_anonymous_id()
except Exception:
return f"cli-anon-{uuid.uuid4().hex}"
def capture_event(
@@ -61,12 +86,27 @@ def capture_event(
try:
from mem0_cli import __version__
from mem0_cli.config import CONFIG_FILE, load_config
from mem0_cli.config import CONFIG_FILE, load_config, save_config
from mem0_cli.state import is_agent_mode
config = load_config()
distinct_id = pre_resolved_email or _get_distinct_id()
# Detect anonymous → identified transition. If a stored anonymous_id
# exists and we just resolved to a real identity, fire a one-shot
# $identify event so PostHog stitches the pre-signup history onto
# the authenticated profile. Clear the stored id so we don't re-alias.
anon_id_to_alias: str | None = None
if (
distinct_id
and not distinct_id.startswith("cli-anon-")
and config.telemetry.anonymous_id
):
anon_id_to_alias = config.telemetry.anonymous_id
config.telemetry.anonymous_id = ""
with contextlib.suppress(Exception):
save_config(config)
payload = {
"api_key": POSTHOG_API_KEY,
"distinct_id": distinct_id,
@@ -92,6 +132,7 @@ def capture_event(
"mem0_api_key": config.platform.api_key or "",
"mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
"config_path": str(CONFIG_FILE),
"anon_distinct_id_to_alias": anon_id_to_alias,
}
subprocess.Popen(
@@ -27,9 +27,31 @@ def main() -> None:
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
_resolve_and_cache_email(ctx, payload)
# Fire $identify *after* email resolution so PostHog links the stored
# anonymous id directly to the final identity (email, not the api-key
# hash). The regular event is sent next so it lands under the merged
# profile.
anon_id = ctx.get("anon_distinct_id_to_alias")
if anon_id:
_send_identify_event(ctx, payload, anon_id)
_send_posthog_event(ctx["posthog_host"], payload)
def _send_identify_event(ctx: dict, payload: dict, anon_id: str) -> None:
"""Send a PostHog $identify event aliasing anon_id → payload['distinct_id']."""
identify_payload = {
"api_key": payload["api_key"],
"event": "$identify",
"distinct_id": payload["distinct_id"],
"properties": {
"$anon_distinct_id": anon_id,
"$lib": payload.get("properties", {}).get("$lib", "posthog-python"),
},
}
_send_posthog_event(ctx["posthog_host"], identify_payload)
def _resolve_and_cache_email(ctx: dict, payload: dict) -> None:
"""Call /v1/ping/ to get the user's email, update the payload, and cache it."""
try:
+2 -2
View File
@@ -10,7 +10,7 @@ description: "REST APIs for memory management, search, and entity operations"
Mem0 provides a comprehensive REST API for integrating advanced memory capabilities into your applications. Create, search, update, and manage memories across users, agents, and custom entities with simple HTTP requests.
<Info>
**Quick start:** Get your API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys) and make your first memory operation in minutes.
**Quick start:** Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a> and make your first memory operation in minutes.
</Info>
---
@@ -87,7 +87,7 @@ All API requests require authentication using Token-based authentication. Includ
Authorization: Token <your-api-key>
```
Get your API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys).
Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a>.
<Warning>
**Keep your API key secure.** Never expose it in client-side code or public repositories. Use environment variables and server-side requests only.
+82
View File
@@ -0,0 +1,82 @@
---
title: "Highlights"
description: "Major product launches, headline features, and milestones for Mem0."
mode: "wide"
---
<Update label="2026-04-06" description="Mem0 Skill Graph">
**Mem0 Skill Graph — In-Context Documentation for AI Agents**
AI coding agents in Claude Code, Cursor, and Codex can now access Mem0 knowledge directly in their workflow — no doc searching required. Three interconnected skills launched:
- **mem0 Core Skill** — Complete Python and TypeScript SDK reference, REST API patterns, and integration guides for LangChain, CrewAI, Autogen, and more
- **mem0-cli Skill** — Terminal command reference, configuration walkthroughs, and CI/CD recipes
- **mem0-vercel-ai-sdk Skill** — Vercel AI SDK provider API, memory-augmented generation patterns, and multi-provider setup
</Update>
<Update label="2026-04-06" description="Mem0 CLI v0.2.2">
**Official Mem0 CLI — Now on PyPI and npm**
A full-featured command-line interface for Mem0, available in both Python and Node.js:
- **Install:** `pip install mem0-cli` or `npm install -g @mem0/cli`
- **Full command suite** — `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`, `event`
- **Interactive setup** — `mem0 init` with email verification or direct API key entry
- **Works everywhere** — Platform (Mem0 Cloud) and self-hosted OSS modes
- **Scriptable** — `--json` flag for CI/CD pipelines and automation
- **Dual SDK** — Same commands, same experience across Python and Node.js
</Update>
<Update label="2026-04-04" description="OpenClaw v1.0.4">
**OpenClaw Plugin — Production-Ready**
The OpenClaw Mem0 plugin went from initial release to production-ready in one week (v1.0.0 → v1.0.4):
- **Skills-based memory architecture** — New extraction pipeline with skill-loader, batched extraction, and domain-aware memory triage
- **Dream gate** — Automatic memory consolidation during idle periods for higher-quality long-term recall
- **Interactive CLI** — `openclaw mem0 init`, `status`, `config`, `import`, and `event` commands
- **Unified tool naming** — `memory_add` and `memory_delete` replace 4 legacy tools, matching the platform API
- **Security hardened** — Path traversal protection, pinned dependencies, 329 tests across 10 files
</Update>
<Update label="2026-04-02" description="Mem0 Plugin for AI Editors">
**Mem0 Plugin for Claude Code, Cursor, and Codex**
Launched a unified Mem0 plugin across three major AI development environments — Claude Code and Cursor first (March 25), then Codex (April 2):
- **9 MCP memory tools** — add, search, get, update, delete, bulk delete, entity management via `mcp.mem0.ai`
- **Lifecycle hooks** — Automatic memory capture at session start, context compaction, task completion, and session end
- **Cloud MCP server** — Managed endpoint replaces local MCP and Smithery setup
- **Streamable HTTP transport** — New MCP transport protocol for real-time streaming
- **Codex-specific skill** — Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
</Update>
<Update label="2026-03-21" description="New Providers">
**Apache AGE, Turbopuffer, MiniMax, and pgvector for Node.js**
Major expansion of the provider ecosystem:
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
- **Turbopuffer** — New vector database provider for Python SDK
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
- **Reasoning models** — `reasoning_effort` parameter for OpenAI o1/o3-style models
</Update>
<Update label="2026-03-14" description="Mem0 Platform Skill">
**Mem0 Platform Skill on skills.sh**
First skill launch — a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
</Update>
+199
View File
@@ -0,0 +1,199 @@
---
title: "OpenClaw"
description: "Release notes for the OpenClaw plugin and agent harness."
mode: "wide"
---
<Update label="2026-04-11" description="v1.0.6">
**Bug Fixes:**
- **Telemetry:** Replaced shared `"anonymous-openclaw"` fallback with a persistent per-machine random hash (`openclaw-anon-<uuid>`), so anonymous plugin users are counted individually in PostHog ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Added PostHog `$identify` event on first authenticated run to stitch anonymous history onto the authenticated profile ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Fixed event loss on short-lived CLI invocations — added `beforeExit` handler to flush queued events before the process exits ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Added lazy `/v1/ping/` email resolution so users who configure API key outside `mem0 init` show as their email in PostHog, not an md5 hash ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Unified CLI event prefix from `openclaw.<cmd>` to `openclaw.cli.<cmd>` on the needsSetup branch to match the authenticated branch ([#4790](https://github.com/mem0ai/mem0/pull/4790))
**Improvements:**
- **API:** Added `source: "OPENCLAW"` to all provider calls (`add`, `search`, `getAll`) across tools, CLI commands, recall, and the OSS backend adapter ([#4790](https://github.com/mem0ai/mem0/pull/4790))
</Update>
<Update label="2026-04-07" description="v1.0.5">
**Bug Fixes:**
- **Init interactive choice bug**: Fixed number selection in `openclaw mem0 init` — entering 1/2/3 now correctly selects the corresponding option (was broken by readline prefill concatenating with user input)
- **OSS pgvector crash** ([#4727](https://github.com/mem0ai/mem0/issues/4727)): Fixed "Client has already been connected" cascade when using pgvector in OSS mode. The warmup call swallowed errors leaving a half-initialized pg client; concurrent recall/capture then all hit `client.connect()` on the same client. Fix: let warmup errors propagate (so `initPromise` resets and retries with a fresh Memory + fresh pg client) and build fresh config objects per attempt instead of mutating shared state.
**Removed:**
- **`orgId` / `projectId` config parameters**: Removed from config schema, CLI (`config show/get/set`), init display, and providers. The API key is project-scoped, so separate org/project IDs are unnecessary and could cause access errors if mismatched.
- **`enableGraph` config parameter**: Removed from all config surfaces, providers, backend, and tools. Graph memory is being deprecated — removing the flag avoids unnecessary exposure.
</Update>
<Update label="2026-04-04" description="v1.0.4">
**New Features:**
- **Interactive init flow**: `openclaw mem0 init` with interactive menu (email verification or direct API key). Non-interactive modes: `--api-key`, `--email`, `--email --code`
- **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
- **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status`, `openclaw mem0 config show`, `openclaw mem0 config set`
- **`import` CLI command**: Bulk-import memories from a JSON file with `--user-id` and `--agent-id` overrides
- **`event list` / `event status` CLI commands**: Monitor background processing events
- **`fs-safe.ts` module**: Isolated filesystem wrappers in a separate entry point
- **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands
- **Plugin manifest**: Added `contracts.tools`, `configSchema`, and `uiHints` to `openclaw.plugin.json`
- **Test suite**: 329 tests across 10 test files
**Changes:**
- **Modular architecture**: Extracted tools into `tools/` directory (6 files) and CLI into `cli/commands.ts`
- **Code splitting**: tsup builds with `splitting: true` and two entry points
- **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`)
- **Auto-recall timeout**: Recall wrapped in 8-second `Promise.race`
- **Auto-capture fire-and-forget**: `provider.add()` runs in background via `.then()/.catch()`
- **Auto-capture minimum content gate**: Skips extraction when total user content is fewer than 50 chars
**Removed:**
- `memory_store` tool — replaced by `memory_add`
- `memory_forget` tool — replaced by `memory_delete`
- `memory_delete_all` tool — merged into `memory_delete`
- `memory_history` tool and `history` CLI command — deprecated
</Update>
<Update label="2026-04-03" description="v1.0.3">
**Bug Fixes:**
- **Security**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal
- **Noise filter**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction`
**Changes:**
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
**Tests:**
- 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
</Update>
<Update label="2026-04-02" description="v1.0.2">
**Bug Fixes:**
- **Security**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — plugin-side env resolution was redundant and triggered static analysis warnings ([#4676](https://github.com/mem0ai/mem0/pull/4676))
</Update>
<Update label="2026-04-02" description="v1.0.1">
**New Features:**
- **CD workflow**: Added continuous deployment workflow with OIDC trusted publishing ([#4672](https://github.com/mem0ai/mem0/pull/4672))
- **Plugin configuration manifest**: Added `compat` and `build` metadata to `package.json` ([#4667](https://github.com/mem0ai/mem0/pull/4667))
- **LICENSE**: Added Apache-2.0 license file ([#4667](https://github.com/mem0ai/mem0/pull/4667))
**Bug Fixes:**
- **Dream gate**: Fixed cheap-first ordering, session isolation, and verified completion ([#4666](https://github.com/mem0ai/mem0/pull/4666))
- **Graceful startup**: Plugin now starts gracefully when no API key is configured ([#4669](https://github.com/mem0ai/mem0/pull/4669))
</Update>
<Update label="2026-04-01" description="v1.0.0">
**New Features:**
- **Skills-based memory architecture**: New skill-loader and skill-based extraction pipeline with batched extraction ([#4624](https://github.com/mem0ai/mem0/pull/4624))
- **Dream gate**: Memory consolidation and dream-cycle processing during idle periods
- **Enhanced recall**: New `recall.ts` module with improved recall logic and skill-aware retrieval
- **Memory triage skill**: Domain-aware memory triage with companion domain support and recall protocol
- **Memory dream skill**: Skill for memory consolidation during idle periods
- **Plugin configuration**: Added `openclaw.plugin.json` manifest and `scripts/configure.py` setup helper
**Changes:**
- Extraction pipeline refactored to use skills-based architecture for more contextual and higher quality memory capture
</Update>
<Update label="2026-03-26" description="v0.4.1">
**New Features:**
- **Improved extraction quality**: Enhanced noise filtering, deduplication, and better extraction instructions
**Bug Fixes:**
- **Credential detection**: Improved detection of credentials, API keys, and secrets in extraction instructions (#4552)
- **Standalone timestamps**: Prevented extraction of standalone timestamps as memories (#4550)
</Update>
<Update label="2026-03-16" description="v0.4.0">
**New Features:**
- **Non-interactive trigger filtering**: Skips recall and capture for `cron`, `heartbeat`, `automation`, and `schedule` triggers
- **Subagent hallucination prevention**: Detects ephemeral subagent sessions and routes recall to parent namespace
- **Dynamic recall thresholding**: Memories scoring less than 50% of top result are dropped
- **SQLite resilience**: Init error recovery with automatic retry for OSS mode
- **`disableHistory` config option**: New `oss.disableHistory` flag
- 78 unit tests covering filtering, isolation, trigger filtering, subagent detection, and SQLite resilience
**Changes:**
- Auto-recall threshold raised from 0.5 to 0.6 for stricter precision
- Recall candidate pool increased to `topK * 2` for better filtering headroom
- Relaxed extraction instructions: related facts kept together to preserve context
**Bug Fixes:**
- **Concurrent session race condition**: Lifecycle hooks now use `ctx.sessionKey` directly instead of a shared mutable variable
</Update>
<Update label="2026-03-12" description="v0.3.1">
**New Features:**
- **Message filtering pipeline**: Multi-stage noise removal before extraction
- **Broad recall for new sessions**: Short or new-session prompts trigger secondary broad search
- **Client-side threshold filtering**: Safety net that drops low-relevance results
- **Temporal anchoring**: Extraction instructions now include current date
- 55 unit tests covering filtering and isolation helpers
**Changes:**
- Extraction window expanded from last 10 to last 20 messages
- Rewritten custom extraction instructions for conciseness and deduplication
- Refactored monolithic `index.ts` (1772 lines) into 6 focused modules
</Update>
<Update label="2026-03-10" description="v0.3.0">
**Bug Fixes:**
- Updated `mem0ai` dependency with sqlite3 to better-sqlite3 migration (#4270)
</Update>
<Update label="2026-03-09" description="v0.2.0">
**New Features:**
- Per-agent memory isolation for multi-agent setups via `agentId`
- "Understanding userId" section in docs
**Changes:**
- Updated config examples to use concrete `userId` values instead of placeholders
**Bug Fixes:**
- Migrated platform search to Mem0 v2 API
</Update>
<Update label="2026-02-19" description="v0.1.2">
**New Features:**
- Source field for openclaw memory entries
**Bug Fixes:**
- Auto-recall injection and auto-capture message drop
</Update>
<Update label="2026-02-02" description="v0.1.0">
**New Features:**
- Initial release of the OpenClaw Mem0 plugin
- Platform mode (Mem0 Cloud) and open-source mode support
- Auto-recall: inject relevant memories before each turn
- Auto-capture: store facts after each turn
- Configurable `topK`, `threshold`, and `apiVersion` options
</Update>
+290
View File
@@ -0,0 +1,290 @@
---
title: "Platform"
description: "Release notes for the Mem0 hosted platform — backend, dashboard, billing, and infrastructure changes."
mode: "wide"
---
<Update label="2025-07-23" description="">
**Bug Fixes:**
- **Memory:** Fixed ADD functionality
</Update>
<Update label="2025-07-19" description="">
**New Features:**
- **UI:** Added Settings UI and latency display
- **Performance:** Neo4j query optimization
**Bug Fixes:**
- **OpenMemory:** Fixed OMM raising unnecessary exceptions
</Update>
<Update label="2025-07-18" description="">
**Improvements:**
- **UI:** Updated Event UI
- **Performance:** Fixed N+1 query issue in semantic_search_v2 by optimizing MemorySerializer field selection
**Bug Fixes:**
- **Memory:** Fixed duplicate memory index sentry error
</Update>
<Update label="2025-07-17" description="">
**New Features:**
- **UI:** New Settings Page
- **Memory:** Duplicate memories entities support
**Improvements:**
- **Performance:** Optimized semantic search and get_all APIs by eliminating N+1 queries
</Update>
<Update label="2025-07-16" description="">
**New Features:**
- **Database:** Implemented read replica routing with enhanced logging and app-specific DB routing
**Improvements:**
- **Performance:** Improved query performance in search v2 and get all v2 endpoints
**Bug Fixes:**
- **API:** Fixed pagination for get all API
</Update>
<Update label="2025-07-12" description="">
**Bug Fixes:**
- **Graph:** Fixed social graph bugs and connection issues
</Update>
<Update label="2025-07-11" description="">
**Improvements:**
- **Rate Limiting:** New rate limit for V2 Search
**Bug Fixes:**
- **Slack:** Fixed Slack rate limit error with backend improvements
</Update>
<Update label="2025-07-10" description="">
**Improvements:**
- **Performance:**
- Changed connection pooling time to 5 minutes
- Separated graph lambdas for better performance
</Update>
<Update label="2025-07-09" description="">
**Improvements:**
- **Graph:** Graph Optimizations V2 and memory improvements
</Update>
<Update label="2025-07-08" description="">
**New Features:**
- **Database:** Added read replica support for improved database performance
- **UI:** Implemented UI changes for Users Page
- **Feedback:** Enabled feedback functionality
**Bug Fixes:**
- **Serializer:** Fixed GET ALL Serializer
</Update>
<Update label="2025-07-05" description="">
**New Features:**
- **UI:** User Page Revamp and New Users Page
</Update>
<Update label="2025-07-04" description="">
**New Features:**
- **Users:** New Users Page implementation
- **Tools:** Added script to backfill memory categories
**Bug Fixes:**
- **Filters:** Fixed Filters Get All functionality
</Update>
<Update label="2025-07-03" description="">
**Improvements:**
- **Graph:** Graph Memory optimization
- **Memory:** Fixed exact memories and semantically similar memories retrieval
</Update>
<Update label="2025-07-02" description="">
**Improvements:**
- **Categorization:** Refactored categorization logic to utilize Gemini 2.5 Flash and improve message handling
</Update>
<Update label="2025-07-01" description="">
**Bug Fixes:**
- **Memory:** Fixed old_memory issue in Async memory addition lambda
- **Events:** Fixed missing events
</Update>
<Update label="2025-06-30" description="">
**Improvements:**
- **Graph:** Improvements to graph memory and added user to LTM-STM
</Update>
<Update label="2025-06-28" description="">
**New Features:**
- **Graph:** Added support for SQS in graph memory addition
- **Testing:** Added Locust load testing script and Grafana Dashboard
</Update>
<Update label="2025-06-27" description="">
**Improvements:**
- **Rate Limiting:** Updated rate limiting for ADD API to 1000/min
- **Performance:** Improved Neo4j performance
</Update>
<Update label="2025-06-26" description="">
**New Features:**
- **Memory:** Edit Memory From Drawer functionality
- **API:** Added Topic Suggestions API Endpoint
</Update>
<Update label="2025-06-25" description="">
**New Features:**
- **Group Chat:** Group-Chat v2 with Actor-Aware Memories
- **Memory:** Editable Metadata in Memories
- **UI:** Memory Actions Badges
</Update>
<Update label="2025-06-19" description="">
**New Features:**
- **Rate Limiting:** Implemented comprehensive rate limiting system
**Improvements:**
- **Performance:** Added performance indexes for memory stats query
**Bug Fixes:**
- **Search:** Fixed search events not respecting top-k parameter
</Update>
<Update label="2025-06-18" description="">
**New Features:**
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
- **Playground:** Added Claude 4 support on Playground
**Improvements:**
- **Memory:** Added ability to update memory metadata
</Update>
<Update label="2025-06-17" description="">
**New Features:**
- **UI:** New Memories Page UI design
</Update>
<Update label="2025-06-16" description="">
**Improvements:**
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
</Update>
<Update label="2025-06-13" description="">
**Improvements:**
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
</Update>
<Update label="2025-06-11" description="">
**New Features:**
- **OMM:** Added OMM Script and UI functionality
**Improvements:**
- **API:** Added filters validation to semantic_search_v2 endpoint
</Update>
<Update label="2025-06-09" description="">
**New Features:**
- **Intercom:** Set Intercom events for ADD and SEARCH operations
- **OpenMemory:** Added Posthog integration and feedback functionality
- **MCP:** New JavaScript MCP Server with feedback support
**Improvements:**
- **Structured Data:** Enhanced structured data handling in memory management
</Update>
<Update label="2025-06-06" description="">
**New Features:**
- **OAuth:** Added Mem0 OAuth integration
- **OMM:** Added OMM-Mem0 sync for deleted memories
</Update>
<Update label="2025-06-05" description="">
**New Features:**
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
</Update>
<Update label="2025-06-02" description="">
**New Features:**
- **OpenMemory Cloud:** Added OpenMemory Cloud support
- **Structured Data:** Added 'structured_attributes' field to Memory model
</Update>
<Update label="2025-05-30" description="">
**New Features:**
- **Projects:** Added version and enable_graph to project views
- **OpenMemory:** Added Postgres support for OpenMemory
</Update>
<Update label="2025-05-19" description="">
**Bug Fixes:**
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
</Update>
+104 -286
View File
@@ -1,13 +1,25 @@
---
title: "Product Updates"
description: "Latest releases, bug fixes, and improvements for the Mem0 Python and TypeScript SDKs."
title: "SDK & Tools"
description: "Release notes for the Mem0 Python SDK, TypeScript SDK, Vercel AI SDK, CLI, and editor plugins."
mode: "wide"
---
<Tabs>
<Tab title="Python">
<Update label="2026-04-04" description="v1.0.11">
**New Features & Updates:**
- **SDK:** Added `multilingual` parameter to project update ([#4314](https://github.com/mem0ai/mem0/pull/4314))
**Bug Fixes:**
- **LLMs:** Fixed Groq model configuration ([#4700](https://github.com/mem0ai/mem0/pull/4700))
- **Core:** Prevented thread and memory leaks from PostHog telemetry ([#4535](https://github.com/mem0ai/mem0/pull/4535))
- **Vector Stores:** Used `DatetimeRange` for datetime string values in Qdrant range filters ([#4659](https://github.com/mem0ai/mem0/pull/4659))
- **Configs:** Added missing `ConfigDict` to vector store configs (Elasticsearch, MongoDB, Neptune, OpenSearch, PGVector, Supabase, Valkey) ([#4656](https://github.com/mem0ai/mem0/pull/4656))
</Update>
<Update label="2026-04-01" description="v1.0.10">
**New Features & Updates:**
@@ -831,6 +843,12 @@ mode: "wide"
</Tab>
<Tab title="TypeScript">
<Update label="2026-04-04" description="v2.4.6">
**New Features & Updates:**
- **Client:** Added `multilingual` parameter to project update types ([#4314](https://github.com/mem0ai/mem0/pull/4314))
</Update>
<Update label="2026-04-01" description="v2.4.5">
@@ -1140,352 +1158,152 @@ mode: "wide"
</Tab>
<Tab title="Platform">
<Tab title="CLI">
<Update label="2025-07-23" description="">
<Update label="2026-04-11" description="Python v0.2.3 / Node v0.2.3">
**Bug Fixes:**
- **Memory:** Fixed ADD functionality
- **Telemetry:** Replaced shared `"anonymous-cli"` fallback with a persistent per-machine random hash (`cli-anon-<uuid>`), so anonymous CLI users are counted individually in PostHog instead of collapsing into one identity ([#4789](https://github.com/mem0ai/mem0/pull/4789))
- **Telemetry:** Added PostHog `$identify` event on first authenticated run to stitch pre-signup anonymous history onto the authenticated user profile ([#4789](https://github.com/mem0ai/mem0/pull/4789))
**Improvements:**
- **API:** All API calls now include `source=CLI` in request bodies (POST/PUT) and query params (GET/DELETE) for server-side attribution ([#4789](https://github.com/mem0ai/mem0/pull/4789))
</Update>
<Update label="2025-07-19" description="">
<Update label="2026-04-06" description="Python v0.2.2 / Node v0.2.2">
**New Features:**
- **UI:** Added Settings UI and latency display
- **Performance:** Neo4j query optimization
- **Telemetry:** Added PostHog telemetry and source tracking to both Python and Node CLIs ([#4699](https://github.com/mem0ai/mem0/pull/4699))
- **Validation:** API key validated upfront via `/v1/ping/` on startup — fail-fast with a helpful error instead of cryptic 401s ([#4701](https://github.com/mem0ai/mem0/pull/4701))
**Bug Fixes:**
- **OpenMemory:** Fixed OMM raising unnecessary exceptions
- **CD:** Fixed OIDC trusted publishing with `npx npm@latest` ([#4724](https://github.com/mem0ai/mem0/pull/4724))
- **CD:** Removed npm self-upgrade from CD workflows ([#4723](https://github.com/mem0ai/mem0/pull/4723))
</Update>
<Update label="2025-07-18" description="">
<Update label="2026-04-03" description="Python v0.2.1 / Node v0.2.1">
**Improvements:**
- **UI:** Updated Event UI
- **Performance:** Fixed N+1 query issue in semantic_search_v2 by optimizing MemorySerializer field selection
**New Features:**
- **Docs:** Comprehensive README with installation, usage examples, and purple branding ([#4680](https://github.com/mem0ai/mem0/pull/4680))
**Bug Fixes:**
- **Memory:** Fixed duplicate memory index sentry error
- **npm:** Added `repository` field to Node packages for npm provenance ([#4671](https://github.com/mem0ai/mem0/pull/4671))
- **CD:** Added CD workflows for Node SDK packages with OIDC trusted publishing ([#4670](https://github.com/mem0ai/mem0/pull/4670))
</Update>
<Update label="2025-07-17" description="">
<Update label="2026-04-02" description="Python v0.2.0 / Node v0.1.1">
**New Features:**
- **UI:** New Settings Page
- **Memory:** Duplicate memories entities support
**Improvements:**
- **Performance:** Optimized semantic search and get_all APIs by eliminating N+1 queries
</Update>
<Update label="2025-07-16" description="">
**New Features:**
- **Database:** Implemented read replica routing with enhanced logging and app-specific DB routing
**Improvements:**
- **Performance:** Improved query performance in search v2 and get all v2 endpoints
- **`event` commands:** `mem0 event list` shows recent background processing events in a table; `mem0 event status <id>` shows full detail including nested memory results ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`--json` / `--agent` flag:** Root-level flag switches all command output to a structured JSON envelope for programmatic/agent consumption. Envelope format: `{"status", "command", "duration_ms", "scope", "count", "data"}` ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **Agent output sanitization:** Raw API responses projected to only relevant fields per command (e.g., `add` → `{id, memory, event}`, `search` → `{id, memory, score, created_at, categories}`) ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **Email login:** Added email verification code login to `mem0 init` ([#4623](https://github.com/mem0ai/mem0/pull/4623))
- **Brand update:** Updated color palette from purple to golden ([#4664](https://github.com/mem0ai/mem0/pull/4664))
- **CI/CD:** Added CI pipelines and CD workflows for both CLIs ([#4640](https://github.com/mem0ai/mem0/pull/4640), [#4653](https://github.com/mem0ai/mem0/pull/4653))
**Bug Fixes:**
- **API:** Fixed pagination for get all API
</Update>
<Update label="2025-07-12" description="">
**Bug Fixes:**
- **Graph:** Fixed social graph bugs and connection issues
</Update>
<Update label="2025-07-11" description="">
- **Node:** Fixed critical `MODULE_NOT_FOUND` crash on `status`, `import`, and all commands when installed globally — replaced runtime `createRequire` with build-time version injection ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **Node:** API errors now show full response detail instead of bare "Bad Request" ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **Python:** Fixed double error printing on all commands ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **`status` command:** Replaced heavyweight `/v1/entities/` check with dedicated `GET /v1/ping/` endpoint ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`add` command:** Deduplicated PENDING results from API; changed misleading count message ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`init` command:** Partial flags now work in non-TTY; warns before overwriting existing config; added `--force` flag ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`delete` command:** Fixed entity delete via v2 API for all entity types ([#4649](https://github.com/mem0ai/mem0/pull/4649))
**Improvements:**
- **Rate Limiting:** New rate limit for V2 Search
**Bug Fixes:**
- **Slack:** Fixed Slack rate limit error with backend improvements
- Tables now show full UUIDs (was truncated to 8 chars, making `mem0 get <id>` fail) ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- Search table includes Score column ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- `config get api_key` short-form aliases added ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- Client-side validation for `--expires`, `--page-size`, `--page`, `--top-k`, `--threshold`, and empty content ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- `printInfo` / `printScope` moved to stderr to avoid contaminating JSON piping ([#4636](https://github.com/mem0ai/mem0/pull/4636))
</Update>
<Update label="2025-07-10" description="">
<Update label="2026-03-26" description="Python v0.1.0 / Node v0.1.0">
**Improvements:**
- **Performance:**
- Changed connection pooling time to 5 minutes
- Separated graph lambdas for better performance
**Initial Release — Official Mem0 CLI**
</Update>
A full-featured command-line interface for Mem0, available in both Python and Node.js:
<Update label="2025-07-09" description="">
**Improvements:**
- **Graph:** Graph Optimizations V2 and memory improvements
</Update>
<Update label="2025-07-08" description="">
**New Features:**
- **Database:** Added read replica support for improved database performance
- **UI:** Implemented UI changes for Users Page
- **Feedback:** Enabled feedback functionality
**Bug Fixes:**
- **Serializer:** Fixed GET ALL Serializer
</Update>
<Update label="2025-07-05" description="">
**New Features:**
- **UI:** User Page Revamp and New Users Page
</Update>
<Update label="2025-07-04" description="">
**New Features:**
- **Users:** New Users Page implementation
- **Tools:** Added script to backfill memory categories
**Bug Fixes:**
- **Filters:** Fixed Filters Get All functionality
</Update>
<Update label="2025-07-03" description="">
**Improvements:**
- **Graph:** Graph Memory optimization
- **Memory:** Fixed exact memories and semantically similar memories retrieval
</Update>
<Update label="2025-07-02" description="">
**Improvements:**
- **Categorization:** Refactored categorization logic to utilize Gemini 2.5 Flash and improve message handling
</Update>
<Update label="2025-07-01" description="">
**Bug Fixes:**
- **Memory:** Fixed old_memory issue in Async memory addition lambda
- **Events:** Fixed missing events
</Update>
<Update label="2025-06-30" description="">
**Improvements:**
- **Graph:** Improvements to graph memory and added user to LTM-STM
</Update>
<Update label="2025-06-28" description="">
**New Features:**
- **Graph:** Added support for SQS in graph memory addition
- **Testing:** Added Locust load testing script and Grafana Dashboard
</Update>
<Update label="2025-06-27" description="">
**Improvements:**
- **Rate Limiting:** Updated rate limiting for ADD API to 1000/min
- **Performance:** Improved Neo4j performance
</Update>
<Update label="2025-06-26" description="">
**New Features:**
- **Memory:** Edit Memory From Drawer functionality
- **API:** Added Topic Suggestions API Endpoint
</Update>
<Update label="2025-06-25" description="">
**New Features:**
- **Group Chat:** Group-Chat v2 with Actor-Aware Memories
- **Memory:** Editable Metadata in Memories
- **UI:** Memory Actions Badges
</Update>
<Update label="2025-06-19" description="">
**New Features:**
- **Rate Limiting:** Implemented comprehensive rate limiting system
**Improvements:**
- **Performance:** Added performance indexes for memory stats query
**Bug Fixes:**
- **Search:** Fixed search events not respecting top-k parameter
</Update>
<Update label="2025-06-18" description="">
**New Features:**
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
- **Playground:** Added Claude 4 support on Playground
**Improvements:**
- **Memory:** Added ability to update memory metadata
</Update>
<Update label="2025-06-17" description="">
**New Features:**
- **UI:** New Memories Page UI design
</Update>
<Update label="2025-06-16" description="">
**Improvements:**
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
</Update>
<Update label="2025-06-13" description="">
**Improvements:**
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
</Update>
<Update label="2025-06-11" description="">
**New Features:**
- **OMM:** Added OMM Script and UI functionality
**Improvements:**
- **API:** Added filters validation to semantic_search_v2 endpoint
</Update>
<Update label="2025-06-09" description="">
**New Features:**
- **Intercom:** Set Intercom events for ADD and SEARCH operations
- **OpenMemory:** Added Posthog integration and feedback functionality
- **MCP:** New JavaScript MCP Server with feedback support
**Improvements:**
- **Structured Data:** Enhanced structured data handling in memory management
</Update>
<Update label="2025-06-06" description="">
**New Features:**
- **OAuth:** Added Mem0 OAuth integration
- **OMM:** Added OMM-Mem0 sync for deleted memories
</Update>
<Update label="2025-06-05" description="">
**New Features:**
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
</Update>
<Update label="2025-06-02" description="">
**New Features:**
- **OpenMemory Cloud:** Added OpenMemory Cloud support
- **Structured Data:** Added 'structured_attributes' field to Memory model
</Update>
<Update label="2025-05-30" description="">
**New Features:**
- **Projects:** Added version and enable_graph to project views
- **OpenMemory:** Added Postgres support for OpenMemory
</Update>
<Update label="2025-05-19" description="">
**Bug Fixes:**
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
- **Install:** `pip install mem0-cli` (Python) or `npm install -g @mem0/cli` (Node.js)
- **Full command suite:** `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`
- **Interactive setup:** `mem0 init` with API key entry and user ID configuration
- **Works everywhere:** Platform (Mem0 Cloud) and self-hosted OSS modes
- **Scriptable:** `-o json` flag for CI/CD pipelines and automation
- **Dual SDK:** Same commands, same experience across Python and Node.js
- **Shared spec:** Both implementations driven by a single `cli-spec.json` ensuring identical behavior ([#4575](https://github.com/mem0ai/mem0/pull/4575))
</Update>
</Tab>
<Tab title="Vercel AI SDK">
<Tab title="Plugins">
<Update label="2025-12-26" description="v2.0.5">
<Update label="2026-04-02" description="mem0-plugin v1.0.0">
**Mem0 Plugin for Claude Code, Cursor, and Codex**
The unified Mem0 plugin for AI development environments:
- **9 MCP memory tools:** `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities` — all via `mcp.mem0.ai`
- **Lifecycle hooks:** Automatic memory capture at session start, context compaction, task completion, and session end
- **Cloud MCP server:** Managed endpoint replaces local MCP and Smithery setup
- **Streamable HTTP transport:** New MCP transport protocol for real-time streaming
- **Codex-specific skill:** Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
- **Supported editors:** Claude Code, Claude Cowork, Cursor, Codex
</Update>
<Update label="2025-12-26" description="Vercel AI SDK v2.0.5">
**Bug Fix:**
- **Vercel AI SDK:** Removed unnecessary dependencies to make the package lighter.
- Removed unnecessary dependencies to make the package lighter.
</Update>
<Update label="2025-09-25" description="v2.0.4">
<Update label="2025-09-25" description="Vercel AI SDK v2.0.3 – v2.0.4">
**New Features:**
- Added file support for multimodal capabilities with memory context (v2.0.3)
**Bug Fix:**
- **Vercel AI SDK:** Fixed version parameter in the AI SDK to use V2 for addition.
- Fixed version parameter to use V2 for addition (v2.0.4)
</Update>
<Update label="2025-09-25" description="v2.0.3">
**New Features:**
- **Vercel AI SDK:** Added file support for multimodal capabilities with memory context
</Update>
<Update label="2025-09-03" description="v2.0.2">
<Update label="2025-09-03" description="Vercel AI SDK v2.0.2">
**Bug Fix:**
- **Vercel AI SDK:** Fixed streaming response in the AI SDK.
- Fixed streaming response in the AI SDK.
</Update>
<Update label="2025-08-05" description="v2.0.1">
<Update label="2025-08-05" description="Vercel AI SDK v2.0.0 – v2.0.1">
**New Features:**
- **Vercel AI SDK:** Added a new param `host` to the config.
- Migration to AI SDK V5 (v2.0.0)
- Added `host` param to the config (v2.0.1)
</Update>
<Update label="2025-08-05" description="v2.0.0">
<Update label="2025-06-15" description="Vercel AI SDK v1.0.6">
**New Features:**
- **Vercel AI SDK:** Migration to AI SDK V5.
- Added `filter_memories` param.
</Update>
<Update label="2025-06-15" description="v1.0.6">
<Update label="2025-05-23" description="Vercel AI SDK v1.0.5">
**New Features:**
- **Vercel AI SDK:** Added param `filter_memories`.
- Added support for Google provider.
</Update>
<Update label="2025-05-23" description="v1.0.5">
<Update label="2025-05-10" description="Vercel AI SDK v1.0.3 – v1.0.4">
**New Features:**
- **Vercel AI SDK:** Added support for Google provider.
</Update>
- Added support for `output_format` param (v1.0.4)
<Update label="2025-05-10" description="v1.0.4">
**New Features:**
- **Vercel AI SDK:** Added support for new param `output_format`.
</Update>
<Update label="2025-05-08" description="v1.0.3">
**Improvements:**
- **Vercel AI SDK:** Added support for graceful failure in cases services are down.
- Added graceful failure handling when services are down (v1.0.3)
</Update>
<Update label="2025-05-01" description="v1.0.1">
<Update label="2025-05-01" description="Vercel AI SDK v1.0.1">
**New Features:**
- **Vercel AI SDK:** Added support for graph memories
- Added support for graph memories.
</Update>
</Tab>
</Tabs>
@@ -45,7 +45,7 @@ Before you begin, follow these steps to set up the demo application:
OPENAI_API_KEY=your_openai_api_key
MEM0_API_KEY=your_mem0_api_key
```
You can obtain your `MEM0_API_KEY` by signing up at [Mem0 API Dashboard](https://app.mem0.ai/dashboard/api-keys).
You can obtain your `MEM0_API_KEY` by signing up at <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Dashboard</a>.
5. Start the development server:
```bash
@@ -38,7 +38,7 @@ client = MemoryClient(api_key="your-api-key")
```
<Note>
Replace `your-api-key` with your actual Mem0 API key from the [dashboard](https://app.mem0.ai). Without proper API authentication, memory operations will fail.
Replace `your-api-key` with your actual Mem0 API key from the <a href="https://app.mem0.ai" rel="nofollow">dashboard</a>. Without proper API authentication, memory operations will fail.
</Note>
---
@@ -17,7 +17,7 @@ from mem0 import MemoryClient
client = MemoryClient(api_key="m0-...")
```
Grab an API key from the <Link href="https://app.mem0.ai/">Mem0 dashboard</Link> to get started.
Grab an API key from the <a href="https://app.mem0.ai/" rel="nofollow">Mem0 dashboard</a> to get started.
## Store and Retrieve Scoped Memories
@@ -20,7 +20,7 @@ client = MemoryClient(api_key="your-api-key")
```
<Note>
Your API key needs export permissions to download memory data. Check your project settings on the [dashboard](https://app.mem0.ai) if export operations fail with authentication errors.
Your API key needs export permissions to download memory data. Check your project settings on the <a href="https://app.mem0.ai" rel="nofollow">dashboard</a> if export operations fail with authentication errors.
</Note>
Let's add some sample memories to work with:
@@ -55,7 +55,7 @@ GEMINI_API_KEY=your-gemini-api-key-here
```
<Note>
Ensure you have your Mem0 API key from the [Mem0 Dashboard](https://app.mem0.ai) and your Gemini API key from the [Google AI Studio](https://ai.studio/app/api-keys).
Ensure you have your Mem0 API key from the <a href="https://app.mem0.ai" rel="nofollow">Mem0 Dashboard</a> and your Gemini API key from the [Google AI Studio](https://ai.studio/app/api-keys).
</Note>
## Gemini Memory Agent
@@ -41,7 +41,7 @@ Set up your environment variables:
- `MEM0_API_KEY`: Your Mem0 Platform API key
- `OPENAI_API_KEY`: Your OpenAI API key
You can obtain your Mem0 Platform API key from the [Mem0 Platform](https://app.mem0.ai).
You can obtain your Mem0 Platform API key from the <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.
## Complete Implementation
@@ -357,7 +357,7 @@ Based on our previous session, I remember we covered Vision Language Models and
## Help & Resources
- [LlamaIndex Agent Workflows](https://docs.llamaindex.ai/en/stable/use_cases/agents/)
- [Mem0 Platform](https://app.mem0.ai/)
- <a href="https://app.mem0.ai/" rel="nofollow">Mem0 Platform</a>
---
@@ -25,7 +25,7 @@ os.environ["OPENAI_API_KEY"] = "<your-openai-api-key>"
llm = OpenAI(model="gpt-4.1-nano-2025-04-14")
```
Initialize the Mem0 client. You can find your API key [here](https://app.mem0.ai/dashboard/api-keys). Read about Mem0 [Open Source](https://docs.mem0.ai/open-source/overview).
Initialize the Mem0 client. You can find your API key <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">here</a>. Read about Mem0 [Open Source](https://docs.mem0.ai/open-source/overview).
```python
os.environ["MEM0_API_KEY"] = "<your-mem0-api-key>"
@@ -754,7 +754,7 @@ Exact output varies as Mem0 automatically extracts and deduplicates entities. Th
- [MiroFish GitHub](https://github.com/666ghj/MiroFish) — Source code and setup guide
- [MiroFish Documentation](https://deepwiki.com/666ghj/MiroFish) — Full framework docs
- [Mem0 Graph Memory](/open-source/features/graph-memory) — Graph Memory documentation
- [Mem0 Documentation](https://docs.mem0.ai/) — Full API reference
- [Mem0 Documentation](https://docs.mem0.ai/introduction) — Full API reference
<CardGroup cols={2}>
<Card title="Graph Memory" icon="network-wired" href="/open-source/features/graph-memory">
@@ -222,8 +222,8 @@ context = Mem0Context(user_id="user123")
## Resources
- [Mem0 Documentation](https://docs.mem0.ai)
- [Mem0 Dashboard](https://app.mem0.ai/dashboard)
- [Mem0 Documentation](https://docs.mem0.ai/introduction)
- <a href="https://app.mem0.ai/dashboard" rel="nofollow">Mem0 Dashboard</a>
- [API Reference](https://docs.mem0.ai/api-reference)
---
@@ -23,7 +23,7 @@ MEM0_API_KEY=your_mem0_api_key
OPENAI_API_KEY=your_openai_api_key
```
Get your Mem0 API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys).
Get your Mem0 API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a>.
### Configuration
@@ -308,8 +308,8 @@ run().catch(console.error);
## Resources
- [Mem0 Documentation](https://docs.mem0.ai)
- [Mem0 Dashboard](https://app.mem0.ai/dashboard)
- [Mem0 Documentation](https://docs.mem0.ai/introduction)
- <a href="https://app.mem0.ai/dashboard" rel="nofollow">Mem0 Dashboard</a>
- [API Reference](https://docs.mem0.ai/api-reference)
- [OpenAI Documentation](https://platform.openai.com/docs)
+1 -1
View File
@@ -21,7 +21,7 @@ Adding memory is how Mem0 captures useful details from a conversation so your ag
- **Messages** – The ordered list of user/assistant turns you send to `add`.
- **Infer** – Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages.
- **Metadata** – Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later.
- **User / Session identifiers** – `user_id`, `session_id`, or `run_id` that scope the memory for future searches.
- **User / Session identifiers** – `user_id`, `agent_id`, or `run_id` that scope the memory for future searches.
## How does it work?
+4 -4
View File
@@ -52,7 +52,7 @@ Mem0 maps these classic categories onto its layered storage so you can decide wh
Mem0 stores each layer separately and merges them when you query:
1. **Capture** – Messages enter the conversation layer while the turn is active.
2. **Promote** – Relevant details persist to session or user memory based on your `user_id`, `session_id`, and metadata.
2. **Promote** – Relevant details persist to session or user memory based on your `user_id`, `run_id`, and metadata.
3. **Retrieve** – The search pipeline pulls from all layers, ranking user memories first, then session notes, then raw history.
```python
@@ -66,19 +66,19 @@ memory = Memory(api_key=os.environ["MEM0_API_KEY"])
memory.add(
["I'm Alex and I prefer boutique hotels."],
user_id="alex",
session_id="trip-planning-2025",
run_id="trip-planning-2025",
)
# Later in the session, pull long-term + session context
results = memory.search(
"Any hotel preferences?",
user_id="alex",
session_id="trip-planning-2025",
run_id="trip-planning-2025",
)
```
<Tip>
Use `session_id` when you want short-term context to expire automatically; rely on `user_id` for lasting personalization.
Use `run_id` when you want short-term context to expire automatically; rely on `user_id` for lasting personalization.
</Tip>
## When should you use each layer?
+23 -7
View File
@@ -12,7 +12,7 @@
"logo": {
"light": "/logo/light.svg",
"dark": "/logo/dark.svg",
"href": "https://app.mem0.ai/"
"href": "https://mem0.ai"
},
"navigation": {
"anchors": [
@@ -138,7 +138,10 @@
"group": "Release Notes",
"icon": "rocket",
"pages": [
"changelog"
"changelog/highlights",
"changelog/sdk",
"changelog/platform",
"changelog/openclaw"
]
}
]
@@ -147,9 +150,11 @@
"tab": "OpenClaw",
"groups": [
{
"group": "OpenClaw",
"group": "Agent Harness",
"icon": "robot",
"pages": [
"integrations/openclaw"
"integrations/openclaw",
"integrations/hermes"
]
}
]
@@ -411,12 +416,11 @@
"integrations/autogen",
"integrations/agno",
"integrations/camel-ai",
"integrations/openclaw",
"integrations/hermes",
"integrations/openai-agents-sdk",
"integrations/google-ai-adk",
"integrations/mastra",
"integrations/vercel-ai-sdk"
"integrations/vercel-ai-sdk",
"integrations/chatdev"
]
},
{
@@ -460,6 +464,14 @@
"integrations/cursor",
"integrations/codex"
]
},
{
"group": "Agent Harness",
"icon": "robot",
"pages": [
"integrations/openclaw",
"integrations/hermes"
]
}
]
},
@@ -600,6 +612,10 @@
]
},
"redirects": [
{
"source": "/changelog",
"destination": "/changelog/highlights"
},
{
"source": "/api-reference/memory/v2-search-memories",
"destination": "/api-reference/memory/search-memories"
+20 -49
View File
@@ -382,67 +382,38 @@ Here are the available integrations for Mem0:
Build AI agents with persistent memory using Mastra's framework and tools.
</Card>
<Card
title="Claude Code"
icon={
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.92888 16.2181L9.86008 14.0122L9.92585 13.8199L9.86008 13.7137H9.66782L9.01009 13.6732L6.76369 13.6125L4.81581 13.5315L2.92863 13.4303L2.45304 13.3292L2.00781 12.7423L2.05335 12.4488L2.45304 12.1807L3.02476 12.2313L4.28962 12.3173L6.18692 12.4488L7.56309 12.5298L9.60204 12.7423H9.92585L9.97138 12.6107L9.86008 12.5298L9.77407 12.4488L7.811 11.1182L5.68603 9.71165L4.57295 8.90214L3.97088 8.49233L3.66731 8.10781L3.53577 7.26794L4.08219 6.66587L4.81581 6.71646L5.00301 6.76706L5.74674 7.33877L7.33541 8.56822L9.40979 10.0962L9.71335 10.3491L9.83478 10.2631L9.84996 10.2024L9.71335 9.97475L8.5851 7.93579L7.38095 5.86141L6.84465 5.00131L6.70298 4.48524C6.65239 4.27275 6.61697 4.09567 6.61697 3.87811L7.23928 3.03318L7.58332 2.92188L8.41307 3.03318L8.76218 3.33675L9.27824 4.5156L10.113 6.37242L11.4083 8.89708L11.7877 9.64588L11.9901 10.339L12.066 10.5515H12.1975V10.4301L12.3038 9.00839L12.5011 7.26288L12.6934 5.01649L12.7591 4.38406L13.0728 3.62514L13.6951 3.21532L14.1808 3.44806L14.5805 4.01978L14.5249 4.38912L14.2871 5.93225L13.8216 8.35066L13.5181 9.96969H13.6951L13.8975 9.76731L14.7171 8.67953L16.0933 6.95931L16.7005 6.27629L17.4088 5.52243L17.8641 5.16321H18.7242L19.3567 6.10427L19.0733 7.07568L18.1879 8.19888L17.4543 9.15006L16.4019 10.5667L15.7442 11.7L15.8049 11.7911L15.9618 11.7759L18.3397 11.27L19.6248 11.0372L21.1578 10.7741L21.851 11.0979L21.9269 11.4268L21.6537 12.0997L20.0144 12.5045L18.0918 12.889L15.2282 13.567L15.1927 13.5923L15.2332 13.6428L16.5234 13.7643L17.0749 13.7946H18.4257L20.9403 13.9818L21.598 14.4169L21.9926 14.9482L21.9269 15.3529L20.915 15.869L19.5489 15.5452L16.3615 14.7863L15.2686 14.5131H15.1168V14.6041L16.0275 15.4946L17.6972 17.0023L19.7867 18.9451L19.893 19.4258L19.6248 19.8053L19.3415 19.7648L17.5049 18.3835L16.7966 17.7612L15.1927 16.4104H15.0865V16.552L15.4558 17.0934L17.4088 20.0279L17.51 20.9285L17.3683 21.2219L16.8624 21.399L16.3058 21.2978L15.1624 19.6939L13.9835 17.8877L13.0324 16.2687L12.916 16.3345L12.3544 22.3805L12.0913 22.6891L11.4842 22.9219L10.9782 22.5374L10.7101 21.915L10.9782 20.6856L11.302 19.0818L11.5651 17.8068L11.8029 16.2232L11.9446 15.697L11.9345 15.6616L11.8181 15.6767L10.6241 17.316L8.80771 19.7698L7.37083 21.3079L7.02679 21.4445L6.42977 21.1359L6.48542 20.5844L6.81935 20.0936L8.80771 17.5639L10.0068 15.9955L10.7809 15.0898L10.7758 14.9583H10.7303L5.44824 18.3886L4.50718 18.51L4.10242 18.1306L4.15302 17.5083L4.34528 17.3059L5.93394 16.213L5.92888 16.2181Z" fill="currentColor"/>
</svg>
}
href="/integrations/claude-code"
title="OpenAI Agents SDK"
icon="robot"
href="/integrations/openai-agents-sdk"
>
Add persistent memory to Claude Code and Cowork with MCP server, lifecycle hooks, and SDK skill.
Integrate Mem0 with the OpenAI Agents SDK for persistent memory across multi-agent workflows.
</Card>
<Card
title="Cursor"
icon={
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.4216 6.73383L12.4151 2.11135C12.158 1.96288 11.8408 1.96288 11.5837 2.11135L3.57759 6.73383C3.36146 6.85862 3.22803 7.08941 3.22803 7.33937V16.6606C3.22803 16.9106 3.36146 17.1414 3.57759 17.2662L11.5841 21.8886C11.8412 22.0371 12.1584 22.0371 12.4155 21.8886L20.4219 17.2662C20.6381 17.1414 20.7715 16.9106 20.7715 16.6606V7.33937C20.7715 7.08941 20.6377 6.85862 20.4216 6.73383ZM19.9187 7.71298L12.1896 21.1001C12.1373 21.1903 11.9994 21.1534 11.9994 21.0489V12.2832C11.9994 12.1081 11.9058 11.9461 11.7539 11.8581L4.16282 7.47543C4.07261 7.42319 4.10945 7.28524 4.21394 7.28524H19.6721C19.8916 7.28524 20.0284 7.52279 19.9187 7.71298Z" fill="currentColor"/>
</svg>
}
href="/integrations/cursor"
title="Google ADK"
icon="google"
href="/integrations/google-ai-adk"
>
Add persistent memory to Cursor with MCP server, lifecycle hooks, and context-aware coding.
Integrate Mem0 with Google Agent Development Kit for persistent memory across multi-agent workflows.
</Card>
<Card
title="Codex"
icon={
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5565 10.6338C21.0009 9.27575 20.8528 7.76958 20.1367 6.53501C19.0503 4.63378 16.8528 3.67081 14.7046 4.11526C13.7663 3.05353 12.3836 2.46094 10.9515 2.46094C8.75399 2.46094 6.82807 3.86835 6.13671 5.94242C4.7293 6.23872 3.51943 7.10291 2.80338 8.36217C1.71696 10.2634 1.96387 12.6338 3.42066 14.2634C2.97622 15.6461 3.14906 17.1276 3.8651 18.3622C4.95152 20.2634 7.14906 21.2511 9.2972 20.7819C10.2602 21.8437 11.6182 22.4609 13.0503 22.4609C15.2478 22.4609 17.1737 21.0535 17.8651 18.9795C19.2725 18.6832 20.4824 17.819 21.1984 16.5597C22.2849 14.6585 22.0379 12.2634 20.5565 10.6338ZM13.0503 21.1523C12.1614 21.1523 11.3219 20.856 10.6552 20.2881C10.6799 20.2634 10.754 20.2387 10.7787 20.214L14.754 17.9177C14.9515 17.7943 15.075 17.5967 15.075 17.3498V11.7449L16.754 12.7079C16.7787 12.7079 16.7787 12.7325 16.7787 12.7572V17.3992C16.8034 19.4733 15.1244 21.1523 13.0503 21.1523ZM5.00091 17.7202C4.55646 16.9548 4.40831 16.0659 4.55646 15.2017C4.58115 15.2264 4.63054 15.2511 4.67992 15.2758L8.65523 17.572C8.85276 17.6955 9.09967 17.6955 9.2972 17.572L14.1614 14.7572V16.7079C14.1614 16.7325 14.1614 16.7572 14.1367 16.7572L10.112 19.0782C8.33424 20.1153 6.03794 19.498 5.00091 17.7202ZM3.96387 9.02884C4.40831 8.26341 5.09967 7.69551 5.91449 7.37452V12.1153C5.91449 12.3375 6.03794 12.5597 6.23548 12.6832L11.0997 15.498L9.42066 16.4609C9.39597 16.4609 9.37128 16.4856 9.37128 16.4609L5.34659 14.1399C3.51943 13.1029 2.92683 10.8066 3.96387 9.02884ZM17.791 12.2387L12.9268 9.4239L14.6058 8.46094C14.6305 8.46094 14.6552 8.43625 14.6552 8.46094L18.6799 10.7819C20.4824 11.819 21.075 14.1153 20.0379 15.893C19.5935 16.6585 18.9021 17.2264 18.0873 17.5227V12.8066C18.112 12.5844 17.9886 12.3622 17.791 12.2387ZM19.4454 9.7202C19.4207 9.69551 19.3713 9.67081 19.3219 9.64612L15.3466 7.34983C15.1491 7.22637 14.9021 7.22637 14.7046 7.34983L9.84041 10.1646V8.21402C9.84041 8.18933 9.84041 8.16464 9.86511 8.16464L13.8898 5.84365C15.6923 4.80662 17.9639 5.4239 19.0009 7.22637C19.4454 7.96711 19.5935 8.856 19.4454 9.7202ZM8.92683 13.177L7.24782 12.214C7.22313 12.214 7.22313 12.1893 7.22313 12.1646V7.52267C7.22313 5.44859 8.90214 3.76958 10.9762 3.76958C11.8651 3.76958 12.7046 4.06588 13.3713 4.63378C13.3466 4.65847 13.2972 4.68316 13.2478 4.70785L9.27251 7.00415C9.07498 7.1276 8.95152 7.32514 8.95152 7.57205V13.177H8.92683ZM9.84041 11.2017L12.0133 9.94242L14.1861 11.2017V13.6955L12.0133 14.9548L9.84041 13.6955V11.2017Z" fill="currentColor"/>
</svg>
}
href="/integrations/codex"
title="Flowise"
icon="diagram-project"
href="/integrations/flowise"
>
Add persistent memory to OpenAI Codex with MCP server and skill-based memory protocol.
Add persistent Mem0 memory to Flowise chatflows for context-aware conversations in the low-code builder.
</Card>
<Card
title="OpenClaw"
icon={
<svg width="24" height="24" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="m153.5 173.5q24.62 1.46 46 13.5 12.11 8.1 17.5 21.5 0.74 2.45 0.5 5 0.09 0.81 1 1 1.48-4.9 1-10 5.04 10.48 1.5 22-9.81 27.86-35.5 42.5-26.17 14.97-56 19.5-2.77-0.4-2 1 2.86 1.27 6 1 25.64 1.53 48.5-10 0.34 10.08 2 20 1.08 5.76 5 10 1 1.5 0 3-31.11 20.84-68.5 17.5-23.7-5.7-32.5-28.5-4.39-9.18-3.5-19 15.41 6.23 32 4.5-20.68-6.39-39-18-34.81-27.22-12.5-65.5 11.84-14.83 29-23 4.21 7.66 11.5 12.5 3 1 6 0-26.04-34.62-29-78-0.13-8.46 2-16.5 1 6.5 2 13 3.43 39.53 24.5 73 2.03 2.28 4.5 4 0.5-1.25 1-2.5-1.27-6.54-5-12 0.5-0.75 1-1.5 9.72-3.43 20-4 0.55 10.34 8 17.5 1.94 0.74 4 0.5-17.8-64.6 16.5-122 0.98-1.79 1.5 0-28.21 56.64-13.5 118 1.08 1.43 2.5 0.5 2.21-4.98 2-10.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m454.5 97.5q-1.33 11.18-8.5 20-21.81 26.28-55.5 32-1.11-0.2-2 0.5 2.31 2.82 5.5 4.5 1 2 0 4-9.56 11.3-19.5 20 19.71-8.72 31-27 2.68-0.43 5 1-14.24 30.97-48 36.5-9.93 1.71-20 1.5-6.8-0.48-13 1 5.81 6.92 14 11-10.78 16.03-27 26.5 27.16-7.4 38-33.5 4.34 1.35 9 1-9.08 23.84-33 33.5-18.45 6.41-38 7 22.59 8.92 45-1 12.05-5.52 24-11 9.01-1.79 17 2.5 5.28-4.38 11-8 12.8-6.07 27-5 0 0.5 0 1-19.34 2.69-34 15.5 0.5 0.25 1 0.5 17.79-8.09 36-15 2.71-0.79 5-2 2.5-1 5-2 5.53-4.04 11-8 11.7-4.18 24-6.5 7.78-1.36 15 1.5-2.97 18.45-13.5 34-34.92 49.37-94.5 62.5-59.27 12.45-108-23-15.53-12.52-21.5-31.5-2.47-14.26 4-27-3.15 24.41 14 42-4.92-10.28-7-22-1.97-17.63 7-33 47.28-69.5 125.5-100 15.86-3.42 32-5.5 18.63-1.47 37 1.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m231.5 238.5q1.31-0.2 2 1-3.13 28.62 15 51-16.25 6.75-27-7.5-1-1-2 0 14.73 29.34 46 18.5 1.79 0.52 0 1.5-37.63 16.82-50.5-22.5-5.1-26.48 16.5-42z" fill="currentColor"/>
<path fill-rule="evenodd" d="m203.5 266.5q1.31-0.2 2 1-2.48 22.08 12 39-6.99 1.35-14 0.5 4.59 4.08 10 7-8.71 0.28-14.5-6.5-16.98-22.76 4.5-41z" fill="currentColor"/>
<path fill-rule="evenodd" d="m58.5 284.5q9.6-2.17 14.5 6 5.15 14.18-1 28-11.05-13.14-27.5-17.5 5.15-9.9 14-16.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m56.5 313.5q3.43 5.43 8 10-4.88 0.44-8 4-1.11-0.2-2 0.5 28.91 1.65 38 28.5 0.45 3.16-1 6-11.02-7.01-23-12.5-4.75-3.75-9.5-7.5 1.47 7.42 7 13 8.34 27.18 32 43 0.99 2.41-1.5 3.5-40.25 5.58-66.5-25.5-15.67-22.01-8-48 10.46-23.87 34.5-15z" fill="currentColor"/>
<path fill-rule="evenodd" d="m198.5 319.5q1.44 0.68 2.5 2 2.41 8.23 6 16 1.2 2.64-0.5 5-30.65 21.41-68 18.5-25.16-6.17-32.5-30.5 6.96 4.99 15.5 6.5 8.99 0.75 18 0.5 16.25 2.38 32-2.5 15.9-3.94 27-15.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m239.5 342.5q7.02-0.25 14 0.5 4.46 1.06 8 3.5-5.2 2.35-10 5.5-3.88 4.65-9 7.5-9.89-3.09-9.5-13 2.36-3.63 6.5-4z" fill="currentColor"/>
<path fill-rule="evenodd" d="m214.5 349.5q5.96 7.2 13.5 13 1 1 0 2-28.58 23.34-65.5 20.5-18.15-4.24-27.5-19.5 1.13 0.94 2.5 1.5 14.7 1.42 29-1.5 26.57-0.52 48-16z" fill="currentColor"/>
<path fill-rule="evenodd" d="m302.5 373.5q0.21 2.44-2 3.5-28.69 7.6-50.5-12.5-0.06-6.71 6.5-9 4.45-0.75 9-1 22.26 2.27 37 19z" fill="currentColor"/>
<path fill-rule="evenodd" d="m232.5 365.5q17.6 6.19 10.5 23-10.6 10.42-25.5 11.5-25.94 3.21-49-9 36.75-1.65 64-25.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m113.5 367.5q7.7-0.01 9.5 7-9.69 7.19-18.5 15.5-7.23 5.76-5.5-3.5 3.12-12.84 14.5-19z" fill="currentColor"/>
<path fill-rule="evenodd" d="m126.5 380.5q7.88-0.4 12 6.5-8.5 7.25-17 14.5-5.62-12.55 5-21z" fill="currentColor"/>
<path fill-rule="evenodd" d="m283.5 385.5q3.22 2.95 7 5.5 2.8 4.03 6 7.5 0.42 2.77-2 4-15.5-9.75-31-19.5-1.79-0.98 0-1.5 9.96 2.49 20 4z" fill="currentColor"/>
</svg>
}
href="/integrations/openclaw"
title="AWS Bedrock"
icon="cloud"
href="/integrations/aws-bedrock"
>
Add long-term memory to OpenClaw agents with auto-recall and auto-capture support.
Use Mem0 with AWS Bedrock and OpenSearch Service for cloud-native persistent semantic memory storage.
</Card>
<Card
title="Hermes Agent"
icon="bolt"
href="/integrations/hermes"
title="ChatDev"
icon="comments"
href="/integrations/chatdev"
>
Add long-term memory to Hermes agents with automatic background sync and zero-latency prefetch.
Add persistent cloud-managed memory to ChatDev multi-agent workflows with zero-code YAML configuration.
</Card>
</CardGroup>
+1 -1
View File
@@ -24,7 +24,7 @@ pip install mem0ai agentops python-dotenv
2. Valid API keys:
- [AgentOps API Key](https://app.agentops.ai/dashboard/api-keys)
- OpenAI API Key (for LLM operations)
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys) (optional, for cloud operations)
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a> (optional, for cloud operations)
## Basic Integration Example
+1 -1
View File
@@ -23,7 +23,7 @@ pip install agno mem0ai python-dotenv
```
2. Valid API keys:
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys)
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a>
- OpenAI API Key (for the agent model)
## Quick Integration (Using `Mem0Tools`)
+1 -1
View File
@@ -19,7 +19,7 @@ pip install autogen mem0ai openai python-dotenv
First, we'll import the necessary libraries and set up our configurations.
<Note>Remember to get the Mem0 API key from [Mem0 Platform](https://app.mem0.ai).</Note>
<Note>Remember to get the Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
```python
import os
+243
View File
@@ -0,0 +1,243 @@
---
title: ChatDev
description: "Add persistent, cloud-managed memory to ChatDev multi-agent workflows with Mem0 — no code required, just YAML configuration."
---
Build multi-agent workflows in [ChatDev](https://github.com/OpenBMB/ChatDev) with persistent memory powered by Mem0. ChatDev is a zero-code multi-agent platform where agents, tools, and workflows are defined entirely in YAML. Mem0 integrates as a built-in memory store (`type: mem0`), giving your agents cloud-managed semantic search and cross-session persistence — all without writing any code.
## Overview
In this guide, you'll:
1. Set up ChatDev with the Mem0 memory store
2. Configure agents with persistent memory using YAML
3. Enable automatic memory retrieval and storage across conversations
4. Leverage cross-session persistence for personalized multi-agent interactions
## Prerequisites
- **Python 3.12+**
- **[uv](https://docs.astral.sh/uv/)** — Python package manager
- **Node.js 18+** and **npm** — only needed if using the web console
- A **Mem0 API key** from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>
- An **OpenAI API key** (or another LLM provider supported by ChatDev)
## Setup and Configuration
Install ChatDev and its dependencies (includes `mem0ai`):
```bash
git clone https://github.com/OpenBMB/ChatDev.git
cd ChatDev
uv sync
```
If you plan to use the web console, also install the frontend:
```bash
cd frontend && npm install && cd ..
```
Set up your environment variables in a `.env` file:
<Note>Get your Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
```bash
MEM0_API_KEY=your-mem0-api-key
API_KEY=your-openai-api-key
BASE_URL=https://api.openai.com/v1
```
## Configure Mem0 Memory Store
In your ChatDev workflow YAML, add a Mem0 memory store in the `memory` section:
```yaml
memory:
- name: mem0_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: my-user-123 # optional: scope memories to a user
agent_id: my-agent # optional: scope memories to an agent
```
Mem0 handles all storage, embeddings, and search server-side — no local vector databases or embedding models are needed.
## Attach Memory to an Agent
Reference the memory store in your agent node's `memories` list:
```yaml
nodes:
- id: writer
type: agent
config:
role: |
You are a knowledgeable writer. Use your memories to build
on past interactions.
memories:
- name: mem0_store
top_k: 5
similarity_threshold: 0.5 # minimum relevance score (0.0–1.0); set to -1.0 to disable
retrieve_stage:
- gen
read: true
write: true
```
- **`read: true`** — Agent retrieves relevant memories before generating a response
- **`write: true`** — Agent stores new memories from user input after each interaction
- **`top_k`** — Number of memories to retrieve per query
- **`similarity_threshold`** — Minimum relevance score for retrieved memories. Set to `-1.0` to return all results regardless of score
- **`retrieve_stage`** — When to retrieve memories. Options: `pre_gen_thinking` (before generation), `gen` (during generation), `post_gen_thinking` (after generation), `finished` (after completion)
## Full Example Workflow
Here's a complete workflow YAML that creates a memory-backed conversational agent:
```yaml
version: 0.4.0
graph:
description: Memory-backed conversation using Mem0
nodes:
- id: writer
type: agent
config:
base_url: ${BASE_URL}
api_key: ${API_KEY}
provider: openai
name: gpt-5.4
role: |
You are a knowledgeable writer. Use your memories to build
on past interactions. If memory sections are provided
(wrapped by ===== Related Memories =====), incorporate
relevant context from those memories into your response.
params:
temperature: 0.7
max_tokens: 2000
memories:
- name: mem0_store
top_k: 5
retrieve_stage:
- gen
read: true
write: true
memory:
- name: mem0_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: project-user-123
agent_id: writer-agent
start:
- writer
end: []
```
Run the workflow:
```bash
# Option 1: CLI (recommended for quick testing)
uv run python run.py --path yaml_instance/demo_mem0_memory.yaml --name my_project
# Option 2: Web console
make dev
# Backend starts at http://localhost:6400, frontend at http://localhost:5173
```
To use the web console, open `http://localhost:5173`, create a new workflow, and paste your YAML configuration into the editor. The web console provides a visual chat interface for interacting with your memory-backed agents.
## How It Works
When an agent with Mem0 memory receives input, the following cycle runs automatically:
**1. Retrieve** — Before generating a response, ChatDev queries Mem0 with the user's input using semantic search. Relevant memories are injected into the agent's context in this format:
```
===== Related Memories =====
--- mem0_store ---
1. User's favorite language is Rust
2. User lives in San Francisco
===== End of Memory =====
```
This is why the role prompt in the example references `===== Related Memories =====` — the agent needs to know how to use this injected context.
**2. Generate** — The agent produces a response using the retrieved memories as additional context.
**3. Store** — After generation, the user's input is sent to Mem0 via `client.add()`. Mem0's extraction model automatically identifies and stores facts, preferences, and key information. Only user input is stored — agent output is excluded to keep memories clean.
Memories persist in Mem0's cloud across all sessions. The next time the same `user_id` or `agent_id` is used, previous memories are automatically retrieved.
## Dual-Scope Memory (User + Agent)
When both `user_id` and `agent_id` are configured, Mem0 uses an OR filter to search across both scopes in a single query:
```yaml
memory:
- name: shared_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: alice # stores user preferences ("Alice prefers dark mode")
agent_id: support-bot # stores agent-learned context ("Resolved Alice's billing issue")
```
This means retrieval returns memories from **both** the user's scope and the agent's scope. Writes include both IDs, so each memory is accessible from either dimension. Use this when you want an agent to remember both what the user told it *and* what the agent learned across sessions.
## Configuration Reference
### Memory Store Config
| Field | Required | Description |
|-------|----------|-------------|
| `api_key` | Yes | Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a> |
| `user_id` | No | Scope memories to a specific user |
| `agent_id` | No | Scope memories to a specific agent |
### Memory Attachment Config
| Field | Default | Description |
|-------|---------|-------------|
| `top_k` | `3` | Number of memories to retrieve |
| `similarity_threshold` | `-1.0` (disabled) | Minimum relevance score. Set a value between `0.0` and `1.0` to filter low-relevance results. Default (`-1.0`) returns all matches without filtering |
| `retrieve_stage` | `["gen"]` | When to retrieve: `pre_gen_thinking`, `gen`, `post_gen_thinking`, or `finished` |
| `read` | `true` | Whether the agent retrieves memories |
| `write` | `true` | Whether the agent stores new memories |
## Tips and Common Pitfalls
<Info>
**Indexing delay** — Freshly stored memories may take a few seconds to become searchable. If a memory isn't retrieved immediately after being stored, wait a moment and try again.
</Info>
- **No memories returned on first run** — This is expected. Memories are stored *after* the agent responds, so the first interaction has no prior context. Memories appear starting from the second interaction onward.
- **`mem0ai` not installed** — If you see `ImportError: mem0ai is required for Mem0Memory`, run `uv add mem0ai` or `pip install mem0ai` to add the dependency.
- **Invalid API key** — A wrong or expired `MEM0_API_KEY` will log errors like `Mem0 search failed` or `Mem0 add failed` but won't crash the agent. Check your key at <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>.
- **Pipeline headers in memories** — ChatDev automatically strips internal pipeline headers (e.g., `=== INPUT FROM TASK (user) ===`) before sending text to Mem0, so your memories stay clean.
- **Clearing test memories** — To delete memories created during testing, use the Mem0 dashboard at <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a> or the Python SDK: `MemoryClient().delete_all(user_id="your-test-user")`.
## Key Features
1. **Zero-Code Integration** — Configure Mem0 entirely through YAML, no Python code required
2. **Cloud-Managed Storage** — Mem0 handles embeddings, persistence, and search server-side
3. **Semantic Search** — Retrieve contextually relevant memories, not just keyword matches
4. **Cross-Session Persistence** — Memories survive across runs, sessions, and restarts
5. **Multi-Agent Memory Sharing** — Multiple agents can share memories through common `user_id` or `agent_id` scopes
6. **Intelligent Input Processing** — Only user input is stored; agent output is excluded to prevent noisy memories
## Conclusion
By adding Mem0 as a memory store in ChatDev, your multi-agent workflows gain persistent, intelligent memory with zero code changes. Agents automatically remember past interactions and use that context to provide personalized, coherent responses across sessions.
<CardGroup cols={2}>
<Card title="CrewAI Integration" icon="users" href="/integrations/crewai">
Build multi-agent systems with CrewAI and Mem0
</Card>
<Card title="AutoGen Integration" icon="robot" href="/integrations/autogen">
Build conversational agents with AutoGen and Mem0
</Card>
</CardGroup>
+2 -2
View File
@@ -17,8 +17,8 @@ Add persistent memory to [**Claude Code**](https://docs.anthropic.com/en/docs/cl
Before setting up Mem0 with Claude Code, ensure you have:
1. A Mem0 Platform account and API key:
- [Sign up at app.mem0.ai](https://app.mem0.ai)
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
- <a href="https://app.mem0.ai" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Get your API key</a> (starts with `m0-`)
2. Claude Code CLI or Claude Cowork desktop app installed
+2 -2
View File
@@ -17,8 +17,8 @@ Add persistent memory to [**OpenAI Codex**](https://openai.com/index/codex/) wit
Before setting up Mem0 with Codex, ensure you have:
1. A Mem0 Platform account and API key:
- [Sign up at app.mem0.ai](https://app.mem0.ai)
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
- <a href="https://app.mem0.ai" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Get your API key</a> (starts with `m0-`)
2. OpenAI Codex access
+1 -1
View File
@@ -22,7 +22,7 @@ pip install crewai crewai-tools mem0ai
Import required modules and set up configurations:
<Note>Remember to get your API keys from [Mem0 Platform](https://app.mem0.ai), [OpenAI](https://platform.openai.com) and [Serper Dev](https://serper.dev) for search capabilities.</Note>
<Note>Remember to get your API keys from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>, [OpenAI](https://platform.openai.com) and [Serper Dev](https://serper.dev) for search capabilities.</Note>
```python
import os
+2 -2
View File
@@ -17,8 +17,8 @@ Add persistent memory to [**Cursor**](https://cursor.com) with the Mem0 plugin.
Before setting up Mem0 with Cursor, ensure you have:
1. A Mem0 Platform account and API key:
- [Sign up at app.mem0.ai](https://app.mem0.ai)
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
- <a href="https://app.mem0.ai" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Get your API key</a> (starts with `m0-`)
2. Cursor installed ([cursor.com](https://cursor.com))
+3 -3
View File
@@ -38,7 +38,7 @@ npx flowise start
### 2. Obtain Your Mem0 API Key
1. Navigate to the [Mem0 API Key dashboard](https://app.mem0.ai/dashboard/api-keys).
1. Navigate to the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key dashboard</a>.
2. Generate or copy your existing Mem0 API Key.
![Mem0 API Key](https://raw.githubusercontent.com/FlowiseAI/FlowiseDocs/main/en/.gitbook/assets/mem0/api-key.png)
@@ -70,7 +70,7 @@ Test your memory configuration:
1. Save your Flowise configuration
2. Run a test chat and store some information
3. Verify the stored memories in the [Mem0 Dashboard](https://app.mem0.ai/dashboard/requests)
3. Verify the stored memories in the <a href="https://app.mem0.ai/dashboard/requests" rel="nofollow">Mem0 Dashboard</a>
![Flowise Test Chat](https://raw.githubusercontent.com/FlowiseAI/FlowiseDocs/main/en/.gitbook/assets/mem0/flowise-chat-1.png)
@@ -103,7 +103,7 @@ Available settings include:
### Platform Configuration
Additional settings available in [Mem0 Project Settings](https://app.mem0.ai/dashboard/project-settings):
Additional settings available in <a href="https://app.mem0.ai/dashboard/project-settings" rel="nofollow">Mem0 Project Settings</a>:
1. **Custom Instructions**: Define memory extraction rules
2. **Expiration Date**: Set automatic memory cleanup periods
+1 -1
View File
@@ -22,7 +22,7 @@ pip install google-adk mem0ai python-dotenv
```
2. Valid API keys:
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys)
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a>
- Google AI Studio API Key
## Basic Integration Example
+1 -1
View File
@@ -52,7 +52,7 @@ hermes memory setup
Select **mem0** as the provider and enter your Mem0 API key when prompted. The wizard writes your config to `~/.hermes/mem0.json`.
<Note>Get your API key from [app.mem0.ai](https://app.mem0.ai).</Note>
<Note>Get your API key from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>.</Note>
### Option 2: Manual Configuration
+1 -1
View File
@@ -16,7 +16,7 @@ Combining Mem0 with Keywords AI allows you to:
4. Optimize token usage and reduce costs
<Note>
You can get your Mem0 API key, user_id, and org_id from the [Mem0 dashboard](https://app.mem0.ai/). These are required for proper integration.
You can get your Mem0 API key, user_id, and org_id from the <a href="https://app.mem0.ai/" rel="nofollow">Mem0 dashboard</a>. These are required for proper integration.
</Note>
## Setup and Configuration
+1 -1
View File
@@ -22,7 +22,7 @@ pip install langchain langchain_openai mem0ai python-dotenv
Import required modules and set up configurations:
<Note>Remember to get the Mem0 API key from [Mem0 Platform](https://app.mem0.ai).</Note>
<Note>Remember to get the Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
```python
import os
+1 -1
View File
@@ -23,7 +23,7 @@ pip install langgraph langchain-openai mem0ai python-dotenv
Import required modules and set up configurations:
<Note>Remember to get the Mem0 API key from [Mem0 Platform](https://app.mem0.ai).</Note>
<Note>Remember to get the Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
```python
from typing import Annotated, TypedDict, List
+1 -1
View File
@@ -22,7 +22,7 @@ pip install llama-index-core llama-index-memory-mem0 python-dotenv
Set your Mem0 Platform API key as an environment variable. You can replace `<your-mem0-api-key>` with your actual API key:
<Note type="info">
You can obtain your Mem0 Platform API key from the [Mem0 Platform](https://app.mem0.ai/login).
You can obtain your Mem0 Platform API key from the <a href="https://app.mem0.ai/login" rel="nofollow">Mem0 Platform</a>.
</Note>
```python
+1 -1
View File
@@ -23,7 +23,7 @@ npm install @mastra/core @mastra/mem0 @ai-sdk/openai zod
Set up your environment variables:
<Note>Remember to get the Mem0 API key from [Mem0 Platform](https://app.mem0.ai).</Note>
<Note>Remember to get the Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
```bash
MEM0_API_KEY=your-mem0-api-key
+1 -1
View File
@@ -22,7 +22,7 @@ pip install openai-agents mem0ai
```
2. Valid API keys:
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys)
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a>
- [OpenAI API Key](https://platform.openai.com/api-keys)
## Basic Integration Example
+1 -1
View File
@@ -42,7 +42,7 @@ All memories are scoped to this `userId` — different values create separate me
### Platform Mode (Mem0 Cloud)
<Note>Get your API key from [app.mem0.ai](https://app.mem0.ai).</Note>
<Note>Get your API key from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>.</Note>
Add to your `openclaw.json`:
+1 -1
View File
@@ -9,7 +9,7 @@ Mem0 is a self-improving memory layer for LLM applications, enabling personalize
**Get your API Key**: You'll need a Mem0 API key to use this extension:
a. Sign up at [app.mem0.ai](https://app.mem0.ai)
a. Sign up at <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>
b. Navigate to your API Keys page
+1 -1
View File
@@ -29,7 +29,7 @@ npm install @mem0/vercel-ai-provider
### Setting Up Mem0
1. Get your **Mem0 API Key** from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys).
1. Get your **Mem0 API Key** from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a>.
2. Initialize the Mem0 Client in your application:
+2 -2
View File
@@ -28,7 +28,7 @@ Move your Mem0 implementation to managed infrastructure with enterprise features
## Plan
1. **Sign up**: Create an account on [Mem0 Platform](https://app.mem0.ai).
1. **Sign up**: Create an account on <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.
2. **Get API Key**: Navigate to **Settings > API Keys** and generate a new key.
3. **Review Usage**: Identify where you instantiate `Memory` and where you call `search` or `get_all`.
@@ -368,7 +368,7 @@ If you encounter issues, you can revert immediately by switching your import bac
## Next Steps
- [Platform Dashboard](https://app.mem0.ai) - Monitor usage and manage settings.
- <a href="https://app.mem0.ai" rel="nofollow">Platform Dashboard</a> - Monitor usage and manage settings.
- [Webhooks Setup](/platform/features/webhooks) - Configure real-time event notifications.
- [Organizations & Projects](/api-reference/organizations-projects) - Set up multi-tenancy for your team.
+4 -4
View File
@@ -7,8 +7,8 @@ estimatedTime: "~2 minutes"
<Info>
**Prerequisites**
- Mem0 Platform account ([Sign up here](https://app.mem0.ai))
- API key ([Get one from dashboard](https://app.mem0.ai/settings/api-keys))
- Mem0 Platform account (<a href="https://app.mem0.ai" rel="nofollow">Sign up here</a>)
- API key (<a href="https://app.mem0.ai/settings/api-keys" rel="nofollow">Get one from dashboard</a>)
- Node.js 14+ (for npx)
- An MCP-compatible client (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode)
</Info>
@@ -163,7 +163,7 @@ Agent: Updated your project status successfully.
```
<Info icon="check">
If you get "Connection failed", ensure you have a valid API key from [Mem0 Dashboard](https://app.mem0.ai/settings/api-keys).
If you get "Connection failed", ensure you have a valid API key from <a href="https://app.mem0.ai/settings/api-keys" rel="nofollow">Mem0 Dashboard</a>.
</Info>
---
@@ -171,7 +171,7 @@ Agent: Updated your project status successfully.
## Quick Recovery
- **"Connection refused"** → Check your internet connection and ensure the MCP client is correctly configured
- **"Invalid API key"** → Get a new key from [Mem0 Dashboard](https://app.mem0.ai/settings/api-keys)
- **"Invalid API key"** → Get a new key from <a href="https://app.mem0.ai/settings/api-keys" rel="nofollow">Mem0 Dashboard</a>
- **"npx command not found"** → Install Node.js from [nodejs.org](https://nodejs.org)
---
+1 -1
View File
@@ -64,7 +64,7 @@ Mem0 is the memory engine that keeps conversations contextual so users never rep
<Card title="Connect Integrations" icon="plug" href="/integrations">
LangChain, CrewAI, Vercel AI SDK.
</Card>
<Card title="Monitor in the Dashboard" icon="presentation" href="https://app.mem0.ai">
<Card title="Monitor in the Dashboard" icon="presentation" href="https://app.mem0.ai/login">
Track activity and manage workspaces.
</Card>
</CardGroup>
+1 -1
View File
@@ -150,7 +150,7 @@ Mem0 offers two powerful ways to add memory to your AI applications. Choose base
<Card
title="Try Platform Free"
icon="rocket"
href="https://app.mem0.ai"
href="https://app.mem0.ai/login"
>
Sign up and test the Platform with our free tier. No credit card required.
</Card>
+2 -2
View File
@@ -9,8 +9,8 @@ Get started with Mem0 Platform's hosted API in under 5 minutes. This guide shows
## Prerequisites
- Mem0 Platform account ([Sign up here](https://app.mem0.ai))
- API key ([Get one from dashboard](https://app.mem0.ai/dashboard/settings?tab=api-keys&subtab=configuration))
- Mem0 Platform account (<a href="https://app.mem0.ai" rel="nofollow">Sign up here</a>)
- API key (<a href="https://app.mem0.ai/dashboard/settings?tab=api-keys&subtab=configuration" rel="nofollow">Get one from dashboard</a>)
- Python 3.10+, Node.js 14+, or cURL
## Installation
+2 -2
View File
@@ -12,7 +12,7 @@ We follow the llms.txt standard:
- [llms.txt](https://docs.mem0.ai/llms.txt)
<CardGroup cols={2}>
<Card title="Get an API Key" icon="key" href="https://app.mem0.ai">
<Card title="Get an API Key" icon="key" href="https://app.mem0.ai/login">
Sign up for Mem0 Platform and start building
</Card>
<Card title="Quickstart" icon="rocket" href="/platform/quickstart">
@@ -34,7 +34,7 @@ Works with Claude Code, Cursor, Windsurf, and any assistant that supports skills
Connect Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode, or any MCP-compatible client to Mem0.
Get your API key from [app.mem0.ai](https://app.mem0.ai), then add Mem0 MCP with a single command:
Get your API key from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>, then add Mem0 MCP with a single command:
```bash
npx mcp-add \
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.4.5",
"version": "2.4.6",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
+43
View File
@@ -0,0 +1,43 @@
import { OpenAILLM } from "./openai";
import { LLMConfig, Message } from "../types";
import { LLMResponse } from "./base";
export class DeepSeekLLM extends OpenAILLM {
constructor(config: LLMConfig) {
const apiKey = config.apiKey || process.env.DEEPSEEK_API_KEY;
if (!apiKey) {
throw new Error("DeepSeek API key is required");
}
super({
...config,
apiKey,
baseURL:
config.baseURL ||
process.env.DEEPSEEK_API_BASE ||
"https://api.deepseek.com",
model: config.model || "deepseek-chat",
});
}
async generateResponse(
messages: Message[],
responseFormat?: { type: string },
tools?: any[],
): Promise<string | LLMResponse> {
try {
return await super.generateResponse(messages, responseFormat, tools);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`DeepSeek LLM failed: ${message}`);
}
}
async generateChat(messages: Message[]): Promise<LLMResponse> {
try {
return await super.generateChat(messages);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`DeepSeek LLM failed: ${message}`);
}
}
}
+3
View File
@@ -35,6 +35,7 @@ import { LangchainEmbedder } from "../embeddings/langchain";
import { LangchainVectorStore } from "../vector_stores/langchain";
import { AzureAISearch } from "../vector_stores/azure_ai_search";
import { PGVector } from "../vector_stores/pgvector";
import { DeepSeekLLM } from "../llms/deepseek";
export class EmbedderFactory {
static create(provider: string, config: EmbeddingConfig): Embedder {
@@ -82,6 +83,8 @@ export class LLMFactory {
return new MistralLLM(config);
case "langchain":
return new LangchainLLM(config);
case "deepseek":
return new DeepSeekLLM(config);
default:
throw new Error(`Unsupported LLM provider: ${provider}`);
}
+27
View File
@@ -14,6 +14,25 @@ try {
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
// Default sampling rate for hot-path OSS events. Lifecycle events always fire at 100%.
// Override via MEM0_TELEMETRY_SAMPLE_RATE env var. Mirrors mem0/memory/telemetry.py.
const DEFAULT_SAMPLE_RATE = 0.1;
const MEM0_TELEMETRY_SAMPLE_RATE: number = ((): number => {
try {
const raw = process?.env?.MEM0_TELEMETRY_SAMPLE_RATE;
if (raw !== undefined) {
const parsed = Number(raw);
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 1) {
return parsed;
}
}
} catch {}
return DEFAULT_SAMPLE_RATE;
})();
// Events that bypass sampling. Keep in sync with _captureEvent call sites in memory/index.ts.
const LIFECYCLE_EVENTS: ReadonlySet<string> = new Set(["init", "reset"]);
class UnifiedTelemetry implements TelemetryClient {
private apiKey: string;
private host: string;
@@ -78,6 +97,12 @@ async function captureClientEvent(
return;
}
// >= so that rate=0 drops everything and rate=1 keeps everything (Math.random() ∈ [0, 1)).
const isLifecycle = LIFECYCLE_EVENTS.has(eventName);
if (!isLifecycle && Math.random() >= MEM0_TELEMETRY_SAMPLE_RATE) {
return;
}
const eventData: TelemetryEventData = {
function: `${instance.constructor.name}`,
method: eventName,
@@ -86,6 +111,8 @@ async function captureClientEvent(
client_version: version,
client_source: "nodejs",
...additionalData,
// sample_rate set AFTER the spread so callers can never override it
sample_rate: isLifecycle ? 1.0 : MEM0_TELEMETRY_SAMPLE_RATE,
};
await telemetry.captureEvent(
@@ -23,6 +23,8 @@ export interface TelemetryEventData {
timestamp?: string;
client_source: "browser" | "nodejs";
client_version: string;
/** Set by the sampling layer so PostHog dashboards can extrapolate via 1/sample_rate. */
sample_rate?: number;
[key: string]: any;
}
+124
View File
@@ -0,0 +1,124 @@
/// <reference types="jest" />
/**
* DeepSeek LLM — unit tests (mocked OpenAI).
*/
import { DeepSeekLLM } from "../src/llms/deepseek";
const mockCreate = jest.fn();
jest.mock("openai", () => {
return jest.fn().mockImplementation(() => ({
chat: { completions: { create: mockCreate } },
}));
});
describe("DeepSeekLLM (unit)", () => {
beforeEach(() => mockCreate.mockClear());
it("throws when no API key is provided", () => {
const original = process.env.DEEPSEEK_API_KEY;
delete process.env.DEEPSEEK_API_KEY;
try {
expect(() => new DeepSeekLLM({})).toThrow("DeepSeek API key is required");
} finally {
if (original !== undefined) process.env.DEEPSEEK_API_KEY = original;
}
});
it("generateResponse() returns a text response", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: "Hello, world!",
role: "assistant",
tool_calls: null,
},
},
],
});
const llm = new DeepSeekLLM({ apiKey: "test-key" });
const result = await llm.generateResponse([
{ role: "user", content: "Hi" },
]);
expect(mockCreate).toHaveBeenCalledTimes(1);
expect(result).toBe("Hello, world!");
});
it("generateResponse() handles tool calls", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: "",
role: "assistant",
tool_calls: [
{
function: {
name: "get_weather",
arguments: '{"city": "London"}',
},
},
],
},
},
],
});
const llm = new DeepSeekLLM({ apiKey: "test-key" });
const result = await llm.generateResponse(
[{ role: "user", content: "What is the weather?" }],
undefined,
[{ type: "function", function: { name: "get_weather" } }],
);
expect(result).toEqual({
content: "",
role: "assistant",
toolCalls: [{ name: "get_weather", arguments: '{"city": "London"}' }],
});
});
it("generateResponse() wraps API errors with a clear message", async () => {
mockCreate.mockRejectedValueOnce(new Error("Connection refused"));
const llm = new DeepSeekLLM({ apiKey: "test-key" });
await expect(
llm.generateResponse([{ role: "user", content: "Hi" }]),
).rejects.toThrow("DeepSeek LLM failed: Connection refused");
});
it("generateChat() returns LLMResponse shape", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: { content: "I can help with that.", role: "assistant" },
},
],
});
const llm = new DeepSeekLLM({ apiKey: "test-key" });
const result = await llm.generateChat([
{ role: "user", content: "Help me" },
]);
expect(result).toEqual({
content: "I can help with that.",
role: "assistant",
});
});
it("generateChat() wraps API errors with a clear message", async () => {
mockCreate.mockRejectedValueOnce(new Error("Timeout"));
const llm = new DeepSeekLLM({ apiKey: "test-key" });
await expect(
llm.generateChat([{ role: "user", content: "Hi" }]),
).rejects.toThrow("DeepSeek LLM failed: Timeout");
});
});
@@ -87,6 +87,11 @@ jest.mock("../src/llms/lmstudio", () => ({
.fn()
.mockImplementation((config) => ({ type: "lmstudio-llm", config })),
}));
jest.mock("../src/llms/deepseek", () => ({
DeepSeekLLM: jest
.fn()
.mockImplementation((config) => ({ type: "deepseek-llm", config })),
}));
jest.mock("../src/vector_stores/qdrant", () => ({
Qdrant: jest
@@ -200,6 +205,7 @@ describe("LLMFactory", () => {
["mistral"],
["langchain"],
["lmstudio"],
["deepseek"],
])("creates LLM for provider '%s'", (provider) => {
expect(() => LLMFactory.create(provider, dummyLLMConfig)).not.toThrow();
});
@@ -0,0 +1,211 @@
/// <reference types="jest" />
/**
* Telemetry sampling — unit tests.
*
* Sampling lives inside captureClientEvent in src/utils/telemetry.ts. It uses
* Math.random() compared against MEM0_TELEMETRY_SAMPLE_RATE (default 0.1) to
* drop hot-path events; lifecycle events ('init', 'reset') always fire.
*
* These tests target captureClientEvent directly. Existing OSS tests that mock
* captureClientEvent (vector-stores-compat, dimension-autodetect, config-manager)
* are unaffected because the function signature and contract are unchanged.
*/
import type { TelemetryInstance } from "../src/utils/telemetry.types";
// Helper to make a fake TelemetryInstance.
function makeInstance(
overrides: Partial<TelemetryInstance> = {},
): TelemetryInstance {
return {
telemetryId: "test-id",
constructor: { name: "Memory" },
host: "https://test.example.com",
...overrides,
};
}
describe("telemetry sampling", () => {
let originalFetch: typeof global.fetch;
let fetchMock: jest.Mock;
let randomSpy: jest.SpyInstance;
beforeEach(() => {
originalFetch = global.fetch;
fetchMock = jest.fn().mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue(""),
});
global.fetch = fetchMock as any;
randomSpy = jest.spyOn(Math, "random");
});
afterEach(() => {
global.fetch = originalFetch;
randomSpy.mockRestore();
jest.resetModules();
});
describe("lifecycle events always fire", () => {
it("init event fires even at the highest random value", async () => {
randomSpy.mockReturnValue(0.999);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("init", makeInstance());
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("reset event fires even at the highest random value", async () => {
randomSpy.mockReturnValue(0.999);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("reset", makeInstance());
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("init event payload has sample_rate: 1.0", async () => {
randomSpy.mockReturnValue(0.999);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("init", makeInstance());
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.properties.sample_rate).toBe(1.0);
});
});
describe("hot-path events are sampled", () => {
it("add event is dropped when random > sample_rate", async () => {
randomSpy.mockReturnValue(0.99); // > 0.1 default rate
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
expect(fetchMock).not.toHaveBeenCalled();
});
it("search event is dropped when random > sample_rate", async () => {
randomSpy.mockReturnValue(0.99);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("search", makeInstance());
expect(fetchMock).not.toHaveBeenCalled();
});
it("add event passes when random < sample_rate", async () => {
randomSpy.mockReturnValue(0.05); // < 0.1 default rate
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("hot-path event payload has sample_rate: 0.1", async () => {
randomSpy.mockReturnValue(0.05);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.properties.sample_rate).toBe(0.1);
});
it("sample_rate cannot be overridden by additionalData", async () => {
randomSpy.mockReturnValue(0.05);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance(), { sample_rate: 0.99 });
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.properties.sample_rate).toBe(0.1);
});
});
describe("env var override", () => {
afterEach(() => {
delete process.env.MEM0_TELEMETRY_SAMPLE_RATE;
});
it("rate 1.0 sends every event including hot-path at high random", async () => {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "1.0";
jest.resetModules();
randomSpy.mockReturnValue(0.999);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
// 0.999 > 1.0 is false, so the gate never trips
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("rate 0.0 drops every hot-path event including at random 0", async () => {
// The gate is `random >= rate`, so rate=0 drops at random=0 (0 >= 0 is true).
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "0.0";
jest.resetModules();
randomSpy.mockReturnValue(0.0);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
expect(fetchMock).not.toHaveBeenCalled();
});
it("rate 0.0 drops at any random value", async () => {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "0.0";
jest.resetModules();
randomSpy.mockReturnValue(0.5);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
expect(fetchMock).not.toHaveBeenCalled();
});
it("rate 0.0 still passes lifecycle events", async () => {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "0.0";
jest.resetModules();
randomSpy.mockReturnValue(0.999);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("init", makeInstance());
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("rate 0.5 passes events at random below 0.5", async () => {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "0.5";
jest.resetModules();
randomSpy.mockReturnValue(0.3);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("invalid env var falls back to default rate", async () => {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "not a number";
jest.resetModules();
randomSpy.mockReturnValue(0.05);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("out-of-range env var (negative) falls back to default rate", async () => {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "-0.5";
jest.resetModules();
randomSpy.mockReturnValue(0.05);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("out-of-range env var (> 1) falls back to default rate", async () => {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "5";
jest.resetModules();
randomSpy.mockReturnValue(0.05);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance());
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});
describe("contract preservation", () => {
it("lifecycle event includes additionalData", async () => {
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("init", makeInstance(), {
api_version: "v1.1",
client_type: "Memory",
});
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.properties.api_version).toBe("v1.1");
expect(body.properties.client_type).toBe("Memory");
expect(body.properties.sample_rate).toBe(1.0);
});
it("captureClientEvent without telemetryId is a no-op", async () => {
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("add", makeInstance({ telemetryId: "" }));
expect(fetchMock).not.toHaveBeenCalled();
});
});
});
+2
View File
@@ -134,6 +134,8 @@ class AzureOpenAILLM(LLMBase):
"messages": messages,
})
if response_format:
params["response_format"] = response_format
if tools:
params["tools"] = tools
params["tool_choice"] = tool_choice
+33 -8
View File
@@ -63,6 +63,19 @@ def _normalize_iso_timestamp_to_utc(timestamp: Optional[str]) -> Optional[str]:
return parsed.astimezone(timezone.utc).isoformat()
def _resolve_mapped_id(temp_uuid_mapping, resp, event_type):
"""Resolve a temp integer ID from the LLM response to a real UUID.
Returns the UUID if found, or None (with a warning log) if the LLM
hallucinated an ID that doesn't exist in the mapping.
"""
raw_id = resp.get("id")
memory_id = temp_uuid_mapping.get(raw_id)
if memory_id is None:
logger.warning(f"{event_type} skipped: LLM returned unknown id {raw_id!r}")
return memory_id
# Fields that hold runtime auth/connection objects and must be preserved.
# These are non-serializable objects (e.g. AWSV4SignerAuth, RequestsHttpConnection)
# needed by clients like OpenSearch — not sensitive strings to redact.
@@ -639,28 +652,34 @@ class Memory(MemoryBase):
)
returned_memories.append({"id": memory_id, "memory": action_text, "event": event_type})
elif event_type == "UPDATE":
memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "UPDATE")
if memory_id is None:
continue
# Ensure action_text has an embedding cached to avoid redundant API calls
if action_text not in new_message_embeddings:
new_message_embeddings[action_text] = self.embedding_model.embed(action_text, "update")
self._update_memory(
memory_id=temp_uuid_mapping[resp.get("id")],
memory_id=memory_id,
data=action_text,
existing_embeddings=new_message_embeddings,
metadata=deepcopy(metadata),
)
returned_memories.append(
{
"id": temp_uuid_mapping[resp.get("id")],
"id": memory_id,
"memory": action_text,
"event": event_type,
"previous_memory": resp.get("old_memory"),
}
)
elif event_type == "DELETE":
self._delete_memory(memory_id=temp_uuid_mapping[resp.get("id")])
memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "DELETE")
if memory_id is None:
continue
self._delete_memory(memory_id=memory_id)
returned_memories.append(
{
"id": temp_uuid_mapping[resp.get("id")],
"id": memory_id,
"memory": action_text,
"event": event_type,
}
@@ -1755,6 +1774,9 @@ class AsyncMemory(MemoryBase):
)
memory_tasks.append((task, resp, "ADD", None))
elif event_type == "UPDATE":
memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "UPDATE")
if memory_id is None:
continue
# Ensure action_text has an embedding cached to avoid redundant API calls
if action_text not in new_message_embeddings:
new_message_embeddings[action_text] = await asyncio.to_thread(
@@ -1762,16 +1784,19 @@ class AsyncMemory(MemoryBase):
)
task = asyncio.create_task(
self._update_memory(
memory_id=temp_uuid_mapping[resp["id"]],
memory_id=memory_id,
data=action_text,
existing_embeddings=new_message_embeddings,
metadata=deepcopy(metadata),
)
)
memory_tasks.append((task, resp, "UPDATE", temp_uuid_mapping[resp["id"]]))
memory_tasks.append((task, resp, "UPDATE", memory_id))
elif event_type == "DELETE":
task = asyncio.create_task(self._delete_memory(memory_id=temp_uuid_mapping[resp.get("id")]))
memory_tasks.append((task, resp, "DELETE", temp_uuid_mapping[resp.get("id")]))
memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "DELETE")
if memory_id is None:
continue
task = asyncio.create_task(self._delete_memory(memory_id=memory_id))
memory_tasks.append((task, resp, "DELETE", memory_id))
elif event_type == "NONE":
# Even if content doesn't need updating, update session IDs if provided
memory_id = temp_uuid_mapping.get(resp.get("id"))
+54 -3
View File
@@ -2,6 +2,7 @@ import atexit
import logging
import os
import platform
import random
import sys
import threading
@@ -22,16 +23,65 @@ if not isinstance(MEM0_TELEMETRY, bool):
logging.getLogger("posthog").setLevel(logging.CRITICAL + 1)
logging.getLogger("urllib3").setLevel(logging.CRITICAL + 1)
_logger = logging.getLogger(__name__)
# Default sampling rate for hot-path OSS events. Lifecycle events always fire at 100%.
# Override via MEM0_TELEMETRY_SAMPLE_RATE env var.
_DEFAULT_SAMPLE_RATE = 0.1
def _parse_sample_rate(raw):
"""Parse MEM0_TELEMETRY_SAMPLE_RATE env var. Never raises."""
try:
value = float(raw)
except (TypeError, ValueError):
_logger.debug("MEM0_TELEMETRY_SAMPLE_RATE %r is not a number, defaulting to %s", raw, _DEFAULT_SAMPLE_RATE)
return _DEFAULT_SAMPLE_RATE
if not 0.0 <= value <= 1.0:
_logger.debug("MEM0_TELEMETRY_SAMPLE_RATE %s out of [0.0, 1.0], defaulting to %s", value, _DEFAULT_SAMPLE_RATE)
return _DEFAULT_SAMPLE_RATE
return value
MEM0_TELEMETRY_SAMPLE_RATE = _parse_sample_rate(os.environ.get("MEM0_TELEMETRY_SAMPLE_RATE", str(_DEFAULT_SAMPLE_RATE)))
# Events that bypass sampling and always fire. Keep this set in sync with the
# event names passed to capture_event() in mem0/memory/main.py.
_LIFECYCLE_EVENTS = frozenset({"mem0.init", "mem0.reset", "mem0._create_procedural_memory"})
def _sampling_before_send(msg):
"""PostHog before_send hook: drop sampled hot-path events, annotate survivors with sample_rate."""
if not isinstance(msg, dict):
return None
event_name = msg.get("event", "")
is_lifecycle = event_name in _LIFECYCLE_EVENTS
# >= so that rate=0 drops everything and rate=1 keeps everything (random ∈ [0, 1)).
if not is_lifecycle and random.random() >= MEM0_TELEMETRY_SAMPLE_RATE:
return None
# Annotate so PostHog dashboards can extrapolate true counts via 1/sample_rate.
properties = msg.setdefault("properties", {})
properties["sample_rate"] = 1.0 if is_lifecycle else MEM0_TELEMETRY_SAMPLE_RATE
return msg
class AnonymousTelemetry:
def __init__(self, vector_store=None):
def __init__(self, vector_store=None, before_send=None):
if not MEM0_TELEMETRY:
self.posthog = None
self.user_id = None
return
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST)
try:
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST, before_send=before_send)
except TypeError:
# posthog <4.5.0 does not accept before_send; fall back without sampling.
_logger.debug("posthog.Posthog does not accept before_send; upgrade to >=4.5.0 for sampling")
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST)
self.user_id = get_or_create_user_id(vector_store)
def capture_event(self, event_name, properties=None, user_email=None):
@@ -87,7 +137,7 @@ def _get_oss_telemetry():
# Double-checked locking
if _oss_telemetry_instance is not None:
return _oss_telemetry_instance
_oss_telemetry_instance = AnonymousTelemetry()
_oss_telemetry_instance = AnonymousTelemetry(before_send=_sampling_before_send)
atexit.register(_shutdown_oss_telemetry)
return _oss_telemetry_instance
@@ -102,6 +152,7 @@ def _shutdown_oss_telemetry():
# Module-level client telemetry singleton (used by capture_client_event).
# No before_send — hosted MemoryClient traffic must never be sampled.
client_telemetry = AnonymousTelemetry()
atexit.register(client_telemetry.close)
-162
View File
@@ -1,162 +0,0 @@
# Changelog
All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file.
## [1.0.4] - 2026-04-04
### Added
- **Interactive init flow**: `openclaw mem0 init` with interactive menu (email verification or direct API key). Non-interactive modes: `--api-key`, `--email`, `--email --code`
- **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
- **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status` (renamed from `stats`), `openclaw mem0 config show`, `openclaw mem0 config set`
- **`fs-safe.ts` module**: Isolated filesystem wrappers (sync read/write/exists/mkdir/unlink) in a separate entry point — keeps file I/O out of the main bundle
- **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands
- **`cli/config-file.ts`**: Persistent plugin auth storage in `~/.openclaw/openclaw.json`
- **Plugin manifest**: Added `contracts.tools`, `configSchema`, and `uiHints` to `openclaw.plugin.json`
- **Test suite**: 329 tests across 10 test files covering tools, CLI, config, dream gate, providers, and skill-loader
### Changed
- **Modular architecture**: Extracted tools into `tools/` directory (7 files) and CLI into `cli/commands.ts` — `index.ts` down from ~1700 to ~890 lines
- **Code splitting**: tsup builds with `splitting: true` and two entry points (`index.ts`, `fs-safe.ts`), separating filesystem I/O from the main bundle
- **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`) matching the plugin manifest
- **WRITE_TOOLS updated**: Dream gate tracks `memory_delete` and `memory_add` instead of `memory_forget` and `memory_store`
- **`mem0ai` dependency**: Updated from `2.3.0` to `2.4.5`
- **Auto-recall timeout**: Recall wrapped in 8-second `Promise.race` — if the LLM takes too long, recall is skipped instead of stalling the gateway
- **Auto-capture fire-and-forget**: `provider.add()` now runs in the background via `.then()/.catch()` — the `agent_end` hook returns immediately, zero event loop blocking
- **Auto-capture minimum content gate**: Skips extraction when total user content is <50 chars after filtering — trivial conversations ("ok", "thanks") no longer trigger LLM calls
- **CLI search**: Lowered threshold to 0.3 so explicit searches are more permissive than auto-recall
### Removed
- `memory_store` tool — replaced by `memory_add`
- `memory_forget` tool — replaced by `memory_delete`
- `memory_delete_all` tool — merged into `memory_delete`
- **`custom_instructions` / `custom_categories` in `buildAddOptions`**: No longer injected into every auto-capture API call. Config fields (`customInstructions`, `customCategories`) remain as user-configurable options.
## [1.0.3] - 2026-04-03
### Fixed
- **Path traversal vulnerability**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal via `config.domain` or the exported `loadSkill` API
- **Noise filter regression**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction` so the filter correctly matches real upstream compaction audit messages
- **Cosmetic revert**: Restored `// Over-fetch for ranking` comment in `recall.ts` (was changed to work around a false-positive scanner match on the substring `fetch`)
### Changed
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
### Added
- **Path traversal tests**: 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
## [1.0.2] - 2026-04-02
### Fixed
- **Security scanner warning**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — OpenClaw already resolves `${VAR}` in `openclaw.json` before passing config to the plugin, so plugin-side env resolution was redundant and triggered the "credential harvesting" static analysis warning ([#4676](https://github.com/mem0ai/mem0/pull/4676))
## [1.0.1] - 2026-04-02
### Added
- **CD workflow**: Added continuous deployment workflow for `@mem0/openclaw-mem0` with OIDC trusted publishing ([#4672](https://github.com/mem0ai/mem0/pull/4672))
- **Plugin configuration manifest**: Added `compat` and `build` metadata to `package.json` specifying minimum gateway version and OpenClaw SDK compatibility (`>=2026.3.24-beta.2`) ([#4667](https://github.com/mem0ai/mem0/pull/4667))
- **LICENSE**: Added Apache-2.0 license file to the package ([#4667](https://github.com/mem0ai/mem0/pull/4667))
### Fixed
- **Dream gate correctness**: Fixed cheap-first ordering, session isolation, and verified completion in the dream gate memory consolidation pipeline ([#4666](https://github.com/mem0ai/mem0/pull/4666))
- **Graceful startup without API key**: Plugin now starts gracefully when no API key is configured instead of crashing on init ([#4669](https://github.com/mem0ai/mem0/pull/4669))
## [1.0.0] - 2026-04-01
### Added
- **Skills-based memory architecture**: New skill-loader and skill-based extraction pipeline with batched extraction for higher quality memory capture ([#4624](https://github.com/mem0ai/mem0/pull/4624))
- **Dream gate**: Added `dream-gate.ts` for memory consolidation and dream-cycle processing
- **Enhanced recall**: New `recall.ts` module with improved recall logic and skill-aware retrieval
- **Memory triage skill**: Domain-aware memory triage with companion domain support and recall protocol
- **Memory dream skill**: Skill for memory consolidation during idle periods
- **Plugin configuration**: Added `openclaw.plugin.json` manifest and `scripts/configure.py` setup helper
### Changed
- Extraction pipeline refactored to use skills-based architecture for more contextual and higher quality memory capture
## [0.4.1] - 2026-03-26
### Added
- **Improved extraction quality**: Enhanced noise filtering, deduplication, and better extraction instructions for higher-quality memory capture (#4302)
### Fixed
- **Credential detection in extraction**: Improved detection of credentials, API keys, and secrets in extraction instructions to prevent them from being stored as memories (#4552)
- **Standalone timestamp extraction**: Prevented extraction of standalone timestamps as memories when no meaningful content accompanies them (#4550)
## [0.4.0] - 2026-03-16
### Added
- **Non-interactive trigger filtering**: Skips recall and capture for `cron`, `heartbeat`, `automation`, and `schedule` triggers — prevents system-generated noise from polluting memory
- **Subagent hallucination prevention**: `isSubagentSession()` detects ephemeral subagent sessions and routes recall to the parent (main user) namespace instead of empty ephemeral namespaces; skips capture to prevent orphaned memories
- **Subagent-specific preamble**: Subagents receive "You are a subagent — use these memories for context but do not assume you are this user" to prevent identity assumption
- **User identity in recall preamble**: Recalled memories now include `userId` attribution for better context
- **User identity in extraction preamble**: Extraction context includes user identity and current date for accurate attribution and temporal anchoring
- **User-content guard**: Skips extraction when no meaningful user messages remain after filtering
- **Dynamic recall thresholding**: Memories scoring less than 50% of the top result are dropped to filter out the long tail of weak matches
- **SQLite resilience for OSS mode**: Init error recovery with automatic retry (history disabled) when native SQLite bindings fail under jiti
- **`disableHistory` config option**: New `oss.disableHistory` flag to explicitly skip history DB initialization
- **Updated minimum package version of mem0ai package**: Updated minimum package version of mem0ai package to ^2.3.0 to force old users to migrate to better-sqlite3
- 78 unit tests covering filtering, isolation, trigger filtering, subagent detection, and SQLite resilience
### Changed
- Auto-recall threshold raised from 0.5 to 0.6 for stricter precision during automatic injection (explicit tool searches remain at 0.5)
- Recall candidate pool increased to `topK * 2` for better filtering headroom
- Provider init promises now reset on failure, allowing retry on subsequent calls
- Relaxed extraction instructions: related facts are kept together to preserve context (removed atomic memory requirement)
### Fixed
- **Concurrent session race condition**: Lifecycle hooks (`before_agent_start`, `agent_end`) now use `ctx.sessionKey` directly from the event context instead of a shared mutable `currentSessionId` variable, preventing cross-session data leaks when multiple sessions run simultaneously
## [0.3.1] - 2026-03-12
### Added
- **Message filtering pipeline**: Multi-stage noise removal before extraction — drops heartbeats, timestamps, single-word acks, system routing metadata, compaction audit logs, and generic assistant acknowledgments
- **Broad recall for new sessions**: Short or new-session prompts trigger a secondary broad search to avoid cold-start blindness
- **Client-side threshold filtering**: Safety net that drops low-relevance results even if the API doesn't honor the threshold parameter
- **Temporal anchoring**: Extraction instructions now include current date so memories are prefixed with "As of YYYY-MM-DD, ..."
- **Summary message inclusion**: Earlier assistant messages containing work summaries are included in extraction context even if outside the recent-message window
- 55 unit tests covering filtering and isolation helpers
### Changed
- Default `searchThreshold` remains at 0.5, with client-side filtering as a safety net
- Extraction window expanded from last 10 → last 20 messages for richer context
- Rewritten custom extraction instructions: conciseness, outcome-over-intent, deduplication guidance, language preservation
- **Refactored** monolithic `index.ts` (1772 lines) into 6 focused modules: `types.ts`, `providers.ts`, `config.ts`, `filtering.ts`, `isolation.ts`, `index.ts`
### Fixed
- **README image on npmjs.com**: Changed architecture diagram from relative path to absolute GitHub URL so it renders correctly on the npm registry
## [0.3.0] - 2026-03-10
### Fixed
- Updated `mem0ai` dependency which includes the sqlite3 to better-sqlite3 migration for native binding resolution (#4270)
## [0.2.0] - 2026-03-09
### Added
- "Understanding userId" section in docs clarifying that `userId` is user-defined
- Per-agent memory isolation for multi-agent setups via `agentId`
- Regression tests for per-agent isolation helpers
### Changed
- Updated config examples to use concrete `userId` values instead of placeholders
### Fixed
- Migrated platform search to Mem0 v2 API
## [0.1.2] - 2026-02-19
### Added
- Source field for openclaw memory entries
### Fixed
- Auto-recall injection and auto-capture message drop
## [0.1.0] - 2026-02-02
### Added
- Initial release of the OpenClaw Mem0 plugin
- Platform mode (Mem0 Cloud) and open-source mode support
- Auto-recall: inject relevant memories before each turn
- Auto-capture: store facts after each turn
- Configurable `topK`, `threshold`, and `apiVersion` options
+39 -65
View File
@@ -12,7 +12,7 @@ openclaw plugins install @mem0/openclaw-mem0
### Platform (Mem0 Cloud)
Get an API key from [app.mem0.ai](https://app.mem0.ai):
Get an API key from [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys):
```bash
openclaw mem0 init --api-key <your-key> --user-id <your-user-id>
@@ -32,7 +32,9 @@ Or configure manually in `openclaw.json`:
### Open-Source (Self-hosted)
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM.
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM. Vectors are stored locally in SQLite at `~/.mem0/vector_store.db` — no external database required.
Defaults: `text-embedding-3-small` for embeddings, `gpt-5.4` for fact extraction.
```json5
"openclaw-mem0": {
@@ -53,7 +55,7 @@ Customize the embedder, vector store, or LLM via the `oss` block:
"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" } }
"llm": { "provider": "openai", "config": { "model": "gpt-5.4" } }
}
}
```
@@ -74,79 +76,57 @@ Both run silently. No prompting, no manual calls required.
### Memory Scopes
| Scope | Description |
|-------|-------------|
| **Session (short-term)** | Memories scoped to the current conversation via `run_id`. Automatically recalled alongside long-term memories. |
| **User (long-term)** | Persistent memories that span all sessions. Stored via `memory_add` with `longTerm: true` (the default). |
During auto-recall, both scopes are searched and presented separately — long-term first, then session — so the agent has full context.
- **Session (short-term)** — Scoped to the current conversation via `run_id`. Recalled alongside long-term memories.
- **User (long-term)** — Persistent across all sessions. Default for `memory_add`.
### Multi-Agent Isolation
In multi-agent setups, each agent gets its own memory namespace automatically. Session keys matching `agent:<name>:<uuid>` route memories to `userId:agent:<name>`. Single-agent deployments are unaffected.
All memory tools accept an optional `agentId` parameter for cross-agent queries:
```
memory_search({ query: "user's tech stack", agentId: "researcher" })
```
Each agent gets its own memory namespace automatically via session key routing (`agent:<name>:<uuid>` maps to `userId:agent:<name>`). Single-agent setups are unaffected.
## Agent Tools
Seven tools are available to the agent during conversations:
Eight tools are registered for agent use:
| Tool | Description |
|------|-------------|
| **`memory_search`** | Search memories by natural language query. Supports `scope` (`session`, `long-term`, `all`) and `agentId` filtering. |
| **`memory_add`** | Save a fact to memory. Supports `category`, `importance`, `longTerm`, and `agentId`. |
| **`memory_get`** | Retrieve a specific memory by ID. |
| **`memory_list`** | List stored memories with optional `userId`, `agentId`, and `limit` filters. |
| **`memory_update`** | Update an existing memory's text in place. Preserves edit history. |
| **`memory_delete`** | Delete by ID, search query, or bulk (`all: true`). Requires `confirm: true` for bulk. |
| **`memory_history`** | View the edit history of a specific memory. |
| ---- | ----------- |
| `memory_search` | Search by natural language query. Supports `scope` (`session`, `long-term`, `all`), `categories`, `filters`, and `agentId`. |
| `memory_add` | Store facts. Accepts `text` or `facts` array, `category`, `importance`, `longTerm`, `metadata`. |
| `memory_get` | Retrieve a single memory by ID. |
| `memory_list` | List all memories. Filter by `userId`, `agentId`, `scope`. |
| `memory_update` | Update a memory's text in place. Preserves history. |
| `memory_delete` | Delete by `memoryId`, `query` (search-and-delete), or `all: true` (requires `confirm: true`). |
| `memory_event_list` | List recent background processing events. Platform mode only. |
| `memory_event_status` | Get status of a specific event by ID. Platform mode only. |
## CLI
All commands follow the pattern `openclaw mem0 <command>`.
### Memory Operations
All commands: `openclaw mem0 <command>`.
```bash
# Add a memory
# Memory operations
openclaw mem0 add "User prefers TypeScript over JavaScript"
# Search memories
openclaw mem0 search "what languages does the user know"
openclaw mem0 search "preferences" --scope long-term
openclaw mem0 search "context" --scope session
# Get, list, update, delete
openclaw mem0 get <memory_id>
openclaw mem0 list --user-id alice --top-k 20
openclaw mem0 update <memory_id> "Updated preference text"
openclaw mem0 delete <memory_id>
openclaw mem0 delete --all --user-id alice --confirm
openclaw mem0 import memories.json
# View edit history
openclaw mem0 history <memory_id>
```
### Management
```bash
# Authenticate and configure
# Management
openclaw mem0 init
openclaw mem0 init --api-key <key> --user-id alice
# Check connectivity
openclaw mem0 status
# Manage configuration
openclaw mem0 config show
openclaw mem0 config get api_key
openclaw mem0 config set user_id alice
# Memory consolidation (review, merge, prune)
# Events (platform only)
openclaw mem0 event list
openclaw mem0 event status <event_id>
# Memory consolidation
openclaw mem0 dream
openclaw mem0 dream --dry-run
```
@@ -156,9 +136,9 @@ openclaw mem0 dream --dry-run
### General
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| --- | ---- | ------- | ----------- |
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
| `userId` | `string` | `"default"` | Unique identifier for the user. You define this — it's not found in any dashboard. All memories are scoped to this value. |
| `userId` | `string` | OS username | User identifier. All memories scoped to this value. |
| `autoRecall` | `boolean` | `true` | Inject relevant memories before each turn |
| `autoCapture` | `boolean` | `true` | Extract and store facts after each turn |
| `topK` | `number` | `5` | Max memories returned per recall |
@@ -167,32 +147,26 @@ openclaw mem0 dream --dry-run
### Platform Mode
| 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` | Enable entity graph for relationship tracking |
| `customInstructions` | `string` | *(built-in)* | Custom extraction rules for what to store and how to format |
| `customCategories` | `object` | *(12 defaults)* | Category name to description map for memory tagging |
| `customInstructions` | `string` | *(built-in)* | Custom extraction rules |
| `customCategories` | `object` | *(12 defaults)* | Category name to description map |
### Open-Source Mode
All fields below are optional. Defaults use OpenAI embeddings, in-memory vector store, and OpenAI LLM.
All fields optional. Defaults: `text-embedding-3-small` embeddings, local SQLite vector store (`~/.mem0/vector_store.db`), `gpt-5.4` LLM.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `customPrompt` | `string` | *(built-in)* | Extraction prompt for memory processing |
| --- | ---- | ------- | ----------- |
| `customPrompt` | `string` | *(built-in)* | Extraction prompt |
| `oss.embedder.provider` | `string` | `"openai"` | Embedding provider |
| `oss.embedder.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) |
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider |
| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`) |
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider (see list above) |
| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`, `dbPath`) |
| `oss.llm.provider` | `string` | `"openai"` | LLM provider |
| `oss.llm.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) |
| `oss.historyDbPath` | `string` | — | SQLite path for memory edit history |
| `oss.disableHistory` | `boolean` | `false` | Skip history DB initialization |
Supported providers: `openai`, `anthropic`, `ollama`, `lmstudio`, `qdrant`, `chroma`, and more. See the [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for the full list.
| `oss.historyDbPath` | `string` | — | SQLite path for edit history |
## License
Apache 2.0
[Apache 2.0](LICENSE)
-3
View File
@@ -14,7 +14,6 @@ export interface AddOptions {
infer?: boolean;
expires?: string;
categories?: string[];
enableGraph?: boolean;
}
export interface SearchOptions {
@@ -28,7 +27,6 @@ export interface SearchOptions {
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
enableGraph?: boolean;
}
export interface ListOptions {
@@ -41,7 +39,6 @@ export interface ListOptions {
category?: string;
after?: string;
before?: string;
enableGraph?: boolean;
}
export interface DeleteOptions {
-3
View File
@@ -111,7 +111,6 @@ export class PlatformBackend implements Backend {
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
return (await this._request("POST", "/v1/memories/", {
json: payload,
@@ -171,7 +170,6 @@ export class PlatformBackend implements Backend {
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
@@ -222,7 +220,6 @@ export class PlatformBackend implements Backend {
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/", {
json: payload,
+263 -91
View File
@@ -10,7 +10,7 @@
* - list : List memories with optional filters (--user-id, --agent-id, --top-k)
* - update : Update a memory's text
* - delete : Delete a memory or all memories (--all, --confirm)
* - history : View edit history of a memory
* - import : Import memories from a JSON file
*
* Management:
* - init : Authenticate with Mem0 Platform (email or API key)
@@ -18,6 +18,8 @@
* - config show : Display current plugin configuration
* - config get : Get a single config value
* - config set : Update a plugin config field
* - event list : List recent background events
* - event status: Get status of a specific event
* - dream : Run memory consolidation
*
* Naming conventions match the Python CLI (`mem0 init`, `mem0 search`, etc.)
@@ -35,12 +37,12 @@ import type {
SearchOptions,
} from "../types.ts";
import { loadDreamPrompt } from "../skill-loader.ts";
import { readText } from "../fs-safe.ts";
import type { PluginAuthConfig } from "./config-file.ts";
import {
readPluginAuth,
writePluginAuth,
writePluginConfigField,
getBaseUrl,
OPENCLAW_CONFIG_FILE,
} from "./config-file.ts";
@@ -49,13 +51,14 @@ import {
// ============================================================================
/** Prompt user for input on stderr (keeps stdout clean for piping). */
function promptInput(question: string): Promise<string> {
function promptInput(question: string, prefill?: string): Promise<string> {
const rl = createInterface({ input: process.stdin, output: process.stderr });
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
if (prefill) rl.write(prefill);
});
}
@@ -196,14 +199,13 @@ async function verifyEmailCode(
*/
function saveLoginConfig(
apiKey: string,
baseUrl: string,
userIdFlag?: string,
userEmail?: string,
): void {
const existingAuth = readPluginAuth();
const userId = resolveUserId(userIdFlag, existingAuth.userId);
writePluginAuth({ apiKey, baseUrl, userId, mode: "platform", ...(userEmail && { userEmail }) });
writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) });
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
console.log(` Mode: platform`);
@@ -245,7 +247,8 @@ export function registerCliCommands(
({ program }) => {
const mem0 = program
.command("mem0")
.description("Mem0 memory plugin commands");
.description("Mem0 memory plugin commands")
.configureHelp({ sortSubcommands: false, subcommandTerm: (cmd) => cmd.name() });
// Telemetry: fire event for each CLI subcommand
if (captureCliEvent) {
@@ -278,7 +281,7 @@ export function registerCliCommands(
userId?: string;
}) => {
try {
const baseUrl = getBaseUrl();
const baseUrl = "https://api.mem0.ai";
const existingAuth = readPluginAuth();
const hasExistingConfig = !!(existingAuth.apiKey || existingAuth.mode);
@@ -290,7 +293,7 @@ export function registerCliCommands(
}
const check = await validateApiKey(baseUrl, opts.apiKey);
saveLoginConfig(opts.apiKey, baseUrl, opts.userId, check.userEmail);
saveLoginConfig(opts.apiKey, opts.userId, check.userEmail);
if (hasExistingConfig) {
console.log(
" Existing configuration detected — updated API key (other settings preserved).",
@@ -322,7 +325,7 @@ export function registerCliCommands(
const apiKey = await verifyEmailCode(baseUrl, email, opts.code);
if (!apiKey) return;
saveLoginConfig(apiKey, baseUrl, opts.userId, email);
saveLoginConfig(apiKey, opts.userId, email);
if (hasExistingConfig) {
console.log(
" Existing configuration detected — updated API key (other settings preserved).",
@@ -381,16 +384,12 @@ export function registerCliCommands(
console.log(` User ID: ${existingAuth.userId}`);
if (existingAuth.mode)
console.log(` Mode: ${existingAuth.mode}`);
if (existingAuth.orgId)
console.log(` Org ID: ${existingAuth.orgId}`);
if (existingAuth.projectId)
console.log(` Project: ${existingAuth.projectId}`);
console.log("");
// Validate existing key before asking
if (existingAuth.apiKey) {
const check = await validateApiKey(
existingAuth.baseUrl || baseUrl,
baseUrl,
existingAuth.apiKey,
);
if (check.ok) {
@@ -405,7 +404,7 @@ export function registerCliCommands(
}
const reuse = await promptInput(
" Keep existing configuration? (Y/n): ",
" Keep existing configuration? (y/n): ",
);
if (
reuse === "" ||
@@ -429,7 +428,7 @@ export function registerCliCommands(
console.log(" 2. Enter API key manually");
console.log(" 3. Open-source mode (self-hosted)\n");
const choice = await promptInput(" Choice (1/2/3): ");
const choice = (await promptInput(" Choice (1/2/3): ")) || "1";
if (choice === "1") {
// --- Email interactive flow ---
@@ -461,13 +460,13 @@ export function registerCliCommands(
if (!userIdValue) {
const defaultUid = resolveUserId(undefined, existingAuth.userId);
const uidInput = await promptInput(
` User ID (${defaultUid}): `,
` User ID: `, defaultUid,
);
userIdValue = uidInput || undefined;
userIdValue = uidInput || defaultUid;
}
console.log("");
saveLoginConfig(apiKey, baseUrl, userIdValue, email);
saveLoginConfig(apiKey, userIdValue, email);
console.log(" Authenticated!");
console.log(
" Restart the gateway: openclaw gateway restart\n",
@@ -485,14 +484,14 @@ export function registerCliCommands(
if (!userIdValue2) {
const defaultUid = resolveUserId(undefined, existingAuth.userId);
const uidInput = await promptInput(
` User ID (${defaultUid}): `,
` User ID: `, defaultUid,
);
userIdValue2 = uidInput || undefined;
userIdValue2 = uidInput || defaultUid;
}
console.log("");
const check = await validateApiKey(baseUrl, key);
saveLoginConfig(key, baseUrl, userIdValue2, check.userEmail);
saveLoginConfig(key, userIdValue2, check.userEmail);
if (check.ok) {
console.log(
@@ -545,7 +544,7 @@ export function registerCliCommands(
"\n Skipped. You can add it later via:",
);
console.log(
" openclaw mem0 config set oss.embedder.config.apiKey <key>",
" openclaw mem0 config set embedder_key <key>",
);
console.log(
" Or set OPENAI_API_KEY in your environment.\n",
@@ -557,9 +556,9 @@ export function registerCliCommands(
if (!userIdValue3) {
const defaultUid = resolveUserId(undefined, existingAuth.userId);
const uidInput = await promptInput(
` User ID (${defaultUid}): `,
` User ID: `, defaultUid,
);
userIdValue3 = uidInput || undefined;
userIdValue3 = uidInput || defaultUid;
}
console.log("");
@@ -622,7 +621,6 @@ export function registerCliCommands(
runId?: string,
): SearchOptions => {
const base = buildSearchOptions(userIdOverride, lim, runId);
delete (base as any).source;
base.threshold = 0.3;
return base;
};
@@ -719,7 +717,7 @@ export function registerCliCommands(
: effectiveUserId(getCurrentSessionId());
const result = await provider.add(
[{ role: "user", content: text }],
{ user_id: uid },
{ user_id: uid, source: "OPENCLAW" },
);
const count = result.results?.length ?? 0;
if (count > 0) {
@@ -901,27 +899,6 @@ export function registerCliCommands(
},
);
// ====================================================================
// history (matches: mem0 history <memory_id>)
// ====================================================================
mem0
.command("history")
.description("View edit history of a memory")
.argument("<memory_id>", "Memory ID to view history for")
.action(async (memoryId: string) => {
try {
const entries = await provider.history(memoryId);
if (!entries.length) {
console.log("No history found for this memory.");
return;
}
console.log(JSON.stringify(entries, null, 2));
} catch (err) {
console.error(`History failed: ${String(err)}`);
}
});
// ====================================================================
// status (matches: mem0 status)
// ====================================================================
@@ -967,39 +944,42 @@ export function registerCliCommands(
const CONFIG_KEYS: Record<string, string> = {
// Short aliases (matches Python CLI)
api_key: "apiKey",
email: "userEmail",
base_url: "baseUrl",
user_id: "userId",
org_id: "orgId",
project_id: "projectId",
enable_graph: "enableGraph",
auto_recall: "autoRecall",
auto_capture: "autoCapture",
top_k: "topK",
mode: "mode",
// Dot notation (matches Python CLI: platform.api_key, defaults.user_id)
"platform.api_key": "apiKey",
"platform.base_url": "baseUrl",
"defaults.user_id": "userId",
"defaults.org_id": "orgId",
"defaults.project_id": "projectId",
"defaults.enable_graph": "enableGraph",
"defaults.auto_recall": "autoRecall",
"defaults.auto_capture": "autoCapture",
"defaults.top_k": "topK",
embedder_provider: "oss.embedder.provider",
embedder_model: "oss.embedder.config.model",
embedder_key: "oss.embedder.config.apiKey",
llm_provider: "oss.llm.provider",
llm_model: "oss.llm.config.model",
llm_key: "oss.llm.config.apiKey",
vector_provider: "oss.vectorStore.provider",
vector_host: "oss.vectorStore.config.host",
vector_port: "oss.vectorStore.config.port",
collection_name: "oss.vectorStore.config.collectionName",
vector_db_name: "oss.vectorStore.config.dbname",
vector_db_user: "oss.vectorStore.config.user",
vector_db_path: "oss.vectorStore.config.dbPath",
history_db_path: "oss.historyDbPath",
disable_history: "oss.disableHistory",
};
// Keys that contain secrets — redact in show/get output
const SECRET_KEYS = new Set(["apiKey"]);
const SECRET_KEYS = new Set(["apiKey", "oss.embedder.config.apiKey", "oss.llm.config.apiKey"]);
// Boolean config fields — coerce "true"/"1"/"yes" on set
const BOOLEAN_KEYS = new Set([
"enableGraph",
"autoRecall",
"autoCapture",
"oss.disableHistory",
]);
// Integer config fields — coerce to number on set
const INTEGER_KEYS = new Set(["topK"]);
const INTEGER_KEYS = new Set(["topK", "oss.vectorStore.config.port"]);
/** Resolve a user-facing key to the internal camelCase field name. */
function resolveConfigKey(key: string): string | null {
@@ -1008,15 +988,22 @@ export function registerCliCommands(
/** Read a config value by internal field name. */
function getConfigValue(field: string): unknown {
if (field.startsWith("oss.")) {
const parts = field.split(".");
let current: unknown = cfg.oss;
for (let i = 1; i < parts.length && current != null; i++) {
current = (current as Record<string, unknown>)[parts[i]];
}
return current;
}
const auth = readPluginAuth();
const values: Record<string, unknown> = {
apiKey: auth.apiKey ?? cfg.apiKey,
baseUrl: auth.baseUrl ?? cfg.baseUrl ?? "https://api.mem0.ai",
userId: auth.userId ?? cfg.userId,
orgId: auth.orgId ?? cfg.orgId,
projectId: auth.projectId ?? cfg.projectId,
mode: auth.mode ?? cfg.mode,
enableGraph: cfg.enableGraph,
userEmail: auth.userEmail,
autoRecall: cfg.autoRecall,
autoCapture: cfg.autoCapture,
topK: cfg.topK,
@@ -1045,20 +1032,34 @@ export function registerCliCommands(
.command("show")
.description("Show current configuration")
.action(() => {
// Display order matching Python CLI: platform first, then defaults
const entries: Array<[string, string, string]> = [
["platform.api_key", "apiKey", ""],
["platform.base_url", "baseUrl", ""],
["defaults.user_id", "userId", ""],
["defaults.org_id", "orgId", ""],
["defaults.project_id", "projectId", ""],
["defaults.enable_graph", "enableGraph", ""],
["defaults.auto_recall", "autoRecall", ""],
["defaults.auto_capture", "autoCapture", ""],
["defaults.top_k", "topK", ""],
["mode", "mode", ""],
// Display order: general first, then mode-specific
const entries: Array<[string, string]> = [
["mode", "mode"],
["user_id", "userId"],
["auto_recall", "autoRecall"],
["auto_capture", "autoCapture"],
["top_k", "topK"],
];
if (cfg.mode === "platform") {
entries.push(
["api_key", "apiKey"],
["email", "userEmail"],
);
} else {
entries.push(
["embedder_provider", "oss.embedder.provider"],
["embedder_model", "oss.embedder.config.model"],
["embedder_key", "oss.embedder.config.apiKey"],
["llm_provider", "oss.llm.provider"],
["llm_model", "oss.llm.config.model"],
["llm_key", "oss.llm.config.apiKey"],
["vector_provider", "oss.vectorStore.provider"],
["history_db_path", "oss.historyDbPath"],
["disable_history", "oss.disableHistory"],
);
}
// Calculate column widths
const maxKeyLen = Math.max(
...entries.map(([k]) => k.length),
@@ -1086,17 +1087,21 @@ export function registerCliCommands(
console.log(" openclaw mem0 config set <key> <value>");
console.log("");
console.log(" Examples:");
console.log(" openclaw mem0 config set mode open-source");
console.log(" openclaw mem0 config set mode platform");
console.log(" openclaw mem0 config set auto_recall false");
console.log(" openclaw mem0 config set top_k 10");
if (cfg.mode === "platform") {
console.log(" openclaw mem0 config set mode open-source");
console.log(" openclaw mem0 config set auto_recall false");
} else {
console.log(" openclaw mem0 config set vector_provider qdrant");
console.log(" openclaw mem0 config set llm_model gpt-4o");
console.log(" openclaw mem0 config set embedder_provider openai");
}
console.log("");
});
configCmd
.command("get")
.description("Get a config value")
.argument("<key>", "Config key (e.g. user_id, platform.api_key)")
.argument("<key>", "Config key (e.g. user_id, api_key, llm_model)")
.action((key: string) => {
const field = resolveConfigKey(key);
if (!field) {
@@ -1112,7 +1117,7 @@ export function registerCliCommands(
configCmd
.command("set")
.description("Set a config value")
.argument("<key>", "Config key (e.g. user_id, platform.api_key)")
.argument("<key>", "Config key (e.g. user_id, api_key, llm_model)")
.argument("<value>", "New value")
.action((key: string, rawValue: string) => {
const field = resolveConfigKey(key);
@@ -1139,12 +1144,179 @@ export function registerCliCommands(
value = parsed;
}
writePluginAuth({ [field]: value } as PluginAuthConfig);
// Nested OSS fields use dot-path writer; flat fields use auth writer
if (field.startsWith("oss.")) {
writePluginConfigField(field.split("."), value);
} else {
writePluginAuth({ [field]: value } as PluginAuthConfig);
}
console.log(
`${key} = ${displayValue(field, value)}`,
);
});
// ====================================================================
// import (matches: mem0 import <file>)
// ====================================================================
mem0
.command("import")
.description("Import memories from a JSON file")
.argument("<file>", "Path to JSON file containing memories")
.option("--user-id <userId>", "Override user ID for all imported memories")
.option("--agent-id <agentId>", "Override agent ID for all imported memories")
.action(
async (
file: string,
opts: { userId?: string; agentId?: string },
) => {
try {
let data: unknown;
try {
data = JSON.parse(readText(file));
} catch (err) {
console.error(`Failed to read file: ${String(err)}`);
return;
}
const items = Array.isArray(data) ? data : [data];
let added = 0;
let failed = 0;
for (const item of items) {
const content =
item?.memory ?? item?.text ?? item?.content ?? "";
if (!content) {
failed++;
continue;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? item?.user_id ?? cfg.userId,
agentId: opts.agentId ?? item?.agent_id,
metadata: item?.metadata,
});
added++;
} catch {
failed++;
}
}
console.log(`Imported ${added} memories.`);
if (failed) {
console.error(`${failed} memories failed to import.`);
}
} catch (err) {
console.error(`Import failed: ${String(err)}`);
}
},
);
// ====================================================================
// event (matches: mem0 event list, mem0 event status <id>)
// ====================================================================
const eventCmd = mem0
.command("event")
.description("Manage background processing events");
eventCmd
.command("list")
.description("List recent background events")
.action(async () => {
try {
if (!backend || cfg.mode === "open-source") {
console.log("Event tracking is only available in platform mode.");
return;
}
const results = await backend.listEvents();
if (!results.length) {
console.log("No events found.");
return;
}
// Table header
const header = [
"Event ID".padEnd(36),
"Type".padEnd(14),
"Status".padEnd(12),
"Latency".padStart(10),
"Created".padEnd(20),
].join(" ");
console.log(header);
console.log("-".repeat(header.length));
for (const ev of results) {
const evId = String(ev.id ?? "");
const evType = String(ev.event_type ?? "—").padEnd(14);
const status = String(ev.status ?? "—").padEnd(12);
const latency = typeof ev.latency === "number"
? `${Math.round(ev.latency as number)}ms`
: "—";
const created = String(ev.created_at ?? "—")
.slice(0, 19)
.replace("T", " ");
console.log(
`${evId.padEnd(36)} ${evType} ${status} ${latency.padStart(10)} ${created}`,
);
}
console.log(`\n${results.length} event${results.length !== 1 ? "s" : ""}`);
} catch (err) {
console.error(`Failed to list events: ${String(err)}`);
}
});
eventCmd
.command("status")
.description("Get status of a specific background event")
.argument("<event_id>", "Event ID to check")
.action(async (eventId: string) => {
try {
if (!backend || cfg.mode === "open-source") {
console.log("Event tracking is only available in platform mode.");
return;
}
const ev = await backend.getEvent(eventId);
const status = String(ev.status ?? "—");
const evType = String(ev.event_type ?? "—");
const latency = typeof ev.latency === "number"
? `${Math.round(ev.latency as number)}ms`
: "—";
const created = String(ev.created_at ?? "—")
.slice(0, 19)
.replace("T", " ");
const updated = String(ev.updated_at ?? "—")
.slice(0, 19)
.replace("T", " ");
console.log(`Event ID: ${eventId}`);
console.log(`Type: ${evType}`);
console.log(`Status: ${status}`);
console.log(`Latency: ${latency}`);
console.log(`Created: ${created}`);
console.log(`Updated: ${updated}`);
const results = ev.results as Record<string, unknown>[] | undefined;
if (results && Array.isArray(results) && results.length) {
console.log(`\nResults (${results.length}):`);
for (const r of results) {
const memId = String(r.id ?? "").slice(0, 8);
const data = r.data as Record<string, unknown> | undefined;
const memory = data?.memory ?? "";
const evName = String(r.event ?? "");
const user = String(r.user_id ?? "");
let detail = `${evName} ${memory}`;
if (user) detail += ` (user_id=${user})`;
console.log(` · ${detail} (${memId})`);
}
}
} catch (err) {
console.error(`Failed to get event: ${String(err)}`);
}
});
// ====================================================================
// help (matches: mem0 help, mem0 help --json)
// ====================================================================
@@ -1156,20 +1328,21 @@ export function registerCliCommands(
.action((opts: { json?: boolean }) => {
const commands = {
memory: {
add: "Add a memory from text, messages, or stdin",
search: "Query your memory store — semantic, keyword, or hybrid retrieval",
add: "Add a memory from text, messages, or stdin",
get: "Get a specific memory by ID",
list: "List memories with optional filters",
update: "Update a memory's text or metadata",
delete: "Delete a memory, all memories, or an entity",
history: "View edit history of a memory",
import: "Import memories from a JSON file",
},
management: {
init: "Interactive setup wizard for mem0 CLI",
status: "Check connectivity and authentication",
help: "Show help. Use --json for machine-readable output (for LLM agents)",
config: "Manage mem0 configuration (show, get, set)",
event: "Manage background processing events (list, status)",
dream: "Run memory consolidation (review, merge, prune)",
help: "Show help. Use --json for machine-readable output (for LLM agents)",
},
};
@@ -1283,9 +1456,8 @@ export function registerCliCommands(
});
},
{
commands: ["mem0"],
descriptors: [
{ name: "mem0", description: "Mem0 memory plugin commands" },
{ name: "mem0", description: "Mem0 memory plugin commands", hasSubcommands: true },
],
},
);
+2 -6
View File
@@ -29,14 +29,12 @@ export interface PluginAuthConfig {
apiKey?: string;
baseUrl?: string;
userId?: string;
orgId?: string;
projectId?: string;
userEmail?: string;
mode?: string;
enableGraph?: boolean;
autoRecall?: boolean;
autoCapture?: boolean;
topK?: number;
anonymousTelemetryId?: string;
}
// ============================================================================
@@ -76,14 +74,12 @@ export function readPluginAuth(): PluginAuthConfig {
apiKey: (cfg.apiKey ?? cfg.api_key) as string | undefined,
baseUrl: (cfg.baseUrl ?? cfg.base_url) as string | undefined,
userId: (cfg.userId ?? cfg.user_id) as string | undefined,
orgId: (cfg.orgId ?? cfg.org_id) as string | undefined,
projectId: (cfg.projectId ?? cfg.project_id) as string | undefined,
userEmail: (cfg.userEmail ?? cfg.user_email) as string | undefined,
mode: cfg.mode as string | undefined,
enableGraph: cfg.enableGraph as boolean | undefined,
autoRecall: cfg.autoRecall as boolean | undefined,
autoCapture: cfg.autoCapture as boolean | undefined,
topK: cfg.topK as number | undefined,
anonymousTelemetryId: cfg.anonymousTelemetryId as string | undefined,
};
}
+10 -14
View File
@@ -19,8 +19,6 @@ import type { Mem0Config, Mem0Mode } from "./types.ts";
export interface FileConfig {
apiKey?: string;
baseUrl?: string;
orgId?: string;
projectId?: string;
}
// ============================================================================
@@ -151,14 +149,12 @@ const ALLOWED_KEYS = [
"apiKey",
"baseUrl",
"userId",
"orgId",
"projectId",
"userEmail",
"autoCapture",
"autoRecall",
"customInstructions",
"customCategories",
"customPrompt",
"enableGraph",
"searchThreshold",
"topK",
"oss",
@@ -184,6 +180,15 @@ export const mem0ConfigSchema = {
assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-mem0 config");
// Only two modes: "platform" (default) or "open-source"
if (
typeof cfg.mode === "string" &&
cfg.mode !== "platform" &&
cfg.mode !== "open-source"
) {
console.warn(
`[mem0] Unknown mode "${cfg.mode}" — expected "platform" or "open-source". Defaulting to "platform".`,
);
}
const mode: Mem0Mode =
cfg.mode === "open-source" ? "open-source" : "platform";
@@ -192,15 +197,9 @@ export const mem0ConfigSchema = {
typeof cfg.apiKey === "string" ? cfg.apiKey : undefined;
let resolvedBaseUrl =
typeof cfg.baseUrl === "string" ? cfg.baseUrl : undefined;
let resolvedOrgId = typeof cfg.orgId === "string" ? cfg.orgId : undefined;
let resolvedProjectId =
typeof cfg.projectId === "string" ? cfg.projectId : undefined;
if (mode === "platform" && !resolvedApiKey && fileConfig) {
if (fileConfig.apiKey) resolvedApiKey = fileConfig.apiKey;
if (fileConfig.baseUrl) resolvedBaseUrl = fileConfig.baseUrl;
if (!resolvedOrgId && fileConfig.orgId) resolvedOrgId = fileConfig.orgId;
if (!resolvedProjectId && fileConfig.projectId)
resolvedProjectId = fileConfig.projectId;
}
// Platform mode requires apiKey — but don't throw on missing config.
@@ -227,8 +226,6 @@ export const mem0ConfigSchema = {
return "default";
}
})(),
orgId: resolvedOrgId,
projectId: resolvedProjectId,
autoCapture: cfg.autoCapture !== false,
autoRecall: cfg.autoRecall !== false,
customInstructions:
@@ -245,7 +242,6 @@ export const mem0ConfigSchema = {
typeof cfg.customPrompt === "string"
? cfg.customPrompt
: DEFAULT_CUSTOM_INSTRUCTIONS,
enableGraph: cfg.enableGraph === true,
searchThreshold:
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5,
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
+7
View File
@@ -34,3 +34,10 @@ export function mkdirp(dirPath: string, mode?: number): void {
export function unlink(filePath: string): void {
fs.unlinkSync(filePath);
}
export function bootstrapTelemetryFlag(): void {
const val = process.env.MEM0_TELEMETRY;
if (val !== undefined) {
(globalThis as any).__mem0_telemetry_override = val;
}
}
+64 -26
View File
@@ -5,8 +5,8 @@
* and the open-source self-hosted SDK. Uses the official `mem0ai` package.
*
* Features:
* - 7 core tools: memory_search, memory_add, memory_get, memory_list,
* memory_update, memory_delete, memory_history
* - 6 core tools: memory_search, memory_add, memory_get, memory_list,
* memory_update, memory_delete
* - 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
@@ -16,6 +16,7 @@
* - Dual mode: platform or open-source (self-hosted)
*/
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type {
@@ -56,6 +57,9 @@ 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();
// ============================================================================
// Re-exports (for tests and external consumers)
@@ -87,12 +91,10 @@ export { createProvider } from "./providers.ts";
// Plugin Definition
// ============================================================================
const memoryPlugin = {
const memoryPlugin = definePluginEntry({
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) {
// Read auth from openclaw.json plugin config (picks up post-startup login).
@@ -101,15 +103,21 @@ const memoryPlugin = {
const fileConfig: FileConfig = {
apiKey: pluginAuth.apiKey,
baseUrl: pluginAuth.baseUrl,
orgId: pluginAuth.orgId,
projectId: pluginAuth.projectId,
};
const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig);
// Telemetry context bound to this plugin instance's config
const telemetryCtx = { apiKey: cfg.apiKey, mode: cfg.mode, skillsActive: false };
const telemetryCtx = {
apiKey: cfg.apiKey,
mode: cfg.mode,
skillsActive: false,
};
const _captureEvent = (event: string, props?: Record<string, unknown>) => {
try { captureEvent(event, props, telemetryCtx); } catch { /* silently swallow */ }
try {
captureEvent(event, props, telemetryCtx);
} catch {
/* silently swallow */
}
};
if (cfg.needsSetup) {
@@ -132,7 +140,7 @@ const memoryPlugin = {
(id: string) => `${cfg.userId}:agent:${id}`,
() => ({ user_id: cfg.userId, top_k: cfg.topK }),
() => undefined,
(cmd: string) => _captureEvent(`openclaw.${cmd}`, { command: cmd }),
(cmd: string) => _captureEvent(`openclaw.cli.${cmd}`, { command: cmd }),
);
api.registerService({
@@ -180,7 +188,7 @@ const memoryPlugin = {
});
api.logger.info(
`openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, graph: ${cfg.enableGraph}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`,
`openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`,
);
// Helper: build add options
@@ -195,7 +203,6 @@ const memoryPlugin = {
};
if (runId) opts.run_id = runId;
if (cfg.mode === "platform") {
opts.enable_graph = cfg.enableGraph;
opts.output_format = "v1.1";
}
return opts;
@@ -231,6 +238,7 @@ const memoryPlugin = {
api,
provider,
cfg,
backend,
resolveUserId: _resolveUserId,
effectiveUserId: _effectiveUserId,
agentUserId: _agentUserId,
@@ -239,7 +247,10 @@ const memoryPlugin = {
getCurrentSessionId: () => currentSessionId,
skillsActive,
captureToolEvent: (toolName: string, props: Record<string, unknown>) => {
_captureEvent(`openclaw.tool.${toolName}`, { tool_name: toolName, ...props });
_captureEvent(`openclaw.tool.${toolName}`, {
tool_name: toolName,
...props,
});
},
};
registerAllTools(toolDeps);
@@ -298,7 +309,7 @@ const memoryPlugin = {
},
});
},
};
});
// ============================================================================
// Lifecycle Hook Registration
@@ -325,7 +336,10 @@ function registerHooks(
getStateDir: () => string | undefined;
},
skillsActive: boolean = false,
_captureEvent: (event: string, props?: Record<string, unknown>) => void = () => {},
_captureEvent: (
event: string,
props?: Record<string, unknown>,
) => void = () => {},
) {
// ========================================================================
// SKILLS MODE: Agentic memory via before_prompt_build
@@ -352,15 +366,11 @@ function registerHooks(
return;
}
// Skip recall for system/bootstrap prompts. These are OpenClaw internal
// commands (/new, /reset) that contain system instructions, not user queries.
// Sending them to mem0 search wastes API calls and returns noise.
const promptLower = event.prompt.toLowerCase();
const isSystemPrompt =
promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session");
if (isSystemPrompt) {
api.logger.info(
@@ -469,7 +479,10 @@ function registerHooks(
"\n</auto-dream>";
// Track which session triggered dream (session-keyed, not global)
dreamSessionId = sessionId;
_captureEvent("openclaw.hook.dream", { phase: "triggered", memory_count: memCount });
_captureEvent("openclaw.hook.dream", {
phase: "triggered",
memory_count: memCount,
});
api.logger.info(
`openclaw-mem0: auto-dream triggered (${memCount} memories, gate passed)`,
);
@@ -542,7 +555,10 @@ function registerHooks(
if (writeToolUsed) {
releaseDreamLock(stateDir);
recordDreamCompletion(stateDir);
_captureEvent("openclaw.hook.dream", { phase: "completed", write_tools_used: true });
_captureEvent("openclaw.hook.dream", {
phase: "completed",
write_tools_used: true,
});
api.logger.info(
"openclaw-mem0: auto-dream completed (verified write tool usage), lock released",
);
@@ -579,11 +595,11 @@ function registerHooks(
// Track last seen session ID to detect actual new sessions (not every turn)
let lastRecallSessionId: string | undefined;
// Auto-recall: inject relevant memories before agent starts
// Auto-recall: inject relevant memories before prompt is built
if (cfg.autoRecall) {
const RECALL_TIMEOUT_MS = 8_000;
api.on("before_agent_start", async (event: any, ctx: any) => {
api.on("before_prompt_build", async (event: any, ctx: any) => {
if (!event.prompt || event.prompt.length < 5) return;
// Skip non-interactive triggers (cron, heartbeat, automation)
@@ -596,13 +612,11 @@ function registerHooks(
return;
}
// Skip recall for system/bootstrap prompts to save API calls
const promptLower = event.prompt.toLowerCase();
const isSystemPrompt =
promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session");
if (isSystemPrompt) {
api.logger.info(
@@ -772,6 +786,27 @@ function registerHooks(
// Update shared state for tools (best-effort — tools don't have ctx)
if (sessionId) session.setCurrentSessionId(sessionId);
const MEMORY_MUTATE_TOOLS = new Set([
"memory_add",
"memory_update",
"memory_delete",
]);
const agentUsedMemoryTool = event.messages.some((msg: any) => {
if (msg?.role !== "assistant" || !Array.isArray(msg?.content))
return false;
return msg.content.some(
(block: any) =>
(block?.type === "tool_use" || block?.type === "toolCall") &&
MEMORY_MUTATE_TOOLS.has(block.name),
);
});
if (agentUsedMemoryTool) {
api.logger.info(
"openclaw-mem0: skipping auto-capture — agent already used memory tools this turn",
);
return;
}
// --- Build capture payload synchronously (cheap), then fire-and-forget ---
// Patterns indicating an assistant message contains a summary of
@@ -831,7 +866,10 @@ function registerHooks(
if (!textContent) continue;
}
// Strip OpenClaw sender metadata prefix (prevents storing TUI identity as memory)
if (textContent.includes("Sender") && textContent.includes("untrusted metadata")) {
if (
textContent.includes("Sender") &&
textContent.includes("untrusted metadata")
) {
textContent = textContent
.replace(
/Sender\s*\(untrusted metadata\):\s*```json[\s\S]*?```\s*/gi,
+30 -3
View File
@@ -9,19 +9,46 @@ declare module "openclaw/plugin-sdk" {
};
resolvePath(p: string): string;
registerTool(
definition: Record<string, unknown>,
metadata?: Record<string, unknown>,
definition: {
name: string;
description: string;
parameters: unknown;
execute: (
toolCallId: string,
params: Record<string, unknown>,
) => Promise<{ content: Array<{ type: string; text: string }>; [key: string]: unknown }>;
[key: string]: unknown;
},
metadata?: { optional?: boolean; [key: string]: unknown },
): void;
on(event: string, handler: (event: any, ctx: any) => any): void;
registerCli(
handler: (context: { program: any }) => void,
options?: Record<string, unknown>,
): void;
registerCommand?(definition: Record<string, unknown>): void;
registerService(service: {
id: string;
start: () => void;
start: (...args: any[]) => void;
stop: () => void;
}): void;
[key: string]: unknown;
}
}
declare module "openclaw/plugin-sdk/plugin-entry" {
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
export interface PluginEntry {
id: string;
name: string;
description?: string;
register(api: OpenClawPluginApi): void;
}
export function definePluginEntry<T extends PluginEntry>(entry: T): T;
}
declare module "openclaw/plugin-sdk/core" {
export * from "openclaw/plugin-sdk";
}
+20 -35
View File
@@ -1,20 +1,35 @@
{
"id": "openclaw-mem0",
"name": "Memory (Mem0)",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source. Injects recalled memories into agent context (auto-recall) and extracts facts after each turn (auto-capture). Both configurable via autoRecall/autoCapture settings.",
"version": "1.0.5",
"kind": "memory",
"skills": ["skills"],
"contracts": {
"tools": [
"memory_search", "memory_add", "memory_get", "memory_list",
"memory_update", "memory_delete", "memory_history"
"memory_update", "memory_delete", "memory_event_list", "memory_event_status"
]
},
"providerAuthEnvVars": {
"mem0": ["MEM0_API_KEY"],
"mem0-oss-openai": ["OPENAI_API_KEY"],
"mem0-oss-anthropic": ["ANTHROPIC_API_KEY"]
"openclaw-mem0-oss": ["OPENAI_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "mem0",
"method": "api-key",
"choiceId": "mem0-api-key",
"choiceLabel": "Mem0 API key",
"choiceHint": "Required for platform mode. Get your key at https://app.mem0.ai/dashboard/api-keys",
"groupId": "mem0",
"groupLabel": "Mem0",
"optionKey": "apiKey",
"cliFlag": "--mem0-api-key",
"cliOption": "--mem0-api-key <key>",
"cliDescription": "Mem0 platform API key"
}
],
"uiHints": {
"mode": {
"label": "Mode",
@@ -26,27 +41,11 @@
"placeholder": "m0-...",
"help": "Platform mode only. Use a SecretRef ({\"source\":\"env\",\"provider\":\"default\",\"id\":\"MEM0_API_KEY\"}) or ${MEM0_API_KEY} instead of storing the key directly."
},
"baseUrl": {
"label": "API Base URL",
"placeholder": "https://api.mem0.ai",
"advanced": true,
"help": "Override the Mem0 API base URL. Only needed for custom deployments."
},
"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"
@@ -70,10 +69,6 @@
"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",
@@ -109,16 +104,10 @@
"apiKey": {
"type": "string"
},
"baseUrl": {
"type": "string"
},
"userId": {
"type": "string"
},
"orgId": {
"type": "string"
},
"projectId": {
"userEmail": {
"type": "string"
},
"autoCapture": {
@@ -139,9 +128,6 @@
"customPrompt": {
"type": "string"
},
"enableGraph": {
"type": "boolean"
},
"searchThreshold": {
"type": "number"
},
@@ -197,7 +183,6 @@
"properties": {
"enabled": { "type": "boolean" },
"importanceThreshold": { "type": "number" },
"enableGraph": { "type": "boolean" },
"credentialPatterns": { "type": "array", "items": { "type": "string" } }
}
},
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "1.0.4",
"version": "1.0.6",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
@@ -42,12 +42,12 @@
"./dist/index.js"
],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
"pluginApi": ">=2026.3.28",
"minGatewayVersion": ">=2026.3.28"
},
"build": {
"openclawVersion": "2026.3.24-beta.2",
"pluginSdkVersion": "2026.3.24-beta.2"
"openclawVersion": "2026.4.1",
"pluginSdkVersion": "2026.4.1"
}
},
"devDependencies": {
+120 -56
View File
@@ -81,8 +81,7 @@ class PlatformProvider implements Mem0Provider {
constructor(
private readonly apiKey: string,
private readonly orgId?: string,
private readonly projectId?: string,
private readonly baseUrl?: string,
) {}
private async ensureClient(): Promise<void> {
@@ -97,11 +96,13 @@ class PlatformProvider implements Mem0Provider {
private async _init(): Promise<void> {
const { default: MemoryClient } = await import("mem0ai");
const opts: { apiKey: string; org_id?: string; project_id?: string } = {
const opts: {
apiKey: string;
host?: string;
} = {
apiKey: this.apiKey,
};
if (this.orgId) opts.org_id = this.orgId;
if (this.projectId) opts.project_id = this.projectId;
if (this.baseUrl) opts.host = this.baseUrl;
this.client = new MemoryClient(opts);
}
@@ -116,7 +117,6 @@ class PlatformProvider implements Mem0Provider {
opts.custom_instructions = options.custom_instructions;
if (options.custom_categories)
opts.custom_categories = options.custom_categories;
if (options.enable_graph) opts.enable_graph = options.enable_graph;
if (options.output_format) opts.output_format = options.output_format;
if (options.source) opts.source = options.source;
// Agentic harness: direct storage bypass
@@ -133,20 +133,11 @@ class PlatformProvider implements Mem0Provider {
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
await this.ensureClient();
// Base filters: always scope by user_id, optionally by run_id
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) baseFilters.run_id = options.run_id;
// Merge agent-provided filters (created_at ranges, metadata, etc.)
// with base filters. Agent filters extend, never override user scoping.
const mergedFilters = options.filters
? { AND: [baseFilters, options.filters] }
: baseFilters;
const opts: Record<string, unknown> = {
api_version: "v2",
filters: mergedFilters,
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)
@@ -155,6 +146,14 @@ class PlatformProvider implements Mem0Provider {
if (options.filter_memories != null)
opts.filter_memories = options.filter_memories;
if (options.categories != null) opts.categories = options.categories;
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) baseFilters.run_id = options.run_id;
if (options.filters) {
opts.filters = { AND: [baseFilters, options.filters] };
} else {
opts.filters = baseFilters;
}
const results = await this.client.search(query, opts);
return normalizeSearchResults(results);
@@ -168,10 +167,16 @@ class PlatformProvider implements Mem0Provider {
async getAll(options: ListOptions): Promise<MemoryItem[]> {
await this.ensureClient();
const opts: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) opts.run_id = options.run_id;
const opts: Record<string, unknown> = {
api_version: "v2",
user_id: options.user_id,
filters: { user_id: options.user_id },
};
if (options.run_id) {
opts.run_id = options.run_id;
(opts.filters as Record<string, unknown>).run_id = options.run_id;
}
if (options.page_size != null) opts.page_size = options.page_size;
if (options.source) opts.source = options.source;
const results = await this.client.getAll(opts);
if (Array.isArray(results)) return results.map(normalizeMemoryItem);
@@ -196,9 +201,7 @@ class PlatformProvider implements Mem0Provider {
await this.client.deleteAll({ user_id: userId });
}
async history(
memoryId: string,
): Promise<
async history(memoryId: string): Promise<
Array<{
id: string;
old_memory: string;
@@ -237,15 +240,53 @@ class OSSProvider implements Mem0Provider {
return this.initPromise;
}
private async _init(): Promise<void> {
const { Memory } = await import("mem0ai/oss");
private _buildConfig(disableHistory = false): Record<string, unknown> {
const config: Record<string, unknown> = { version: "v1.1" };
if (this.ossConfig?.embedder) config.embedder = this.ossConfig.embedder;
const defaultEmbedder = {
provider: "openai",
config: { model: "text-embedding-3-small" },
};
const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } };
const stripEmpty = (obj: Record<string, unknown>) => {
const out = { ...obj };
for (const k of Object.keys(out)) {
if (out[k] === "") delete out[k];
}
return out;
};
if (this.ossConfig?.embedder) {
const ec = stripEmpty(this.ossConfig.embedder.config ?? {});
if (ec.host && !ec.url) {
ec.url = ec.host;
delete ec.host;
}
config.embedder = {
provider: this.ossConfig.embedder.provider || defaultEmbedder.provider,
config: { ...defaultEmbedder.config, ...ec },
};
} else {
config.embedder = defaultEmbedder;
}
if (this.ossConfig?.llm) {
const lc = stripEmpty(this.ossConfig.llm.config ?? {});
if (lc.host && !lc.url) {
lc.url = lc.host;
delete lc.host;
}
config.llm = {
provider: this.ossConfig.llm.provider || defaultLlm.provider,
config: { ...defaultLlm.config, ...lc },
};
} else {
config.llm = defaultLlm;
}
if (this.ossConfig?.vectorStore)
config.vectorStore = this.ossConfig.vectorStore;
if (this.ossConfig?.llm) config.llm = this.ossConfig.llm;
config.vectorStore = { ...this.ossConfig.vectorStore };
if (this.ossConfig?.historyDbPath) {
const dbPath = this.resolvePath
@@ -254,42 +295,61 @@ class OSSProvider implements Mem0Provider {
config.historyDbPath = dbPath;
}
if (this.ossConfig?.disableHistory) {
if (disableHistory || this.ossConfig?.disableHistory) {
config.disableHistory = true;
}
if (this.customPrompt) config.customPrompt = this.customPrompt;
return config;
}
private async _init(): Promise<void> {
const mod = await import("mem0ai/oss");
const Memory = mod.Memory;
for (const cls of ["PGVector", "RedisDB", "Qdrant"]) {
const VectorCls = (mod as any)[cls];
if (!VectorCls || VectorCls.prototype.__patched) continue;
const origInit = VectorCls.prototype.initialize;
VectorCls.prototype.initialize = function (this: any) {
if (!this.config?.embeddingModelDims && this.config?.dimension) {
this.config.embeddingModelDims = this.config.dimension;
}
// Qdrant reads this.dimension directly
if (!this.dimension && this.config?.dimension) {
this.dimension = this.config.dimension;
}
// Skip premature constructor call when dimensions unknown
const dims = this.config?.embeddingModelDims ?? this.dimension;
if (!dims) return Promise.resolve();
// Run the real initialize only once
if (!this._initializePromise) {
this._initializePromise = origInit.call(this);
}
return this._initializePromise;
};
VectorCls.prototype.__patched = true;
}
let mem: any;
try {
this.memory = new Memory(config);
mem = new Memory(this._buildConfig());
} catch (err) {
// If initialization fails (e.g. native SQLite binding resolution under
// jiti), retry with history disabled — the history DB is the most common
// source of native-binding failures and is not required for core
// memory operations.
if (!config.disableHistory) {
// If constructor fails (e.g. native SQLite binding under jiti/Docker),
// retry with a FRESH config that has history disabled.
if (!this.ossConfig?.disableHistory) {
console.warn(
"[mem0] Memory initialization failed, retrying with history disabled:",
err instanceof Error ? err.message : err,
);
config.disableHistory = true;
this.memory = new Memory(config);
mem = new Memory(this._buildConfig(true));
} else {
throw err;
}
}
// Force the SDK's internal auto-initialization to complete now.
// Without this, concurrent method calls (e.g. auto-recall + search)
// both trigger _autoInitialize() simultaneously, causing PGVector's
// pg client to call connect() twice → "Client has already been
// connected" crash. (#4638)
try {
await this.memory.getAll({ userId: "__mem0_warmup__" });
} catch {
// Warmup errors are non-fatal — the SDK may still work for
// subsequent calls once its internal state settles.
}
await mem.getAll({ userId: "__mem0_warmup__" });
this.memory = mem;
}
async add(
@@ -383,9 +443,7 @@ class OSSProvider implements Mem0Provider {
await this.memory.deleteAll({ userId });
}
async history(
memoryId: string,
): Promise<
async history(memoryId: string): Promise<
Array<{
id: string;
old_memory: string;
@@ -398,8 +456,12 @@ class OSSProvider implements Mem0Provider {
try {
const result = await this.memory.history(memoryId);
return Array.isArray(result) ? result : [];
} catch {
// OSS may not support history depending on config
} catch (err) {
// OSS may not support history depending on config (e.g. disableHistory)
console.warn(
"[mem0] OSS history() failed:",
err instanceof Error ? err.message : err,
);
return [];
}
}
@@ -419,7 +481,7 @@ export function createProvider(
);
}
return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId);
return new PlatformProvider(cfg.apiKey!, cfg.baseUrl);
}
// ============================================================================
@@ -444,12 +506,12 @@ export function providerToBackend(
msgs as Array<{ role: string; content: string }>,
{
user_id: opts.userId ?? userId,
source: "OPENCLAW",
...(opts.runId && { run_id: opts.runId }),
...(opts.metadata && { metadata: opts.metadata }),
...(opts.immutable && { immutable: true }),
...(opts.infer === false && { infer: false }),
...(opts.expires && { expiration_date: opts.expires }),
...(opts.enableGraph && { enable_graph: true }),
},
);
return result as unknown as Record<string, unknown>;
@@ -463,6 +525,7 @@ export function providerToBackend(
keyword_search: opts.keyword,
reranking: opts.rerank,
filters: opts.filters,
source: "OPENCLAW",
});
return results as unknown as Record<string, unknown>[];
},
@@ -476,6 +539,7 @@ export function providerToBackend(
const items = await provider.getAll({
user_id: opts.userId ?? userId,
page_size: opts.pageSize,
source: "OPENCLAW",
});
return items as unknown as Record<string, unknown>[];
},
+1
View File
@@ -260,6 +260,7 @@ export async function recall(
threshold,
keyword_search: recallConfig.keywordSearch !== false, // Default on
reranking: recallConfig.rerank !== false, // Default on
source: "OPENCLAW",
};
if (recallConfig.filterMemories) {
searchOpts.filter_memories = true;
+1 -4
View File
@@ -7,7 +7,7 @@ description: >
Also triggers automatically after sufficient activity (configurable).
user-invocable: true
metadata:
{"openclaw": {"emoji": "💤"}}
{"openclaw": {"emoji": "💤", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}}
---
# Memory Consolidation
@@ -34,9 +34,6 @@ Identify which memories need action. Use the tools to investigate.
**Search for recent additions:**
Call `memory_search` with a `created_at` filter to find memories added since the last consolidation. These are the most likely to need merging or cleanup.
**Check edit history on suspicious entries:**
If a memory looks like it was updated multiple times or has contradictory content, call `memory_history` on it to understand how it evolved.
**Classify each target into one of these actions:**
- DELETE: contains credentials, expired by TTL, pure noise, raw tool output, standalone timestamps
- MERGE: two or more memories express the same fact in different words, or a series tracks incremental changes to the same entity
+5 -6
View File
@@ -1,14 +1,13 @@
---
name: memory-triage
description: >
ALWAYS USE THIS SKILL. Persistent long-term memory protocol powered by mem0.
After EVERY response, evaluate the conversation for durable facts worth storing
via memory_add. Handles identity, preferences, decisions, configurations, rules,
projects, and relationships. Use memory_add (NOT workspace files) for all user facts.
Read this skill at the start of every session.
Persistent long-term memory protocol powered by mem0.
Evaluate conversations for durable facts worth storing via memory_add.
Handles identity, preferences, decisions, configurations, rules,
projects, and relationships. Loaded by the openclaw-mem0 plugin when skills mode is active.
user-invocable: false
metadata:
{"openclaw": {"always": true, "emoji": "🧠"}}
{"openclaw": {"always": false, "emoji": "🧠", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}}
---
# Memory Protocol
+387 -9
View File
@@ -4,9 +4,18 @@
* 2. initPromise poisoning fix (retry after failure)
* 3. Graceful SQLite fallback in OSSProvider
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mem0ConfigSchema, createProvider } from "./index.ts";
/** Stub vector-store classes required by OSSProvider._init's patching loop. */
function vectorStubs() {
return {
PGVector: class { initialize() { return Promise.resolve(); } },
RedisDB: class { initialize() { return Promise.resolve(); } },
Qdrant: class { initialize() { return Promise.resolve(); } },
};
}
// ---------------------------------------------------------------------------
// 1. Config: disableHistory passthrough
// ---------------------------------------------------------------------------
@@ -60,6 +69,7 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
beforeEach(() => {
capturedConfig = undefined;
memoryCallCount = 0;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
@@ -81,9 +91,14 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("passes disableHistory: true to Memory when configured", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -130,6 +145,7 @@ describe("OSSProvider — initPromise retry after failure", () => {
beforeEach(() => {
callCount = 0;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
@@ -154,9 +170,14 @@ describe("OSSProvider — initPromise retry after failure", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -184,14 +205,21 @@ describe("OSSProvider — initPromise retry after failure", () => {
// ---------------------------------------------------------------------------
describe("OSSProvider — graceful SQLite fallback", () => {
let capturedConfigs: Record<string, unknown>[];
/** When set, the mock Memory constructor always throws with this message. */
let forceConstructorError: string | null;
beforeEach(() => {
capturedConfigs = [];
forceConstructorError = null;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfigs.push({ ...config });
if (forceConstructorError) {
throw new Error(forceConstructorError);
}
if (!config.disableHistory) {
throw new Error("Could not locate the bindings file");
}
@@ -211,9 +239,14 @@ describe("OSSProvider — graceful SQLite fallback", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries with disableHistory: true when initial construction fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts");
@@ -242,14 +275,8 @@ describe("OSSProvider — graceful SQLite fallback", () => {
});
it("does not retry when disableHistory is already true", async () => {
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
// Fail even with disableHistory (e.g. vector store issue)
throw new Error("vector store connection refused");
}
},
}));
// Force the constructor to always throw, regardless of disableHistory
forceConstructorError = "vector store connection refused";
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -274,6 +301,7 @@ describe("PlatformProvider — initPromise retry after failure", () => {
beforeEach(() => {
callCount = 0;
vi.resetModules();
vi.doMock("mem0ai", () => ({
default: class MockMemoryClient {
@@ -300,6 +328,10 @@ describe("PlatformProvider — initPromise retry after failure", () => {
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -320,3 +352,349 @@ describe("PlatformProvider — initPromise retry after failure", () => {
expect(callCount).toBe(2);
});
});
// ---------------------------------------------------------------------------
// 6. OSSProvider: _buildConfig covers all branches
// ---------------------------------------------------------------------------
describe("OSSProvider — _buildConfig branch coverage", () => {
let capturedConfig: Record<string, unknown> | undefined;
beforeEach(() => {
capturedConfig = undefined;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfig = { ...config };
}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("builds config with custom embedder, llm, vectorStore, and historyDbPath", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "openai", config: { apiKey: "sk-e", model: "text-embedding-3-small" } },
llm: { provider: "openai", config: { apiKey: "sk-l", model: "gpt-4" } },
vectorStore: { provider: "qdrant", config: { host: "localhost", port: 6333 } },
historyDbPath: "/tmp/history.db",
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => `/resolved${p}` } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
expect(capturedConfig!.embedder).toEqual({
provider: "openai",
config: { model: "text-embedding-3-small", apiKey: "sk-e" },
});
expect(capturedConfig!.llm).toEqual({
provider: "openai",
config: expect.objectContaining({ model: "gpt-4", apiKey: "sk-l" }),
});
expect(capturedConfig!.vectorStore).toEqual({ provider: "qdrant", config: { host: "localhost", port: 6333 } });
expect(capturedConfig!.historyDbPath).toBe("/resolved/tmp/history.db");
expect(capturedConfig!.disableHistory).toBe(true);
});
it("strips empty-string values from embedder and llm config", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "openai", config: { apiKey: "", model: "custom-model" } },
llm: { provider: "openai", config: { apiKey: "", model: "" } },
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
// Empty apiKey should be stripped, leaving only the non-empty model
const embedderCfg = (capturedConfig!.embedder as any).config;
expect(embedderCfg.apiKey).toBeUndefined();
expect(embedderCfg.model).toBe("custom-model");
// Both empty keys in llm should be stripped, defaults applied
const llmCfg = (capturedConfig!.llm as any).config;
expect(llmCfg.apiKey).toBeUndefined();
});
it("falls back to default provider when embedder/llm provider is empty", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "", config: { apiKey: "sk-e" } },
llm: { provider: "", config: { apiKey: "sk-l" } },
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
// Empty provider should fall back to "openai" default
expect((capturedConfig!.embedder as any).provider).toBe("openai");
expect((capturedConfig!.llm as any).provider).toBe("openai");
});
});
// ---------------------------------------------------------------------------
// 7. OSSProvider: vector store dimension patching
// ---------------------------------------------------------------------------
describe("OSSProvider — vector store dimension patching", () => {
let capturedModule: any;
beforeEach(() => {
vi.resetModules();
vi.doMock("mem0ai/oss", () => {
const mod = {
Memory: class MockMemory {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
PGVector: class {
config: any;
dimension: any;
_initializePromise: any;
initialize() { return Promise.resolve("pg-initialized"); }
},
RedisDB: class {
config: any;
_initializePromise: any;
initialize() { return Promise.resolve("redis-initialized"); }
},
Qdrant: class {
config: any;
dimension: any;
_initializePromise: any;
initialize() { return Promise.resolve("qdrant-initialized"); }
},
};
capturedModule = mod;
return mod;
});
});
afterEach(() => {
vi.restoreAllMocks();
});
async function triggerInit() {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const provider = createProvider(cfg, { resolvePath: (p: string) => p } as any);
await provider.search("test", { user_id: "u1" });
}
it("copies config.dimension to embeddingModelDims and this.dimension", async () => {
await triggerInit();
const pg = new capturedModule.PGVector();
pg.config = { dimension: 1536 };
await pg.initialize();
expect(pg.config.embeddingModelDims).toBe(1536);
expect(pg.dimension).toBe(1536);
});
it("returns resolved promise when no dimensions are known", async () => {
await triggerInit();
const pg = new capturedModule.PGVector();
pg.config = {};
const result = await pg.initialize();
expect(result).toBeUndefined();
});
it("runs original initialize only once via cached promise", async () => {
await triggerInit();
const q = new capturedModule.Qdrant();
q.config = { dimension: 768 };
const first = await q.initialize();
const second = await q.initialize();
expect(first).toBe("qdrant-initialized");
expect(second).toBe("qdrant-initialized");
expect(q._initializePromise).toBeDefined();
});
it("skips missing vector store classes without crashing", async () => {
// Override with a mock that omits PGVector entirely
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
PGVector: undefined, // explicitly absent — tests the !VectorCls guard
RedisDB: class { initialize() { return Promise.resolve(); } },
Qdrant: class { initialize() { return Promise.resolve(); } },
}));
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const provider = createProvider(cfg, { resolvePath: (p: string) => p } as any);
// Should not throw even though PGVector is missing
const results = await provider.search("test", { user_id: "u1" });
expect(results).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// 8. OSSProvider: history() error handler
// ---------------------------------------------------------------------------
describe("OSSProvider — history error handling", () => {
/** When set, the mock history() throws this value instead of an Error. */
let historyThrowValue: unknown;
beforeEach(() => {
historyThrowValue = new Error("history not available");
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
async history() { throw historyThrowValue; }
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("returns empty array and warns when history() throws an Error", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
const result = await provider.history("mem-123");
expect(result).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
"[mem0] OSS history() failed:",
"history not available",
);
warnSpy.mockRestore();
});
it("handles non-Error thrown values in history()", async () => {
historyThrowValue = "raw string error";
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
const result = await provider.history("mem-456");
expect(result).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
"[mem0] OSS history() failed:",
"raw string error",
);
warnSpy.mockRestore();
});
});
// ---------------------------------------------------------------------------
// 9. OSSProvider: customPrompt passthrough
// ---------------------------------------------------------------------------
describe("OSSProvider — customPrompt passthrough", () => {
let capturedConfig: Record<string, unknown> | undefined;
beforeEach(() => {
capturedConfig = undefined;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfig = { ...config };
}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("passes customPrompt to Memory config when provided", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
customPrompt: "Extract only user preferences.",
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
expect(capturedConfig!.customPrompt).toBe("Extract only user preferences.");
});
});
+200 -7
View File
@@ -8,10 +8,10 @@
* Disable with: MEM0_TELEMETRY=false
*/
import { createHash } from "node:crypto";
import { readPluginAuth } from "./cli/config-file.ts";
import { createHash, randomUUID } from "node:crypto";
import { readPluginAuth, writePluginAuth, getBaseUrl } from "./cli/config-file.ts";
export const PLUGIN_VERSION = "1.0.4";
export const PLUGIN_VERSION = "1.0.6";
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
@@ -22,15 +22,155 @@ const FLUSH_THRESHOLD = 10;
let eventQueue: Record<string, unknown>[] = [];
let flushTimer: ReturnType<typeof setInterval> | undefined;
let _cachedAnonymousId: string | undefined;
let _aliasCheckDone = false;
/**
* Return a persistent per-machine anonymous ID, generating one if needed.
*
* Stored in ~/.openclaw/openclaw.json under the plugin's `anonymousTelemetryId`
* field so repeat sessions on the same machine share one PostHog identity
* instead of collapsing into a single shared fallback string. The result is
* cached in module memory after the first read so we don't re-touch disk on
* every queued event.
*/
function getOrCreateAnonymousId(): string {
if (_cachedAnonymousId) return _cachedAnonymousId;
try {
const auth = readPluginAuth();
if (auth.anonymousTelemetryId) {
_cachedAnonymousId = auth.anonymousTelemetryId;
return _cachedAnonymousId;
}
} catch {
/* ignore */
}
const newId = `openclaw-anon-${randomUUID().replace(/-/g, "")}`;
try {
writePluginAuth({ anonymousTelemetryId: newId });
} catch {
/* ignore — return generated id anyway */
}
_cachedAnonymousId = newId;
return newId;
}
/**
* If we just resolved to a real identity but a stored anonymous id exists,
* build a one-shot PostHog $identify event so the pre-signup history gets
* stitched onto the authenticated profile. Returns null when no aliasing is
* needed (already done, or no anon id on disk, or still anonymous).
*
* Caller is responsible for pushing the returned event onto eventQueue ahead
* of the regular event.
*/
function maybeBuildIdentifyEvent(
distinctId: string,
): Record<string, unknown> | null {
if (_aliasCheckDone) return null;
if (!distinctId || distinctId.startsWith("openclaw-anon-")) return null;
try {
const auth = readPluginAuth();
const storedAnon = auth.anonymousTelemetryId;
if (!storedAnon) {
_aliasCheckDone = true;
return null;
}
const identifyEvent = {
event: "$identify",
distinct_id: distinctId,
properties: {
$anon_distinct_id: storedAnon,
$lib: "posthog-node",
},
};
try {
writePluginAuth({ anonymousTelemetryId: "" });
} catch {
/* ignore — alias may double-fire next session, harmless */
}
_aliasCheckDone = true;
_cachedAnonymousId = undefined;
return identifyEvent;
} catch {
return null;
}
}
let _emailResolutionAttempted = false;
/**
* If we have an apiKey but no cached userEmail, do a one-shot /v1/ping/
* call to resolve the email and cache it. This runs async as a side-effect;
* the current event ships with md5(apiKey) but subsequent events (including
* those flushed by the beforeExit handler in the same process) will use
* the resolved email.
*/
function maybeResolveEmail(apiKey: string): void {
if (_emailResolutionAttempted) return;
_emailResolutionAttempted = true;
const baseUrl = getBaseUrl().replace(/\/+$/, "");
fetch(`${baseUrl}/v1/ping/`, {
method: "GET",
headers: {
Authorization: `Token ${apiKey}`,
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5_000),
})
.then((res) => res.json())
.then((data: any) => {
const email = data?.user_email;
if (email) {
try {
writePluginAuth({ userEmail: email });
} catch {
/* ignore */
}
// Upgrade any already-queued events from md5(apiKey) to email
const oldId = createHash("md5").update(apiKey).digest("hex");
for (const ev of eventQueue) {
if (ev.distinct_id === oldId) {
ev.distinct_id = email;
}
// Also upgrade $identify's distinct_id if present
if (
ev.event === "$identify" &&
ev.distinct_id === oldId
) {
ev.distinct_id = email;
}
}
}
})
.catch(() => {
/* silently swallow — md5(apiKey) is used as fallback */
});
}
let _telemetryEnabled: boolean | undefined;
function isTelemetryEnabled(): boolean {
const val = (process.env.MEM0_TELEMETRY ?? "true").toLowerCase();
return val !== "false" && val !== "0" && val !== "no";
if (_telemetryEnabled !== undefined) return _telemetryEnabled;
try {
const val = (globalThis as any).__mem0_telemetry_override;
if (val !== undefined) {
const s = String(val).toLowerCase();
_telemetryEnabled = s !== "false" && s !== "0" && s !== "no";
} else {
_telemetryEnabled = true;
}
} catch {
_telemetryEnabled = true;
}
return _telemetryEnabled;
}
/**
* Return a stable anonymous identifier for the current user.
*
* Priority: cached userEmail (from /v1/ping/) > MD5(apiKey) > fallback.
* Priority: cached userEmail (from /v1/ping/) > MD5(apiKey) >
* persistent per-machine anonymous ID.
*/
function getDistinctId(apiKey?: string): string {
try {
@@ -42,7 +182,7 @@ function getDistinctId(apiKey?: string): string {
if (apiKey) {
return createHash("md5").update(apiKey).digest("hex");
}
return "anonymous-openclaw";
return getOrCreateAnonymousId();
}
function ensureFlushTimer(): void {
@@ -53,6 +193,42 @@ function ensureFlushTimer(): void {
}
}
let _exitHandlerInstalled = false;
/**
* Install a one-time `beforeExit` handler that drains queued events on
* process exit. Without this, short-lived CLI invocations (e.g. one
* `openclaw mem0 status` call) exit before the unref'd flushTimer fires
* and before FLUSH_THRESHOLD is hit, dropping every queued event silently.
*
* Returning a Promise from a `beforeExit` handler keeps the event loop
* alive until that Promise resolves, so the awaited fetch actually has
* time to land at PostHog.
*/
function ensureExitHandler(): void {
if (_exitHandlerInstalled) return;
_exitHandlerInstalled = true;
process.on("beforeExit", async () => {
if (eventQueue.length === 0) return;
const batch = eventQueue;
eventQueue = [];
const body = JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
try {
await fetch(POSTHOG_HOST, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": String(Buffer.byteLength(body)),
},
body,
signal: AbortSignal.timeout(3_000),
});
} catch {
/* silently swallow */
}
});
}
function flushEvents(): void {
if (eventQueue.length === 0) return;
const batch = eventQueue;
@@ -85,6 +261,22 @@ export function captureEvent(
try {
const distinctId = getDistinctId(ctx?.apiKey);
// If we resolved to md5(apiKey) instead of email, kick off a background
// /v1/ping/ to resolve and cache the email. The current event ships with
// the hash, but the async resolution upgrades any still-queued events
// (including this one) before the beforeExit flush fires.
if (ctx?.apiKey && distinctId && !distinctId.includes("@") && !distinctId.startsWith("openclaw-anon-")) {
maybeResolveEmail(ctx.apiKey);
}
// First authenticated event after a previous anonymous session: queue a
// $identify ahead of the regular event so PostHog merges the anonymous
// history onto the authenticated profile in the same batch flush.
const identifyEvent = maybeBuildIdentifyEvent(distinctId);
if (identifyEvent) {
eventQueue.push(identifyEvent);
}
eventQueue.push({
event: eventName,
distinct_id: distinctId,
@@ -103,6 +295,7 @@ export function captureEvent(
});
ensureFlushTimer();
ensureExitHandler();
if (eventQueue.length >= FLUSH_THRESHOLD) {
flushEvents();
+7
View File
@@ -0,0 +1,7 @@
/**
* Test shim for openclaw/plugin-sdk/plugin-entry.
* At runtime this is resolved from the OpenClaw gateway.
*/
export function definePluginEntry<T>(entry: T): T {
return entry;
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Test shim for openclaw/plugin-sdk.
* At runtime this is resolved from the OpenClaw gateway.
*/
export interface OpenClawPluginApi {
pluginConfig: Record<string, unknown>;
logger: {
info(msg: string): void;
warn(msg: string): void;
error(msg: string): void;
debug(msg: string): void;
};
resolvePath(p: string): string;
registerTool(definition: Record<string, unknown>, metadata?: Record<string, unknown>): void;
on(event: string, handler: (event: any, ctx: any) => any): void;
registerCli(handler: (context: { program: any }) => void, options?: Record<string, unknown>): void;
registerService(service: { id: string; start: (...args: any[]) => void; stop: () => void }): void;
[key: string]: unknown;
}
+266 -78
View File
@@ -58,6 +58,8 @@ interface MockCommand {
_args: Array<{ name: string; desc: string }>;
command(name: string): MockCommand;
description(desc: string): MockCommand;
configureHelp(opts: any): MockCommand;
hook(event: string, fn: (...args: any[]) => any): MockCommand;
option(flags: string, desc: string, defaultVal?: string): MockCommand;
argument(name: string, desc: string): MockCommand;
action(fn: (...args: any[]) => any): MockCommand;
@@ -80,6 +82,12 @@ function createMockCommand(name: string): MockCommand {
cmd._description = desc;
return cmd;
},
configureHelp(_opts: any) {
return cmd;
},
hook(_event: string, _fn: (...args: any[]) => any) {
return cmd;
},
option(flags: string, desc: string, defaultVal?: string) {
cmd._options.push({ flags, desc, defaultVal });
return cmd;
@@ -145,6 +153,18 @@ function createMockBackend() {
connected: true,
url: "https://api.mem0.ai",
}),
add: vi.fn().mockResolvedValue({ id: "new-1" }),
listEvents: vi.fn().mockResolvedValue([
{ id: "evt-1111-2222-3333-4444", event_type: "ADD", status: "SUCCEEDED", latency: 1500, created_at: "2026-04-01T12:00:00Z" },
]),
getEvent: vi.fn().mockResolvedValue({
id: "evt-1111-2222-3333-4444",
event_type: "ADD",
status: "SUCCEEDED",
latency: 1500,
created_at: "2026-04-01T12:00:00Z",
updated_at: "2026-04-01T12:00:01Z",
}),
};
}
@@ -155,7 +175,6 @@ function createMockCfg() {
apiKey: "m0-test-key-1234",
baseUrl: "https://api.mem0.ai",
topK: 5,
enableGraph: false,
autoCapture: true,
autoRecall: true,
searchThreshold: 0.5,
@@ -284,7 +303,6 @@ describe("registerCliCommands", () => {
expect(names).toContain("list");
expect(names).toContain("update");
expect(names).toContain("delete");
expect(names).toContain("history");
expect(names).toContain("status");
expect(names).toContain("config");
expect(names).toContain("dream");
@@ -321,7 +339,6 @@ describe("registerCliCommands", () => {
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: "m0-my-key-1234",
baseUrl: "https://api.mem0.ai",
mode: "platform",
}),
);
@@ -857,48 +874,6 @@ describe("registerCliCommands", () => {
});
});
// ========================================================================
// history subcommand
// ========================================================================
describe("history subcommand", () => {
it("calls provider.history and prints entries as JSON", async () => {
const { mem0, provider } = setup();
const historyCmd = findCommand(mem0, "history")!;
await historyCmd._action!("m1");
expect(provider.history).toHaveBeenCalledWith("m1");
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("UPDATE"),
);
});
it("prints message when no history found", async () => {
const { mem0, provider } = setup();
provider.history.mockResolvedValueOnce([]);
const historyCmd = findCommand(mem0, "history")!;
await historyCmd._action!("m1");
expect(consoleSpy.log).toHaveBeenCalledWith(
"No history found for this memory.",
);
});
it("handles history errors gracefully", async () => {
const { mem0, provider } = setup();
provider.history.mockRejectedValueOnce(new Error("history boom"));
const historyCmd = findCommand(mem0, "history")!;
await historyCmd._action!("m1");
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("History failed"),
);
});
});
// ========================================================================
// status subcommand
// ========================================================================
@@ -964,9 +939,9 @@ describe("registerCliCommands", () => {
const allOutput = consoleSpy.log.mock.calls.map((c) => c[0]).join("\n");
expect(allOutput).toContain("Key");
expect(allOutput).toContain("Value");
expect(allOutput).toContain("platform.api_key");
expect(allOutput).toContain("platform.base_url");
expect(allOutput).toContain("defaults.user_id");
expect(allOutput).toContain("api_key");
expect(allOutput).toContain("email");
expect(allOutput).toContain("user_id");
expect(allOutput).toContain("Config file:");
});
});
@@ -991,7 +966,7 @@ describe("registerCliCommands", () => {
const configCmd = findCommand(mem0, "config")!;
const getCmd = findCommand(configCmd, "get")!;
getCmd._action!("org_id");
getCmd._action!("email");
expect(consoleSpy.log).toHaveBeenCalledWith("(not set)");
});
@@ -1026,14 +1001,14 @@ describe("registerCliCommands", () => {
expect(logged).not.toContain("supersecret");
});
it("supports dot-notation keys like platform.api_key", () => {
it("supports short alias keys like email", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const getCmd = findCommand(configCmd, "get")!;
getCmd._action!("platform.base_url");
getCmd._action!("email");
expect(consoleSpy.log).toHaveBeenCalledWith("https://api.mem0.ai");
expect(consoleSpy.log).toHaveBeenCalledWith("(not set)");
});
});
@@ -1057,18 +1032,6 @@ describe("registerCliCommands", () => {
);
});
it("coerces 'true' to boolean for boolean keys", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const setCmd = findCommand(configCmd, "set")!;
setCmd._action!("enable_graph", "true");
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({ enableGraph: true }),
);
});
it("coerces 'false' to boolean false for boolean keys", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
@@ -1093,18 +1056,6 @@ describe("registerCliCommands", () => {
);
});
it("coerces 'yes' to boolean true for boolean keys", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const setCmd = findCommand(configCmd, "set")!;
setCmd._action!("enable_graph", "yes");
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({ enableGraph: true }),
);
});
it("coerces integer string for integer keys", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
@@ -1143,12 +1094,12 @@ describe("registerCliCommands", () => {
expect(writePluginAuth).not.toHaveBeenCalled();
});
it("supports dot-notation keys for set", () => {
it("supports short alias keys for set", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const setCmd = findCommand(configCmd, "set")!;
setCmd._action!("defaults.user_id", "bob");
setCmd._action!("user_id", "bob");
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({ userId: "bob" }),
@@ -1268,4 +1219,241 @@ describe("registerCliCommands", () => {
);
});
});
// ========================================================================
// import subcommand
// ========================================================================
describe("import subcommand", () => {
it("imports memories from a JSON array file", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify([
{ memory: "fact one" },
{ memory: "fact two" },
]),
);
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("memories.json", {});
expect(backend.add).toHaveBeenCalledTimes(2);
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 2 memories.");
});
it("imports a single JSON object", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify({ memory: "single fact" }),
);
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("single.json", {});
expect(backend.add).toHaveBeenCalledTimes(1);
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 1 memories.");
});
it("skips items with no extractable content", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify([{ memory: "valid" }, { nofield: true }]),
);
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("mixed.json", {});
expect(backend.add).toHaveBeenCalledTimes(1);
expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import.");
});
it("uses --user-id and --agent-id overrides", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify([{ memory: "fact" }]),
);
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("f.json", { userId: "override-user", agentId: "agent-1" });
expect(backend.add).toHaveBeenCalledWith("fact", undefined, expect.objectContaining({
userId: "override-user",
agentId: "agent-1",
}));
});
it("handles file read errors gracefully", async () => {
const { mem0 } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
throw new Error("ENOENT");
});
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("missing.json", {});
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to read file"),
);
});
it("handles backend.add failures gracefully", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify([{ memory: "will fail" }]),
);
backend.add.mockRejectedValueOnce(new Error("API error"));
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("fail.json", {});
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 0 memories.");
expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import.");
});
});
// ========================================================================
// event subcommand
// ========================================================================
describe("event subcommand", () => {
it("lists events in table format", async () => {
const { mem0, backend } = setup();
const eventCmd = findCommand(mem0, "event")!;
const listCmd = findCommand(eventCmd, "list")!;
await listCmd._action!();
expect(backend.listEvents).toHaveBeenCalled();
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("evt-1111-2222-3333-4444"),
);
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("1 event"),
);
});
it("prints message when no events found", async () => {
const { mem0, backend } = setup();
backend.listEvents.mockResolvedValueOnce([]);
const eventCmd = findCommand(mem0, "event")!;
const listCmd = findCommand(eventCmd, "list")!;
await listCmd._action!();
expect(consoleSpy.log).toHaveBeenCalledWith("No events found.");
});
it("handles event list errors gracefully", async () => {
const { mem0, backend } = setup();
backend.listEvents.mockRejectedValueOnce(new Error("event boom"));
const eventCmd = findCommand(mem0, "event")!;
const listCmd = findCommand(eventCmd, "list")!;
await listCmd._action!();
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to list events"),
);
});
it("shows event status details", async () => {
const { mem0, backend } = setup();
const eventCmd = findCommand(mem0, "event")!;
const statusCmd = findCommand(eventCmd, "status")!;
await statusCmd._action!("evt-1111-2222-3333-4444");
expect(backend.getEvent).toHaveBeenCalledWith("evt-1111-2222-3333-4444");
expect(consoleSpy.log).toHaveBeenCalledWith("Event ID: evt-1111-2222-3333-4444");
expect(consoleSpy.log).toHaveBeenCalledWith("Type: ADD");
expect(consoleSpy.log).toHaveBeenCalledWith("Status: SUCCEEDED");
});
it("handles event status errors gracefully", async () => {
const { mem0, backend } = setup();
backend.getEvent.mockRejectedValueOnce(new Error("not found"));
const eventCmd = findCommand(mem0, "event")!;
const statusCmd = findCommand(eventCmd, "status")!;
await statusCmd._action!("bad-id");
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to get event"),
);
});
it("event list returns early in open-source mode", async () => {
const provider = createMockProvider();
const cfg = { ...createMockCfg(), mode: "open-source" as const };
const mockApi = {
registerCli: vi.fn((cb: any) => {
const root = createMockCommand("root");
cb({ program: root });
const mem0 = findCommand(root, "mem0")!;
const eventCmd = findCommand(mem0, "event")!;
const listCmd = findCommand(eventCmd, "list")!;
listCmd._action!();
}),
logger: { info: vi.fn(), warn: vi.fn() },
} as any;
registerCliCommands(
mockApi,
null as any,
provider as any,
cfg as any,
vi.fn().mockReturnValue("testuser"),
vi.fn((id: string) => `testuser:agent:${id}`),
vi.fn().mockReturnValue({ user_id: "testuser", top_k: 5 }),
vi.fn().mockReturnValue(undefined),
);
// Wait for async action
await new Promise((r) => setTimeout(r, 10));
expect(consoleSpy.log).toHaveBeenCalledWith(
"Event tracking is only available in platform mode.",
);
});
it("event status returns early in open-source mode", async () => {
const provider = createMockProvider();
const cfg = { ...createMockCfg(), mode: "open-source" as const };
const mockApi = {
registerCli: vi.fn((cb: any) => {
const root = createMockCommand("root");
cb({ program: root });
const mem0 = findCommand(root, "mem0")!;
const eventCmd = findCommand(mem0, "event")!;
const statusCmd = findCommand(eventCmd, "status")!;
statusCmd._action!("evt-123");
}),
logger: { info: vi.fn(), warn: vi.fn() },
} as any;
registerCliCommands(
mockApi,
null as any,
provider as any,
cfg as any,
vi.fn().mockReturnValue("testuser"),
vi.fn((id: string) => `testuser:agent:${id}`),
vi.fn().mockReturnValue({ user_id: "testuser", top_k: 5 }),
vi.fn().mockReturnValue(undefined),
);
// Wait for async action
await new Promise((r) => setTimeout(r, 10));
expect(consoleSpy.log).toHaveBeenCalledWith(
"Event tracking is only available in platform mode.",
);
});
});
});
+1 -11
View File
@@ -69,10 +69,7 @@ describe("readPluginAuth", () => {
apiKey: "sk-test-123",
baseUrl: "https://custom.api.com",
userId: "user-1",
orgId: "org-1",
projectId: "proj-1",
mode: "platform",
enableGraph: true,
autoRecall: true,
autoCapture: false,
topK: 10,
@@ -87,17 +84,14 @@ describe("readPluginAuth", () => {
apiKey: "sk-test-123",
baseUrl: "https://custom.api.com",
userId: "user-1",
orgId: "org-1",
projectId: "proj-1",
mode: "platform",
enableGraph: true,
autoRecall: true,
autoCapture: false,
topK: 10,
});
});
it("handles snake_case aliases (api_key, base_url, user_id, org_id, project_id)", () => {
it("handles snake_case aliases (api_key, base_url, user_id)", () => {
setConfigFile({
plugins: {
entries: {
@@ -107,8 +101,6 @@ describe("readPluginAuth", () => {
api_key: "sk-snake",
base_url: "https://snake.api.com",
user_id: "user-snake",
org_id: "org-snake",
project_id: "proj-snake",
},
},
},
@@ -119,8 +111,6 @@ describe("readPluginAuth", () => {
expect(auth.apiKey).toBe("sk-snake");
expect(auth.baseUrl).toBe("https://snake.api.com");
expect(auth.userId).toBe("user-snake");
expect(auth.orgId).toBe("org-snake");
expect(auth.projectId).toBe("proj-snake");
});
it("returns empty object when JSON is invalid", () => {
+5 -30
View File
@@ -64,11 +64,6 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
expect(cfg.searchThreshold).toBe(0.5);
});
it("enableGraph defaults to false", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.enableGraph).toBe(false);
});
it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.customInstructions).toBe(DEFAULT_CUSTOM_INSTRUCTIONS);
@@ -274,14 +269,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => {
expect(cfg.autoRecall).toBe(false);
});
it("enableGraph can be set to true", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
enableGraph: true,
});
expect(cfg.enableGraph).toBe(true);
});
it("custom topK is used when provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", topK: 20 });
expect(cfg.topK).toBe(20);
@@ -330,18 +317,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => {
expect(cfg.baseUrl).toBe("https://custom.api.com");
});
it("orgId is passed through when provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", orgId: "org-123" });
expect(cfg.orgId).toBe("org-123");
});
it("projectId is passed through when provided", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
projectId: "proj-456",
});
expect(cfg.projectId).toBe("proj-456");
});
});
// ---------------------------------------------------------------------------
@@ -359,22 +334,23 @@ describe("mem0ConfigSchema.parse() — oss config", () => {
historyDbPath: "/tmp/history.db",
disableHistory: false,
};
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: ossConfig });
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: ossConfig });
expect(cfg.mode).toBe("open-source");
expect(cfg.oss).toEqual(ossConfig);
});
it("ignores oss when it is not a plain object", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: "not-an-object" });
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: "not-an-object" });
expect(cfg.oss).toBeUndefined();
});
it("ignores oss when it is an array", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: [1, 2, 3] });
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: [1, 2, 3] });
expect(cfg.oss).toBeUndefined();
});
it("ignores oss when it is null", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: null });
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: null });
expect(cfg.oss).toBeUndefined();
});
});
@@ -388,7 +364,6 @@ describe("mem0ConfigSchema.parse() — skills config", () => {
triage: {
enabled: true,
importanceThreshold: 3,
enableGraph: false,
credentialPatterns: ["sk-", "ghp_"],
},
recall: {
+37
View File
@@ -0,0 +1,37 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { bootstrapTelemetryFlag } from "../fs-safe.ts";
describe("bootstrapTelemetryFlag", () => {
const originalEnv = process.env.MEM0_TELEMETRY;
beforeEach(() => {
delete (globalThis as any).__mem0_telemetry_override;
delete process.env.MEM0_TELEMETRY;
});
afterEach(() => {
delete (globalThis as any).__mem0_telemetry_override;
if (originalEnv !== undefined) {
process.env.MEM0_TELEMETRY = originalEnv;
} else {
delete process.env.MEM0_TELEMETRY;
}
});
it("sets globalThis override when MEM0_TELEMETRY is set", () => {
process.env.MEM0_TELEMETRY = "false";
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBe("false");
});
it("does not set globalThis override when MEM0_TELEMETRY is unset", () => {
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBeUndefined();
});
it("passes through truthy values", () => {
process.env.MEM0_TELEMETRY = "true";
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBe("true");
});
});
+4 -2
View File
@@ -61,6 +61,7 @@ describe("providerToBackend — search", () => {
keyword_search: true,
reranking: true,
filters: { category: "preference" },
source: "OPENCLAW",
});
expect(results).toHaveLength(1);
expect((results[0] as any).id).toBe("m1");
@@ -79,6 +80,7 @@ describe("providerToBackend — search", () => {
keyword_search: undefined,
reranking: undefined,
filters: undefined,
source: "OPENCLAW",
});
});
});
@@ -127,7 +129,6 @@ describe("providerToBackend — add", () => {
immutable: true,
infer: false,
expires: "2027-01-01",
enableGraph: true,
});
expect(provider.add).toHaveBeenCalledWith(
@@ -139,7 +140,6 @@ describe("providerToBackend — add", () => {
immutable: true,
infer: false,
expiration_date: "2027-01-01",
enable_graph: true,
}),
);
});
@@ -175,6 +175,7 @@ describe("providerToBackend — listMemories", () => {
expect(provider.getAll).toHaveBeenCalledWith({
user_id: DEFAULT_USER,
page_size: 50,
source: "OPENCLAW",
});
expect(results).toHaveLength(1);
});
@@ -188,6 +189,7 @@ describe("providerToBackend — listMemories", () => {
expect(provider.getAll).toHaveBeenCalledWith({
user_id: DEFAULT_USER,
page_size: undefined,
source: "OPENCLAW",
});
});
});
+65
View File
@@ -0,0 +1,65 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock config-file before importing telemetry
vi.mock("../cli/config-file.ts", () => ({
readPluginAuth: vi.fn().mockReturnValue({}),
}));
import { captureEvent, PLUGIN_VERSION } from "../telemetry.ts";
import { readPluginAuth } from "../cli/config-file.ts";
describe("telemetry", () => {
let fetchSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
// Reset telemetry enabled state
(globalThis as any).__mem0_telemetry_override = undefined;
fetchSpy = vi.fn().mockResolvedValue({ ok: true });
vi.stubGlobal("fetch", fetchSpy);
});
afterEach(() => {
vi.restoreAllMocks();
delete (globalThis as any).__mem0_telemetry_override;
});
it("exports PLUGIN_VERSION", () => {
expect(PLUGIN_VERSION).toBe("1.0.6");
});
it("captureEvent does not throw", () => {
expect(() => captureEvent("test_event")).not.toThrow();
});
it("captureEvent accepts properties and context", () => {
expect(() =>
captureEvent("test_event", { key: "val" }, { apiKey: "m0-key", mode: "platform" }),
).not.toThrow();
});
it("captureEvent is silent when telemetry disabled", () => {
(globalThis as any).__mem0_telemetry_override = "false";
// Force re-evaluation by resetting cached value
// Since _telemetryEnabled is module-level, we test indirectly
expect(() => captureEvent("test_event")).not.toThrow();
});
it("uses userEmail as distinct ID when available", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({
userEmail: "test@example.com",
});
expect(() => captureEvent("test_event")).not.toThrow();
});
it("falls back to a generated anonymous id when no apiKey", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({});
expect(() => captureEvent("test_event", {}, {})).not.toThrow();
});
it("handles readPluginAuth errors gracefully", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
throw new Error("config read failed");
});
expect(() => captureEvent("test_event")).not.toThrow();
});
});
+8 -117
View File
@@ -15,7 +15,8 @@ import { createMemoryGetTool } from "../tools/memory-get.ts";
import { createMemoryDeleteTool } from "../tools/memory-delete.ts";
import { createMemoryListTool } from "../tools/memory-list.ts";
import { createMemoryUpdateTool } from "../tools/memory-update.ts";
import { createMemoryHistoryTool } from "../tools/memory-history.ts";
import { createMemoryEventListTool } from "../tools/memory-event-list.ts";
import { createMemoryEventStatusTool } from "../tools/memory-event-status.ts";
// ---------------------------------------------------------------------------
// Mock helper
@@ -31,7 +32,6 @@ function createMockToolDeps(overrides = {}): ToolDeps {
mode: "platform",
userId: "testuser",
topK: 5,
enableGraph: false,
autoCapture: true,
autoRecall: true,
searchThreshold: 0.5,
@@ -80,10 +80,10 @@ function createMockToolDeps(overrides = {}): ToolDeps {
// ---------------------------------------------------------------------------
describe("registerAllTools", () => {
it("calls api.registerTool exactly 7 times", () => {
it("calls api.registerTool exactly 8 times", () => {
const ctx = createMockToolDeps();
registerAllTools(ctx);
expect(ctx.api.registerTool).toHaveBeenCalledTimes(7);
expect(ctx.api.registerTool).toHaveBeenCalledTimes(8);
});
it("registers tools with the correct names", () => {
@@ -103,7 +103,8 @@ describe("registerAllTools", () => {
"memory_list",
"memory_update",
"memory_delete",
"memory_history",
"memory_event_list",
"memory_event_status",
]);
});
@@ -129,6 +130,8 @@ describe("tool factory shape", () => {
{ fn: createMemoryGetTool, expectedName: "memory_get" },
{ fn: createMemoryDeleteTool, expectedName: "memory_delete" },
{ fn: createMemoryListTool, expectedName: "memory_list" },
{ fn: createMemoryEventListTool, expectedName: "memory_event_list" },
{ fn: createMemoryEventStatusTool, expectedName: "memory_event_status" },
];
for (const { fn, expectedName } of factories) {
@@ -883,115 +886,3 @@ describe("memory_update execute", () => {
});
});
// ---------------------------------------------------------------------------
// memory_history execute
// ---------------------------------------------------------------------------
describe("memory_history execute", () => {
it("returns formatted history", async () => {
const historyEntries = [
{
event: "ADD",
created_at: "2026-01-01T00:00:00Z",
old_memory: null,
new_memory: "Initial memory text",
},
{
event: "UPDATE",
created_at: "2026-01-02T00:00:00Z",
old_memory: "Initial memory text",
new_memory: "Updated memory text",
},
];
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn().mockResolvedValue(historyEntries),
},
});
const tool = createMemoryHistoryTool(ctx);
const result = await tool.execute("call-1", { memoryId: "mem-abc" });
expect(ctx.provider!.history).toHaveBeenCalledWith("mem-abc");
expect(result.content[0].text).toContain("History for memory mem-abc");
expect(result.content[0].text).toContain("2 entries");
expect(result.content[0].text).toContain("[ADD]");
expect(result.content[0].text).toContain("[UPDATE]");
expect(result.content[0].text).toContain("Initial memory text");
expect(result.content[0].text).toContain("Updated memory text");
expect(result.content[0].text).toContain("Old: (none)");
expect(result.details.count).toBe(2);
expect(result.details.history).toEqual(historyEntries);
});
it("handles empty history", async () => {
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn().mockResolvedValue([]),
},
});
const tool = createMemoryHistoryTool(ctx);
const result = await tool.execute("call-2", { memoryId: "mem-empty" });
expect(result.content[0].text).toBe(
"No history found for memory mem-empty.",
);
expect(result.details.count).toBe(0);
});
it("handles null history", async () => {
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn().mockResolvedValue(null),
},
});
const tool = createMemoryHistoryTool(ctx);
const result = await tool.execute("call-3", { memoryId: "mem-null" });
expect(result.content[0].text).toBe(
"No history found for memory mem-null.",
);
expect(result.details.count).toBe(0);
});
it("handles errors gracefully", async () => {
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn().mockRejectedValue(new Error("history unavailable")),
},
});
const tool = createMemoryHistoryTool(ctx);
const result = await tool.execute("call-4", { memoryId: "mem-err" });
expect(result.content[0].text).toContain("Memory history failed");
expect(result.content[0].text).toContain("history unavailable");
expect(result.details.error).toContain("history unavailable");
});
});

Some files were not shown because too many files have changed in this diff Show More