Compare commits

..

21 Commits

Author SHA1 Message Date
kartik-mem0 013718c817 refactor(opencode): native SDK tools, drop MCP, trim skills, expand telemetry
Reworks @mem0/opencode-plugin to expose memory operations as native OpenCode
tools (via the @opencode-ai/plugin `tool()` helper, backed by the mem0ai SDK)
instead of delegating to the remote MCP server. Skills load via the `config`
hook (`skills.paths`) instead of being copied into the project `.opencode/`.

- Drop MCP: remove the regex-based MCP tool interception, the bundled
  opencode.json MCP registration, and the cli.ts installer (mem0-opencode bin).
- Native tools: add_memory, search_memories, get_memories, get_memory,
  update_memory, delete_memory, delete_all_memories, delete_entities,
  list_entities, get_event_status.
- Trim skills 16 -> 8 (context-loader, dream, forget, health, peek, pin,
  remember, tour); remove import, export, memory-reviewer, mem0, list-projects,
  switch-project, stats, onboard. De-MCP/onboard wording in kept skills.
- Telemetry: emit the full shared plugin.* schema (adds user_prompt, bash_error,
  pre_compact, session_stop alongside session_start, tool_use); tool_use now
  fires from inside each native tool; add project_hash + os_version props.
- Fix duplicate error-search (single topK:6); correct the documented
  experimental.chat.messages.transform hook name.
- Bump to 0.2.0.
2026-06-16 12:35:58 +05:30
Hrushikesh Yadav a2f01a8fcc fix: async delete_all aborts on first error, leaving partial deletion (#5529) 2026-06-16 11:59:33 +05:30
Hrushikesh Yadav 30d172e826 fix: omit None config values from Gemini GenerateContentConfig (#5528) 2026-06-16 11:54:42 +05:30
ly-wang19 bb69b036b5 fix(vector_stores): return None from get() for missing IDs (milvus/weaviate/supabase) (#5562)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-16 11:52:59 +05:30
Hrushikesh Yadav b55c51e004 fix(anthropic): tool_choice format and tool response parsing (#5537)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-15 17:28:45 +05:30
Harsh Vardhan Gupta 4492e75d04 fix(deps): bump esbuild >=0.28.1 across all npm packages (#5563)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:04:11 +05:30
Hrushikesh Yadav 4d949022f2 fix: preserve custom metadata fields during memory update (#5480) 2026-06-15 16:29:44 +05:30
ly-wang19 3ef034a9e4 fix(vector_stores): return None from ChromaDB.get() for missing IDs (#5561)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-15 16:11:03 +05:30
Yash Singh b90e3c0b76 fix(reranker): respect config.top_k in Cohere and ZeroEntropy fallback paths (#5560) 2026-06-15 16:10:00 +05:30
ly-wang19 a8eeddde64 fix(llms): honor reasoning-model params in AzureOpenAIStructuredLLM (#5548)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-15 16:07:19 +05:30
Hrushikesh Yadav 09a9e34382 fix(litellm): function-calling check blocks all calls on non-tool models (#5536) 2026-06-15 15:59:46 +05:30
anish 66c4394b40 fix(pyproject): rename vector_stores extra to vector-stores for PEP 503/508 compliance (#4934)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-15 12:38:23 +05:30
ly-wang19 32575a65fc fix(llms): honor reasoning-model params in OpenAIStructuredLLM (#5458)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-15 12:36:15 +05:30
Yash Singh 66901d7393 fix(llms): accept and forward **kwargs in Together/LangChain/Sarvam providers (#5556) 2026-06-15 12:23:04 +05:30
Hrushikesh Yadav a1eefc31bc fix(bedrock): use dict literal instead of set in AI21 response parse default (#5527) 2026-06-15 12:09:34 +05:30
Davide Leopardi de471799d1 fix(llms): send max_completion_tokens for the GPT-5 family across providers (#5547) 2026-06-15 12:04:19 +05:30
Rod Boev 3951ad4705 fix(openclaw): reduce skills-mode triage prompt footprint (#5502) 2026-06-15 11:18:39 +05:30
Kartik 9315e3036f chore: retire in-repo evaluation/ in favor of mem0ai/memory-benchmarks (#5520) 2026-06-14 00:43:02 +05:30
Kartik b3ede5b7c0 chore: update changelog, bump SDK and package versions to 3.0.8 and 2.0.6 (#5522) 2026-06-13 20:59:53 +05:30
youneshima 3553fc79dd feat(memory): add OSS-to-Platform notices (#5494) 2026-06-13 18:34:20 +05:30
Kartik f322cf82b9 chore: consolidate cookbooks/ into an indexed examples/ directory (#5517) 2026-06-13 18:25:50 +05:30
141 changed files with 12678 additions and 8874 deletions
+4
View File
@@ -0,0 +1,4 @@
[submodule "evaluation"]
path = evaluation
url = https://github.com/mem0ai/memory-benchmarks
branch = main
+14 -14
View File
@@ -12,7 +12,7 @@ This file provides context for AI coding assistants (Claude Code, Cursor, GitHub
## Repository Structure
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation.
### Key Directories
@@ -32,9 +32,8 @@ This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs,
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/`, `mem0-oss-to-platform/` |
| `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 |
| `evaluation/` | Submodule → [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) — benchmarking (LOCOMO, LongMemEval, BEAM) lives in that repo |
| `examples/` | Sample projects & runnable demos — apps, Chrome extension, multi-agent patterns, and Jupyter notebooks (`notebooks/`) |
| `pr-reviews/` | Pull request review materials |
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
@@ -247,18 +246,19 @@ make docs # or: cd docs && mintlify dev
- **API spec:** `docs/openapi.json`
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
### Evaluation (`evaluation/`)
### Evaluation / Benchmarking
Benchmarking lives in the external [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) repo (LOCOMO + LongMemEval + BEAM). The in-repo `evaluation/` path is a **git submodule** pinned to that repo's `main` — populate it with `git submodule update --init evaluation` (or clone mem0 with `--recurse-submodules`), or clone the benchmarks repo standalone:
```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
git clone https://github.com/mem0ai/memory-benchmarks.git
cd memory-benchmarks
pip install -r requirements.txt
# Run a benchmark (Mem0 Cloud; use docker compose for OSS)
python -m benchmarks.locomo.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY
python -m benchmarks.longmemeval.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --all-questions
python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --chat-sizes 100K --conversations 0-9
```
## Core APIs
+2 -1
View File
@@ -1026,7 +1026,8 @@ def get_user_preferences(user_id: str):
### AutoGen Integration
```python
from cookbooks.helper.mem0_teachability import Mem0Teachability
# Mem0Teachability lives in examples/notebooks/helper/ — see examples/notebooks/mem0-autogen.ipynb
from helper.mem0_teachability import Mem0Teachability
from mem0 import Memory
# Add memory capability to AutoGen agents
+10
View File
@@ -44,5 +44,15 @@
"vitest": "^4.1.0",
"@biomejs/biome": "^1.7.0",
"@types/node": "^20.0.0"
},
"pnpm": {
"overrides": {
"jws@4.0.0": "4.0.1",
"langsmith@<0.6.0": "^0.6.0",
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
"picomatch@<2.3.2": "^2.3.2",
"postcss@<8.5.10": ">=8.5.10",
"esbuild": ">=0.28.1"
}
}
}
+115 -399
View File
@@ -10,6 +10,7 @@ overrides:
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
picomatch@<2.3.2: ^2.3.2
postcss@<8.5.10: '>=8.5.10'
esbuild: '>=0.28.1'
importers:
@@ -77,28 +78,24 @@ packages:
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
@@ -116,314 +113,158 @@ packages:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.27.4':
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.27.4':
resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.27.4':
resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.27.4':
resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.27.4':
resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.27.4':
resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.27.4':
resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.4':
resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.27.4':
resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.27.4':
resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.27.4':
resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.27.4':
resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.27.4':
resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.27.4':
resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.27.4':
resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.27.4':
resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.27.4':
resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-arm64@0.27.4':
resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.4':
resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.27.4':
resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.4':
resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.12':
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/openharmony-arm64@0.27.4':
resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.27.4':
resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.27.4':
resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.27.4':
resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.27.4':
resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==}
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -475,79 +316,66 @@ packages:
resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.0':
resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.0':
resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.0':
resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.0':
resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.0':
resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.0':
resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.0':
resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.0':
resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.0':
resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.0':
resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.0':
resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.0':
resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.0':
resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==}
@@ -658,7 +486,7 @@ packages:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
esbuild: '>=0.28.1'
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
@@ -738,13 +566,8 @@ packages:
es-module-lexer@2.1.0:
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
hasBin: true
esbuild@0.27.4:
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
@@ -1164,160 +987,82 @@ snapshots:
'@colors/colors@1.5.0':
optional: true
'@esbuild/aix-ppc64@0.25.12':
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/aix-ppc64@0.27.4':
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm64@0.25.12':
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-arm64@0.27.4':
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/android-arm@0.25.12':
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.27.4':
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/android-x64@0.25.12':
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/android-x64@0.27.4':
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.25.12':
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.27.4':
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/darwin-x64@0.25.12':
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/darwin-x64@0.27.4':
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.27.4':
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.25.12':
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.27.4':
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-arm64@0.25.12':
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.27.4':
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.25.12':
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm@0.27.4':
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/linux-ia32@0.25.12':
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/linux-ia32@0.27.4':
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/linux-loong64@0.25.12':
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/linux-loong64@0.27.4':
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.25.12':
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.27.4':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.27.4':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
'@esbuild/linux-riscv64@0.27.4':
optional: true
'@esbuild/linux-s390x@0.25.12':
optional: true
'@esbuild/linux-s390x@0.27.4':
optional: true
'@esbuild/linux-x64@0.25.12':
optional: true
'@esbuild/linux-x64@0.27.4':
optional: true
'@esbuild/netbsd-arm64@0.25.12':
optional: true
'@esbuild/netbsd-arm64@0.27.4':
optional: true
'@esbuild/netbsd-x64@0.25.12':
optional: true
'@esbuild/netbsd-x64@0.27.4':
optional: true
'@esbuild/openbsd-arm64@0.25.12':
optional: true
'@esbuild/openbsd-arm64@0.27.4':
optional: true
'@esbuild/openbsd-x64@0.25.12':
optional: true
'@esbuild/openbsd-x64@0.27.4':
optional: true
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.27.4':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.27.4':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.27.4':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-ia32@0.27.4':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@esbuild/win32-x64@0.27.4':
'@esbuild/win32-x64@0.28.1':
optional: true
'@jridgewell/gen-mapping@0.3.13':
@@ -1492,9 +1237,9 @@ snapshots:
widest-line: 4.0.1
wrap-ansi: 8.1.0
bundle-require@5.1.0(esbuild@0.27.4):
bundle-require@5.1.0(esbuild@0.28.1):
dependencies:
esbuild: 0.27.4
esbuild: 0.28.1
load-tsconfig: 0.2.5
cac@6.7.14: {}
@@ -1547,63 +1292,34 @@ snapshots:
es-module-lexer@2.1.0: {}
esbuild@0.25.12:
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
'@esbuild/android-arm': 0.25.12
'@esbuild/android-arm64': 0.25.12
'@esbuild/android-x64': 0.25.12
'@esbuild/darwin-arm64': 0.25.12
'@esbuild/darwin-x64': 0.25.12
'@esbuild/freebsd-arm64': 0.25.12
'@esbuild/freebsd-x64': 0.25.12
'@esbuild/linux-arm': 0.25.12
'@esbuild/linux-arm64': 0.25.12
'@esbuild/linux-ia32': 0.25.12
'@esbuild/linux-loong64': 0.25.12
'@esbuild/linux-mips64el': 0.25.12
'@esbuild/linux-ppc64': 0.25.12
'@esbuild/linux-riscv64': 0.25.12
'@esbuild/linux-s390x': 0.25.12
'@esbuild/linux-x64': 0.25.12
'@esbuild/netbsd-arm64': 0.25.12
'@esbuild/netbsd-x64': 0.25.12
'@esbuild/openbsd-arm64': 0.25.12
'@esbuild/openbsd-x64': 0.25.12
'@esbuild/openharmony-arm64': 0.25.12
'@esbuild/sunos-x64': 0.25.12
'@esbuild/win32-arm64': 0.25.12
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
esbuild@0.27.4:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.4
'@esbuild/android-arm': 0.27.4
'@esbuild/android-arm64': 0.27.4
'@esbuild/android-x64': 0.27.4
'@esbuild/darwin-arm64': 0.27.4
'@esbuild/darwin-x64': 0.27.4
'@esbuild/freebsd-arm64': 0.27.4
'@esbuild/freebsd-x64': 0.27.4
'@esbuild/linux-arm': 0.27.4
'@esbuild/linux-arm64': 0.27.4
'@esbuild/linux-ia32': 0.27.4
'@esbuild/linux-loong64': 0.27.4
'@esbuild/linux-mips64el': 0.27.4
'@esbuild/linux-ppc64': 0.27.4
'@esbuild/linux-riscv64': 0.27.4
'@esbuild/linux-s390x': 0.27.4
'@esbuild/linux-x64': 0.27.4
'@esbuild/netbsd-arm64': 0.27.4
'@esbuild/netbsd-x64': 0.27.4
'@esbuild/openbsd-arm64': 0.27.4
'@esbuild/openbsd-x64': 0.27.4
'@esbuild/openharmony-arm64': 0.27.4
'@esbuild/sunos-x64': 0.27.4
'@esbuild/win32-arm64': 0.27.4
'@esbuild/win32-ia32': 0.27.4
'@esbuild/win32-x64': 0.27.4
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
estree-walker@3.0.3:
dependencies:
@@ -1840,12 +1556,12 @@ snapshots:
tsup@8.5.1(postcss@8.5.15)(tsx@4.21.0)(typescript@5.9.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.4)
bundle-require: 5.1.0(esbuild@0.28.1)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3
esbuild: 0.27.4
esbuild: 0.28.1
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
@@ -1868,7 +1584,7 @@ snapshots:
tsx@4.21.0:
dependencies:
esbuild: 0.27.4
esbuild: 0.28.1
get-tsconfig: 4.13.7
optionalDependencies:
fsevents: 2.3.3
@@ -1883,7 +1599,7 @@ snapshots:
vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0):
dependencies:
esbuild: 0.25.12
esbuild: 0.28.1
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.15
+1
View File
@@ -11,3 +11,4 @@ overrides:
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
picomatch@<2.3.2: ^2.3.2
"postcss@<8.5.10": ">=8.5.10"
"esbuild": ">=0.28.1"
+27
View File
@@ -7,6 +7,22 @@ mode: "wide"
<Tabs>
<Tab title="Python">
<Update label="2026-06-13" description="v2.0.6">
**New Features:**
- **Memory:** Add a contextual OSS-to-Platform notices system that surfaces occasional, situation-aware messages (first run, scale/performance thresholds, slow queries, and when temporal/decay features are relevant) pointing to the corresponding Mem0 Platform capabilities; disable via `MEM0_TELEMETRY=false` ([#5494](https://github.com/mem0ai/mem0/pull/5494))
**Bug Fixes:**
- **Memory:** Prevent a crash in `parse_vision_messages` when vision support is disabled ([#5487](https://github.com/mem0ai/mem0/pull/5487))
- **Vector Stores:** Expose the `https` option on the Qdrant vector store configuration so TLS endpoints can be targeted explicitly ([#5380](https://github.com/mem0ai/mem0/pull/5380))
- **Vector Stores:** Use valid S3 Vectors entity index names, fixing index operations that failed on invalid names ([#5416](https://github.com/mem0ai/mem0/pull/5416))
- **Vector Stores:** Fix `search()` crashing with a `TypeError` in the LangChain vector store when a result score is `None` ([#5072](https://github.com/mem0ai/mem0/pull/5072))
- **Vector Stores:** Use `is not None` instead of a truthiness check for vector/payload in the PGVector `update()` path, so valid empty/zero values are no longer skipped ([#5488](https://github.com/mem0ai/mem0/pull/5488))
- **Vector Stores:** Index the Valkey `memory` field as `TEXT` rather than `TAG` so full-text search behaves correctly ([#5443](https://github.com/mem0ai/mem0/pull/5443))
- **Vector Stores:** Implement `$not` filter support in the ChromaDB vector store ([#5485](https://github.com/mem0ai/mem0/pull/5485))
</Update>
<Update label="2026-06-10" description="v2.0.5">
**New Features:**
@@ -960,6 +976,17 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
<Tab title="TypeScript">
<Update label="2026-06-13" description="v3.0.8">
**New Features:**
- **Memory:** Add a contextual OSS-to-Platform notices system that surfaces occasional, situation-aware messages (first run, scale/performance thresholds, slow queries, and when temporal/decay features are relevant) pointing to the corresponding Mem0 Platform capabilities; disable via `MEM0_TELEMETRY=false` ([#5494](https://github.com/mem0ai/mem0/pull/5494))
**Security:**
- **Dependencies:** Upgrade `@langchain/community` to `^1.1.18` to remediate CVE-2026-27795 and CVE-2026-26019 ([#5510](https://github.com/mem0ai/mem0/pull/5510))
- **Dependencies:** Resolve all open MEDIUM Dependabot alerts via pnpm overrides ([#5489](https://github.com/mem0ai/mem0/pull/5489))
</Update>
<Update label="2026-06-10" description="v3.0.7">
**New Features:**
+13 -22
View File
@@ -1,6 +1,6 @@
---
title: OpenCode
description: "Add persistent memory to OpenCode with the Mem0 plugin — MCP server, lifecycle hooks, and slash commands."
description: "Add persistent memory to OpenCode with the Mem0 plugin — native SDK-backed memory tools, lifecycle hooks, and skills."
---
Add persistent memory to [**OpenCode**](https://opencode.ai) with the Mem0 plugin. Your agent forgets everything between sessions — Mem0 fixes that by storing decisions, preferences, and learnings so they carry over automatically.
@@ -30,27 +30,17 @@ echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc && source ~/.bashrc
opencode plugin @mem0/opencode-plugin
```
Or using this command which does the same thing:
```bash
bunx @mem0/opencode-plugin@latest install
```
**Or let your agent do it** — paste this into OpenCode:
```
Install @mem0/opencode-plugin by following https://raw.githubusercontent.com/mem0ai/mem0/main/integrations/mem0-plugin/.opencode-plugin/README.md
```
All commands auto-add the plugin and MCP server to your `~/.config/opencode/opencode.json`. Restart OpenCode — you get the MCP server, lifecycle hooks, and all `/mem0:` slash commands.
This adds the plugin to your `~/.config/opencode/opencode.json`. Restart OpenCode — you get the native memory tools, lifecycle hooks, and all `/mem0:` slash commands. The memory tools are registered by the plugin itself via the `mem0ai` SDK — no MCP server to configure.
### Option B — MCP Only
### Option B — Standalone MCP Server
If you only need the memory tools without hooks or skills, add this to your `opencode.json` (project-level or global at `~/.config/opencode/opencode.json`):
If you only need the memory tools without the plugin's hooks or skills, point OpenCode at Mem0's hosted MCP server directly. Add this to your `opencode.json` (project-level or global at `~/.config/opencode/opencode.json`):
```json
{
@@ -69,13 +59,13 @@ If you only need the memory tools without hooks or skills, add this to your `ope
## What's Included
| Component | Plugin (A) | MCP Only (B) |
|-----------|:----------:|:------------:|
| MCP Server (9 memory tools) | Yes | Yes |
| Component | Plugin (A) | Standalone MCP (B) |
|-----------|:----------:|:------------------:|
| 9 memory tools | Native (SDK) | Remote MCP server |
| Lifecycle Hooks | Yes | No |
| 16 Slash Commands | Yes | No |
| 8 Skills | Yes | No |
## Available MCP Tools
## Available Memory Tools
| Tool | Description |
|------|-------------|
@@ -95,10 +85,11 @@ The plugin uses the [mem0ai](https://www.npmjs.com/package/mem0ai) TypeScript SD
| OpenCode Event | Hook | What happens |
|----------------|------|-------------|
| `config` | **Config** | Registers the bundled skills (`skills.paths`) and `/mem0:*` slash commands at startup |
| `chat.message` | **Chat message** | Searches prior memories on session start, searches relevant memories before each prompt, auto-captures learnings periodically |
| `tool.execute.before` | **Pre-tool** | Blocks MEMORY.md writes, injects `user_id`/`app_id` on mem0 tool calls |
| `tool.execute.after` | **Post-tool** | Tracks stats, scans Bash errors and pre-fetches related error memories |
| `experimental.chat.system.transform` | **System transform** | Injects memory context (session memories, search results, error lookups) into the system prompt |
| `tool.execute.before` | **Pre-tool** | Blocks MEMORY.md writes, steering them to the `add_memory` tool |
| `tool.execute.after` | **Post-tool** | Scans Bash errors and pre-fetches related error memories |
| `experimental.chat.messages.transform` | **Messages transform** | Injects memory context (session memories, search results, error lookups) into the prompt |
| `experimental.session.compacting` | **Compaction** | Stores session state memory, then injects prior memories into compaction context so nothing is lost |
| `shell.env` | **Shell env** | Exports `MEM0_USER_ID`, `MEM0_APP_ID`, `MEM0_SESSION_ID`, and `MEM0_BRANCH` to all shell executions |
Submodule
+1
Submodule evaluation added at 4b61c5d31b
-31
View File
@@ -1,31 +0,0 @@
# Run the experiments
run-mem0-add:
python run_experiments.py --technique_type mem0 --method add
run-mem0-search:
python run_experiments.py --technique_type mem0 --method search --output_folder results/ --top_k 30
run-mem0-plus-add:
python run_experiments.py --technique_type mem0 --method add --is_graph
run-mem0-plus-search:
python run_experiments.py --technique_type mem0 --method search --is_graph --output_folder results/ --top_k 30
run-rag:
python run_experiments.py --technique_type rag --chunk_size 500 --num_chunks 1 --output_folder results/
run-full-context:
python run_experiments.py --technique_type rag --chunk_size -1 --num_chunks 1 --output_folder results/
run-langmem:
python run_experiments.py --technique_type langmem --output_folder results/
run-zep-add:
python run_experiments.py --technique_type zep --method add --output_folder results/
run-zep-search:
python run_experiments.py --technique_type zep --method search --output_folder results/
run-openai:
python run_experiments.py --technique_type openai --output_folder results/
-198
View File
@@ -1,198 +0,0 @@
# Mem0: Building Production‑Ready AI Agents with Scalable Long‑Term Memory
[![arXiv](https://img.shields.io/badge/arXiv-Paper-b31b1b.svg)](https://arxiv.org/abs/2504.19413)
[![Website](https://img.shields.io/badge/Website-Project-blue)](https://mem0.ai/research)
This repository contains the code and dataset for our paper: **Mem0: Building Production‑Ready AI Agents with Scalable Long‑Term Memory**.
## 📋 Overview
This project evaluates Mem0 and compares it with different memory and retrieval techniques for AI systems:
1. **Established LOCOMO Benchmarks**: We evaluate against five established approaches from the literature: LoCoMo, ReadAgent, MemoryBank, MemGPT, and A-Mem.
2. **Open-Source Memory Solutions**: We test promising open-source memory architectures including LangMem, which provides flexible memory management capabilities.
3. **RAG Systems**: We implement Retrieval-Augmented Generation with various configurations, testing different chunk sizes and retrieval counts to optimize performance.
4. **Full-Context Processing**: We examine the effectiveness of passing the entire conversation history within the context window of the LLM as a baseline approach.
5. **Proprietary Memory Systems**: We evaluate OpenAI's built-in memory feature available in their ChatGPT interface to compare against commercial solutions.
6. **Third-Party Memory Providers**: We incorporate Zep, a specialized memory management platform designed for AI agents, to assess the performance of dedicated memory infrastructure.
We test these techniques on the LOCOMO dataset, which contains conversational data with various question types to evaluate memory recall and understanding.
## 🔍 Dataset
The LOCOMO dataset used in our experiments can be downloaded from our Google Drive repository:
[Download LOCOMO Dataset](https://drive.google.com/drive/folders/1L-cTjTm0ohMsitsHg4dijSPJtqNflwX-?usp=drive_link)
The dataset contains conversational data specifically designed to test memory recall and understanding across various question types and complexity levels.
Place the dataset files in the `dataset/` directory:
- `locomo10.json`: Original dataset
- `locomo10_rag.json`: Dataset formatted for RAG experiments
## 📁 Project Structure
```
.
├── src/ # Source code for different memory techniques
│ ├── mem0/ # Implementation of the Mem0 technique
│ ├── openai/ # Implementation of the OpenAI memory
│ ├── zep/ # Implementation of the Zep memory
│ ├── rag.py # Implementation of the RAG technique
│ └── langmem.py # Implementation of the Language-based memory
├── metrics/ # Code for evaluation metrics
├── results/ # Results of experiments
├── dataset/ # Dataset files
├── evals.py # Evaluation script
├── run_experiments.py # Script to run experiments
├── generate_scores.py # Script to generate scores from results
└── prompts.py # Prompts used for the models
```
## 🚀 Getting Started
### Prerequisites
Create a `.env` file with your API keys and configurations. The following keys are required:
```
# OpenAI API key for GPT models and embeddings
OPENAI_API_KEY="your-openai-api-key"
# Mem0 API keys (for Mem0 and Mem0+ techniques)
MEM0_API_KEY="your-mem0-api-key"
MEM0_PROJECT_ID="your-mem0-project-id"
MEM0_ORGANIZATION_ID="your-mem0-organization-id"
# Model configuration
MODEL="gpt-4o-mini" # or your preferred model
EMBEDDING_MODEL="text-embedding-3-small" # or your preferred embedding model
ZEP_API_KEY="api-key-from-zep"
```
### Running Experiments
You can run experiments using the provided Makefile commands:
#### Memory Techniques
```bash
# Run Mem0 experiments
make run-mem0-add # Add memories using Mem0
make run-mem0-search # Search memories using Mem0
# Run Mem0+ experiments (with graph-based search)
make run-mem0-plus-add # Add memories using Mem0+
make run-mem0-plus-search # Search memories using Mem0+
# Run RAG experiments
make run-rag # Run RAG with chunk size 500
make run-full-context # Run RAG with full context
# Run LangMem experiments
make run-langmem # Run LangMem
# Run Zep experiments
make run-zep-add # Add memories using Zep
make run-zep-search # Search memories using Zep
# Run OpenAI experiments
make run-openai # Run OpenAI experiments
```
Alternatively, you can run experiments directly with custom parameters:
```bash
python run_experiments.py --technique_type [mem0|rag|langmem] [additional parameters]
```
#### Command-line Parameters:
| Parameter | Description | Default |
|-----------|-------------|---------|
| `--technique_type` | Memory technique to use (mem0, rag, langmem) | mem0 |
| `--method` | Method to use (add, search) | add |
| `--chunk_size` | Chunk size for processing | 1000 |
| `--top_k` | Number of top memories to retrieve | 30 |
| `--filter_memories` | Whether to filter memories | False |
| `--is_graph` | Whether to use graph-based search | False |
| `--num_chunks` | Number of chunks to process for RAG | 1 |
### 📊 Evaluation
To evaluate results, run:
```bash
python evals.py --input_file [path_to_results] --output_file [output_path]
```
This script:
1. Processes each question-answer pair
2. Calculates BLEU and F1 scores automatically
3. Uses an LLM judge to evaluate answer correctness
4. Saves the combined results to the output file
### 📈 Generating Scores
Generate final scores with:
```bash
python generate_scores.py
```
This script:
1. Loads the evaluation metrics data
2. Calculates mean scores for each category (BLEU, F1, LLM)
3. Reports the number of questions per category
4. Calculates overall mean scores across all categories
Example output:
```
Mean Scores Per Category:
bleu_score f1_score llm_score count
category
1 0.xxxx 0.xxxx 0.xxxx xx
2 0.xxxx 0.xxxx 0.xxxx xx
3 0.xxxx 0.xxxx 0.xxxx xx
Overall Mean Scores:
bleu_score 0.xxxx
f1_score 0.xxxx
llm_score 0.xxxx
```
## 📏 Evaluation Metrics
We use several metrics to evaluate the performance of different memory techniques:
1. **BLEU Score**: Measures the similarity between the model's response and the ground truth
2. **F1 Score**: Measures the harmonic mean of precision and recall
3. **LLM Score**: A binary score (0 or 1) determined by an LLM judge evaluating the correctness of responses
4. **Token Consumption**: Number of tokens required to generate final answer.
5. **Latency**: Time required during search and to generate response.
## 📚 Citation
If you use this code or dataset in your research, please cite our paper:
```bibtex
@article{mem0,
title={Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory},
author={Chhikara, Prateek and Khant, Dev and Aryan, Saket and Singh, Taranjeet and Yadav, Deshraj},
journal={arXiv preprint arXiv:2504.19413},
year={2025}
}
```
## 📄 License
[MIT License](LICENSE)
## 👥 Contributors
- [Prateek Chhikara](https://github.com/prateekchhikara)
- [Dev Khant](https://github.com/Dev-Khant)
- [Saket Aryan](https://github.com/whysosaket)
- [Taranjeet Singh](https://github.com/taranjeet)
- [Deshraj Yadav](https://github.com/deshraj)
-81
View File
@@ -1,81 +0,0 @@
import argparse
import concurrent.futures
import json
import threading
from collections import defaultdict
from metrics.llm_judge import evaluate_llm_judge
from metrics.utils import calculate_bleu_scores, calculate_metrics
from tqdm import tqdm
def process_item(item_data):
k, v = item_data
local_results = defaultdict(list)
for item in v:
gt_answer = str(item["answer"])
pred_answer = str(item["response"])
category = str(item["category"])
question = str(item["question"])
# Skip category 5
if category == "5":
continue
metrics = calculate_metrics(pred_answer, gt_answer)
bleu_scores = calculate_bleu_scores(pred_answer, gt_answer)
llm_score = evaluate_llm_judge(question, gt_answer, pred_answer)
local_results[k].append(
{
"question": question,
"answer": gt_answer,
"response": pred_answer,
"category": category,
"bleu_score": bleu_scores["bleu1"],
"f1_score": metrics["f1"],
"llm_score": llm_score,
}
)
return local_results
def main():
parser = argparse.ArgumentParser(description="Evaluate RAG results")
parser.add_argument(
"--input_file", type=str, default="results/rag_results_500_k1.json", help="Path to the input dataset file"
)
parser.add_argument(
"--output_file", type=str, default="evaluation_metrics.json", help="Path to save the evaluation results"
)
parser.add_argument("--max_workers", type=int, default=10, help="Maximum number of worker threads")
args = parser.parse_args()
with open(args.input_file, "r") as f:
data = json.load(f)
results = defaultdict(list)
results_lock = threading.Lock()
# Use ThreadPoolExecutor with specified workers
with concurrent.futures.ThreadPoolExecutor(max_workers=args.max_workers) as executor:
futures = [executor.submit(process_item, item_data) for item_data in data.items()]
for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures)):
local_results = future.result()
with results_lock:
for k, items in local_results.items():
results[k].extend(items)
# Save results to JSON file
with open(args.output_file, "w") as f:
json.dump(results, f, indent=4)
print(f"Results saved to {args.output_file}")
if __name__ == "__main__":
main()
-34
View File
@@ -1,34 +0,0 @@
import json
import pandas as pd
# Load the evaluation metrics data
with open("evaluation_metrics.json", "r") as f:
data = json.load(f)
# Flatten the data into a list of question items
all_items = []
for key in data:
all_items.extend(data[key])
# Convert to DataFrame
df = pd.DataFrame(all_items)
# Convert category to numeric type
df["category"] = pd.to_numeric(df["category"])
# Calculate mean scores by category
result = df.groupby("category").agg({"bleu_score": "mean", "f1_score": "mean", "llm_score": "mean"}).round(4)
# Add count of questions per category
result["count"] = df.groupby("category").size()
# Print the results
print("Mean Scores Per Category:")
print(result)
# Calculate overall means
overall_means = df.agg({"bleu_score": "mean", "f1_score": "mean", "llm_score": "mean"}).round(4)
print("\nOverall Mean Scores:")
print(overall_means)
-130
View File
@@ -1,130 +0,0 @@
import argparse
import json
from collections import defaultdict
import numpy as np
from openai import OpenAI
from mem0.memory.utils import extract_json
client = OpenAI()
ACCURACY_PROMPT = """
Your task is to label an answer to a question as ’CORRECT’ or ’WRONG’. You will be given the following data:
(1) a question (posed by one user to another user),
(2) a ’gold’ (ground truth) answer,
(3) a generated answer
which you will score as CORRECT/WRONG.
The point of the question is to ask about something one user should know about the other user based on their prior conversations.
The gold answer will usually be a concise and short answer that includes the referenced topic, for example:
Question: Do you remember what I got the last time I went to Hawaii?
Gold answer: A shell necklace
The generated answer might be much longer, but you should be generous with your grading - as long as it touches on the same topic as the gold answer, it should be counted as CORRECT.
For time related questions, the gold answer will be a specific date, month, year, etc. The generated answer might be much longer or use relative time references (like "last Tuesday" or "next month"), but you should be generous with your grading - as long as it refers to the same date or time period as the gold answer, it should be counted as CORRECT. Even if the format differs (e.g., "May 7th" vs "7 May"), consider it CORRECT if it's the same date.
Now it's time for the real question:
Question: {question}
Gold answer: {gold_answer}
Generated answer: {generated_answer}
First, provide a short (one sentence) explanation of your reasoning, then finish with CORRECT or WRONG.
Do NOT include both CORRECT and WRONG in your response, or it will break the evaluation script.
Just return the label CORRECT or WRONG in a json format with the key as "label".
"""
def evaluate_llm_judge(question, gold_answer, generated_answer):
"""Evaluate the generated answer against the gold answer using an LLM judge."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": ACCURACY_PROMPT.format(
question=question, gold_answer=gold_answer, generated_answer=generated_answer
),
}
],
response_format={"type": "json_object"},
temperature=0.0,
)
label = json.loads(extract_json(response.choices[0].message.content))["label"]
return 1 if label == "CORRECT" else 0
def main():
"""Main function to evaluate RAG results using LLM judge."""
parser = argparse.ArgumentParser(description="Evaluate RAG results using LLM judge")
parser.add_argument(
"--input_file",
type=str,
default="results/default_run_v4_k30_new_graph.json",
help="Path to the input dataset file",
)
args = parser.parse_args()
dataset_path = args.input_file
output_path = f"results/llm_judge_{dataset_path.split('/')[-1]}"
with open(dataset_path, "r") as f:
data = json.load(f)
LLM_JUDGE = defaultdict(list)
RESULTS = defaultdict(list)
index = 0
for k, v in data.items():
for x in v:
question = x["question"]
gold_answer = x["answer"]
generated_answer = x["response"]
category = x["category"]
# Skip category 5
if int(category) == 5:
continue
# Evaluate the answer
label = evaluate_llm_judge(question, gold_answer, generated_answer)
LLM_JUDGE[category].append(label)
# Store the results
RESULTS[index].append(
{
"question": question,
"gt_answer": gold_answer,
"response": generated_answer,
"category": category,
"llm_label": label,
}
)
# Save intermediate results
with open(output_path, "w") as f:
json.dump(RESULTS, f, indent=4)
# Print current accuracy for all categories
print("All categories accuracy:")
for cat, results in LLM_JUDGE.items():
if results: # Only print if there are results for this category
print(f" Category {cat}: {np.mean(results):.4f} ({sum(results)}/{len(results)})")
print("------------------------------------------")
index += 1
# Save final results
with open(output_path, "w") as f:
json.dump(RESULTS, f, indent=4)
# Print final summary
print("PATH: ", dataset_path)
print("------------------------------------------")
for k, v in LLM_JUDGE.items():
print(k, np.mean(v))
if __name__ == "__main__":
main()
-211
View File
@@ -1,211 +0,0 @@
"""
Borrowed from https://github.com/WujiangXu/AgenticMemory/blob/main/utils.py
@article{xu2025mem,
title={A-mem: Agentic memory for llm agents},
author={Xu, Wujiang and Liang, Zujie and Mei, Kai and Gao, Hang and Tan, Juntao
and Zhang, Yongfeng},
journal={arXiv preprint arXiv:2502.12110},
year={2025}
}
"""
import statistics
from collections import defaultdict
from typing import Dict, List, Union
import nltk
from bert_score import score as bert_score
from nltk.translate.bleu_score import SmoothingFunction, sentence_bleu
from nltk.translate.meteor_score import meteor_score
from rouge_score import rouge_scorer
from sentence_transformers import SentenceTransformer
# from load_dataset import load_locomo_dataset, QA, Turn, Session, Conversation
from sentence_transformers.util import pytorch_cos_sim
# Download required NLTK data
try:
nltk.download("punkt", quiet=True)
nltk.download("wordnet", quiet=True)
except Exception as e:
print(f"Error downloading NLTK data: {e}")
# Initialize SentenceTransformer model (this will be reused)
try:
sentence_model = SentenceTransformer("all-MiniLM-L6-v2")
except Exception as e:
print(f"Warning: Could not load SentenceTransformer model: {e}")
sentence_model = None
def simple_tokenize(text):
"""Simple tokenization function."""
# Convert to string if not already
text = str(text)
return text.lower().replace(".", " ").replace(",", " ").replace("!", " ").replace("?", " ").split()
def calculate_rouge_scores(prediction: str, reference: str) -> Dict[str, float]:
"""Calculate ROUGE scores for prediction against reference."""
scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"], use_stemmer=True)
scores = scorer.score(reference, prediction)
return {
"rouge1_f": scores["rouge1"].fmeasure,
"rouge2_f": scores["rouge2"].fmeasure,
"rougeL_f": scores["rougeL"].fmeasure,
}
def calculate_bleu_scores(prediction: str, reference: str) -> Dict[str, float]:
"""Calculate BLEU scores with different n-gram settings."""
pred_tokens = nltk.word_tokenize(prediction.lower())
ref_tokens = [nltk.word_tokenize(reference.lower())]
weights_list = [(1, 0, 0, 0), (0.5, 0.5, 0, 0), (0.33, 0.33, 0.33, 0), (0.25, 0.25, 0.25, 0.25)]
smooth = SmoothingFunction().method1
scores = {}
for n, weights in enumerate(weights_list, start=1):
try:
score = sentence_bleu(ref_tokens, pred_tokens, weights=weights, smoothing_function=smooth)
except Exception as e:
print(f"Error calculating BLEU score: {e}")
score = 0.0
scores[f"bleu{n}"] = score
return scores
def calculate_bert_scores(prediction: str, reference: str) -> Dict[str, float]:
"""Calculate BERTScore for semantic similarity."""
try:
P, R, F1 = bert_score([prediction], [reference], lang="en", verbose=False)
return {"bert_precision": P.item(), "bert_recall": R.item(), "bert_f1": F1.item()}
except Exception as e:
print(f"Error calculating BERTScore: {e}")
return {"bert_precision": 0.0, "bert_recall": 0.0, "bert_f1": 0.0}
def calculate_meteor_score(prediction: str, reference: str) -> float:
"""Calculate METEOR score for the prediction."""
try:
return meteor_score([reference.split()], prediction.split())
except Exception as e:
print(f"Error calculating METEOR score: {e}")
return 0.0
def calculate_sentence_similarity(prediction: str, reference: str) -> float:
"""Calculate sentence embedding similarity using SentenceBERT."""
if sentence_model is None:
return 0.0
try:
# Encode sentences
embedding1 = sentence_model.encode([prediction], convert_to_tensor=True)
embedding2 = sentence_model.encode([reference], convert_to_tensor=True)
# Calculate cosine similarity
similarity = pytorch_cos_sim(embedding1, embedding2).item()
return float(similarity)
except Exception as e:
print(f"Error calculating sentence similarity: {e}")
return 0.0
def calculate_metrics(prediction: str, reference: str) -> Dict[str, float]:
"""Calculate comprehensive evaluation metrics for a prediction."""
# Handle empty or None values
if not prediction or not reference:
return {
"exact_match": 0,
"f1": 0.0,
"rouge1_f": 0.0,
"rouge2_f": 0.0,
"rougeL_f": 0.0,
"bleu1": 0.0,
"bleu2": 0.0,
"bleu3": 0.0,
"bleu4": 0.0,
"bert_f1": 0.0,
"meteor": 0.0,
"sbert_similarity": 0.0,
}
# Convert to strings if they're not already
prediction = str(prediction).strip()
reference = str(reference).strip()
# Calculate exact match
exact_match = int(prediction.lower() == reference.lower())
# Calculate token-based F1 score
pred_tokens = set(simple_tokenize(prediction))
ref_tokens = set(simple_tokenize(reference))
common_tokens = pred_tokens & ref_tokens
if not pred_tokens or not ref_tokens:
f1 = 0.0
else:
precision = len(common_tokens) / len(pred_tokens)
recall = len(common_tokens) / len(ref_tokens)
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
# Calculate all scores
bleu_scores = calculate_bleu_scores(prediction, reference)
# Combine all metrics
metrics = {
"exact_match": exact_match,
"f1": f1,
**bleu_scores,
}
return metrics
def aggregate_metrics(
all_metrics: List[Dict[str, float]], all_categories: List[int]
) -> Dict[str, Dict[str, Union[float, Dict[str, float]]]]:
"""Calculate aggregate statistics for all metrics, split by category."""
if not all_metrics:
return {}
# Initialize aggregates for overall and per-category metrics
aggregates = defaultdict(list)
category_aggregates = defaultdict(lambda: defaultdict(list))
# Collect all values for each metric, both overall and per category
for metrics, category in zip(all_metrics, all_categories):
for metric_name, value in metrics.items():
aggregates[metric_name].append(value)
category_aggregates[category][metric_name].append(value)
# Calculate statistics for overall metrics
results = {"overall": {}}
for metric_name, values in aggregates.items():
results["overall"][metric_name] = {
"mean": statistics.mean(values),
"std": statistics.stdev(values) if len(values) > 1 else 0.0,
"median": statistics.median(values),
"min": min(values),
"max": max(values),
"count": len(values),
}
# Calculate statistics for each category
for category in sorted(category_aggregates.keys()):
results[f"category_{category}"] = {}
for metric_name, values in category_aggregates[category].items():
if values: # Only calculate if we have values for this category
results[f"category_{category}"][metric_name] = {
"mean": statistics.mean(values),
"std": statistics.stdev(values) if len(values) > 1 else 0.0,
"median": statistics.median(values),
"min": min(values),
"max": max(values),
"count": len(values),
}
return results
-147
View File
@@ -1,147 +0,0 @@
ANSWER_PROMPT_GRAPH = """
You are an intelligent memory assistant tasked with retrieving accurate information from
conversation memories.
# CONTEXT:
You have access to memories from two speakers in a conversation. These memories contain
timestamped information that may be relevant to answering the question. You also have
access to knowledge graph relations for each user, showing connections between entities,
concepts, and events relevant to that user.
# INSTRUCTIONS:
1. Carefully analyze all provided memories from both speakers
2. Pay special attention to the timestamps to determine the answer
3. If the question asks about a specific event or fact, look for direct evidence in the
memories
4. If the memories contain contradictory information, prioritize the most recent memory
5. If there is a question about time references (like "last year", "two months ago",
etc.), calculate the actual date based on the memory timestamp. For example, if a
memory from 4 May 2022 mentions "went to India last year," then the trip occurred
in 2021.
6. Always convert relative time references to specific dates, months, or years. For
example, convert "last year" to "2022" or "two months ago" to "March 2023" based
on the memory timestamp. Ignore the reference while answering the question.
7. Focus only on the content of the memories from both speakers. Do not confuse
character names mentioned in memories with the actual users who created those
memories.
8. The answer should be less than 5-6 words.
9. Use the knowledge graph relations to understand the user's knowledge network and
identify important relationships between entities in the user's world.
# APPROACH (Think step by step):
1. First, examine all memories that contain information related to the question
2. Examine the timestamps and content of these memories carefully
3. Look for explicit mentions of dates, times, locations, or events that answer the
question
4. If the answer requires calculation (e.g., converting relative time references),
show your work
5. Analyze the knowledge graph relations to understand the user's knowledge context
6. Formulate a precise, concise answer based solely on the evidence in the memories
7. Double-check that your answer directly addresses the question asked
8. Ensure your final answer is specific and avoids vague time references
Memories for user {{speaker_1_user_id}}:
{{speaker_1_memories}}
Relations for user {{speaker_1_user_id}}:
{{speaker_1_graph_memories}}
Memories for user {{speaker_2_user_id}}:
{{speaker_2_memories}}
Relations for user {{speaker_2_user_id}}:
{{speaker_2_graph_memories}}
Question: {{question}}
Answer:
"""
ANSWER_PROMPT = """
You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories.
# CONTEXT:
You have access to memories from two speakers in a conversation. These memories contain
timestamped information that may be relevant to answering the question.
# INSTRUCTIONS:
1. Carefully analyze all provided memories from both speakers
2. Pay special attention to the timestamps to determine the answer
3. If the question asks about a specific event or fact, look for direct evidence in the memories
4. If the memories contain contradictory information, prioritize the most recent memory
5. If there is a question about time references (like "last year", "two months ago", etc.),
calculate the actual date based on the memory timestamp. For example, if a memory from
4 May 2022 mentions "went to India last year," then the trip occurred in 2021.
6. Always convert relative time references to specific dates, months, or years. For example,
convert "last year" to "2022" or "two months ago" to "March 2023" based on the memory
timestamp. Ignore the reference while answering the question.
7. Focus only on the content of the memories from both speakers. Do not confuse character
names mentioned in memories with the actual users who created those memories.
8. The answer should be less than 5-6 words.
# APPROACH (Think step by step):
1. First, examine all memories that contain information related to the question
2. Examine the timestamps and content of these memories carefully
3. Look for explicit mentions of dates, times, locations, or events that answer the question
4. If the answer requires calculation (e.g., converting relative time references), show your work
5. Formulate a precise, concise answer based solely on the evidence in the memories
6. Double-check that your answer directly addresses the question asked
7. Ensure your final answer is specific and avoids vague time references
Memories for user {{speaker_1_user_id}}:
{{speaker_1_memories}}
Memories for user {{speaker_2_user_id}}:
{{speaker_2_memories}}
Question: {{question}}
Answer:
"""
ANSWER_PROMPT_ZEP = """
You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories.
# CONTEXT:
You have access to memories from a conversation. These memories contain
timestamped information that may be relevant to answering the question.
# INSTRUCTIONS:
1. Carefully analyze all provided memories
2. Pay special attention to the timestamps to determine the answer
3. If the question asks about a specific event or fact, look for direct evidence in the memories
4. If the memories contain contradictory information, prioritize the most recent memory
5. If there is a question about time references (like "last year", "two months ago", etc.),
calculate the actual date based on the memory timestamp. For example, if a memory from
4 May 2022 mentions "went to India last year," then the trip occurred in 2021.
6. Always convert relative time references to specific dates, months, or years. For example,
convert "last year" to "2022" or "two months ago" to "March 2023" based on the memory
timestamp. Ignore the reference while answering the question.
7. Focus only on the content of the memories. Do not confuse character
names mentioned in memories with the actual users who created those memories.
8. The answer should be less than 5-6 words.
# APPROACH (Think step by step):
1. First, examine all memories that contain information related to the question
2. Examine the timestamps and content of these memories carefully
3. Look for explicit mentions of dates, times, locations, or events that answer the question
4. If the answer requires calculation (e.g., converting relative time references), show your work
5. Formulate a precise, concise answer based solely on the evidence in the memories
6. Double-check that your answer directly addresses the question asked
7. Ensure your final answer is specific and avoids vague time references
Memories:
{{memories}}
Question: {{question}}
Answer:
"""
-75
View File
@@ -1,75 +0,0 @@
import argparse
import os
from src.langmem import LangMemManager
from src.memzero.add import MemoryADD
from src.memzero.search import MemorySearch
from src.openai.predict import OpenAIPredict
from src.rag import RAGManager
from src.utils import METHODS, TECHNIQUES
from src.zep.add import ZepAdd
from src.zep.search import ZepSearch
class Experiment:
def __init__(self, technique_type, chunk_size):
self.technique_type = technique_type
self.chunk_size = chunk_size
def run(self):
print(f"Running experiment with technique: {self.technique_type}, chunk size: {self.chunk_size}")
def main():
parser = argparse.ArgumentParser(description="Run memory experiments")
parser.add_argument("--technique_type", choices=TECHNIQUES, default="mem0", help="Memory technique to use")
parser.add_argument("--method", choices=METHODS, default="add", help="Method to use")
parser.add_argument("--chunk_size", type=int, default=1000, help="Chunk size for processing")
parser.add_argument("--output_folder", type=str, default="results/", help="Output path for results")
parser.add_argument("--top_k", type=int, default=30, help="Number of top memories to retrieve")
parser.add_argument("--filter_memories", action="store_true", default=False, help="Whether to filter memories")
parser.add_argument("--is_graph", action="store_true", default=False, help="Whether to use graph-based search")
parser.add_argument("--num_chunks", type=int, default=1, help="Number of chunks to process")
args = parser.parse_args()
# Add your experiment logic here
print(f"Running experiments with technique: {args.technique_type}, chunk size: {args.chunk_size}")
if args.technique_type == "mem0":
if args.method == "add":
memory_manager = MemoryADD(data_path="dataset/locomo10.json", is_graph=args.is_graph)
memory_manager.process_all_conversations()
elif args.method == "search":
output_file_path = os.path.join(
args.output_folder,
f"mem0_results_top_{args.top_k}_filter_{args.filter_memories}_graph_{args.is_graph}.json",
)
memory_searcher = MemorySearch(output_file_path, args.top_k, args.filter_memories, args.is_graph)
memory_searcher.process_data_file("dataset/locomo10.json")
elif args.technique_type == "rag":
output_file_path = os.path.join(args.output_folder, f"rag_results_{args.chunk_size}_k{args.num_chunks}.json")
rag_manager = RAGManager(data_path="dataset/locomo10_rag.json", chunk_size=args.chunk_size, k=args.num_chunks)
rag_manager.process_all_conversations(output_file_path)
elif args.technique_type == "langmem":
output_file_path = os.path.join(args.output_folder, "langmem_results.json")
langmem_manager = LangMemManager(dataset_path="dataset/locomo10_rag.json")
langmem_manager.process_all_conversations(output_file_path)
elif args.technique_type == "zep":
if args.method == "add":
zep_manager = ZepAdd(data_path="dataset/locomo10.json")
zep_manager.process_all_conversations("1")
elif args.method == "search":
output_file_path = os.path.join(args.output_folder, "zep_search_results.json")
zep_manager = ZepSearch()
zep_manager.process_data_file("dataset/locomo10.json", "1", output_file_path)
elif args.technique_type == "openai":
output_file_path = os.path.join(args.output_folder, "openai_results.json")
openai_manager = OpenAIPredict()
openai_manager.process_data_file("dataset/locomo10.json", output_file_path)
else:
raise ValueError(f"Invalid technique type: {args.technique_type}")
if __name__ == "__main__":
main()
-185
View File
@@ -1,185 +0,0 @@
import json
import multiprocessing as mp
import os
import time
from collections import defaultdict
from dotenv import load_dotenv
from jinja2 import Template
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
from langgraph.store.memory import InMemoryStore
from langgraph.utils.config import get_store
from langmem import create_manage_memory_tool, create_search_memory_tool
from openai import OpenAI
from prompts import ANSWER_PROMPT
from tqdm import tqdm
load_dotenv()
client = OpenAI()
ANSWER_PROMPT_TEMPLATE = Template(ANSWER_PROMPT)
def get_answer(question, speaker_1_user_id, speaker_1_memories, speaker_2_user_id, speaker_2_memories):
prompt = ANSWER_PROMPT_TEMPLATE.render(
question=question,
speaker_1_user_id=speaker_1_user_id,
speaker_1_memories=speaker_1_memories,
speaker_2_user_id=speaker_2_user_id,
speaker_2_memories=speaker_2_memories,
)
t1 = time.time()
response = client.chat.completions.create(
model=os.getenv("MODEL"), messages=[{"role": "system", "content": prompt}], temperature=0.0
)
t2 = time.time()
return response.choices[0].message.content, t2 - t1
def prompt(state):
"""Prepare the messages for the LLM."""
store = get_store()
memories = store.search(
("memories",),
query=state["messages"][-1].content,
)
system_msg = f"""You are a helpful assistant.
## Memories
<memories>
{memories}
</memories>
"""
return [{"role": "system", "content": system_msg}, *state["messages"]]
class LangMem:
def __init__(
self,
):
self.store = InMemoryStore(
index={
"dims": 1536,
"embed": f"openai:{os.getenv('EMBEDDING_MODEL')}",
}
)
self.checkpointer = MemorySaver() # Checkpoint graph state
self.agent = create_react_agent(
f"openai:{os.getenv('MODEL')}",
prompt=prompt,
tools=[
create_manage_memory_tool(namespace=("memories",)),
create_search_memory_tool(namespace=("memories",)),
],
store=self.store,
checkpointer=self.checkpointer,
)
def add_memory(self, message, config):
return self.agent.invoke({"messages": [{"role": "user", "content": message}]}, config=config)
def search_memory(self, query, config):
try:
t1 = time.time()
response = self.agent.invoke({"messages": [{"role": "user", "content": query}]}, config=config)
t2 = time.time()
return response["messages"][-1].content, t2 - t1
except Exception as e:
print(f"Error in search_memory: {e}")
return "", t2 - t1
class LangMemManager:
def __init__(self, dataset_path):
self.dataset_path = dataset_path
with open(self.dataset_path, "r") as f:
self.data = json.load(f)
def process_all_conversations(self, output_file_path):
OUTPUT = defaultdict(list)
# Process conversations in parallel with multiple workers
def process_conversation(key_value_pair):
key, value = key_value_pair
result = defaultdict(list)
chat_history = value["conversation"]
questions = value["question"]
agent1 = LangMem()
agent2 = LangMem()
config = {"configurable": {"thread_id": f"thread-{key}"}}
speakers = set()
# Identify speakers
for conv in chat_history:
speakers.add(conv["speaker"])
if len(speakers) != 2:
raise ValueError(f"Expected 2 speakers, got {len(speakers)}")
speaker1 = list(speakers)[0]
speaker2 = list(speakers)[1]
# Add memories for each message
for conv in tqdm(chat_history, desc=f"Processing messages {key}", leave=False):
message = f"{conv['timestamp']} | {conv['speaker']}: {conv['text']}"
if conv["speaker"] == speaker1:
agent1.add_memory(message, config)
elif conv["speaker"] == speaker2:
agent2.add_memory(message, config)
else:
raise ValueError(f"Expected speaker1 or speaker2, got {conv['speaker']}")
# Process questions
for q in tqdm(questions, desc=f"Processing questions {key}", leave=False):
category = q["category"]
if int(category) == 5:
continue
answer = q["answer"]
question = q["question"]
response1, speaker1_memory_time = agent1.search_memory(question, config)
response2, speaker2_memory_time = agent2.search_memory(question, config)
generated_answer, response_time = get_answer(question, speaker1, response1, speaker2, response2)
result[key].append(
{
"question": question,
"answer": answer,
"response1": response1,
"response2": response2,
"category": category,
"speaker1_memory_time": speaker1_memory_time,
"speaker2_memory_time": speaker2_memory_time,
"response_time": response_time,
"response": generated_answer,
}
)
return result
# Use multiprocessing to process conversations in parallel
with mp.Pool(processes=10) as pool:
results = list(
tqdm(
pool.imap(process_conversation, list(self.data.items())),
total=len(self.data),
desc="Processing conversations",
)
)
# Combine results from all workers
for result in results:
for key, items in result.items():
OUTPUT[key].extend(items)
# Save final results
with open(output_file_path, "w") as f:
json.dump(OUTPUT, f, indent=4)
-141
View File
@@ -1,141 +0,0 @@
import json
import os
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from dotenv import load_dotenv
from tqdm import tqdm
from mem0 import MemoryClient
load_dotenv()
# Update custom instructions
custom_instructions = """
Generate personal memories that follow these guidelines:
1. Each memory should be self-contained with complete context, including:
- The person's name, do not use "user" while creating memories
- Personal details (career aspirations, hobbies, life circumstances)
- Emotional states and reactions
- Ongoing journeys or future plans
- Specific dates when events occurred
2. Include meaningful personal narratives focusing on:
- Identity and self-acceptance journeys
- Family planning and parenting
- Creative outlets and hobbies
- Mental health and self-care activities
- Career aspirations and education goals
- Important life events and milestones
3. Make each memory rich with specific details rather than general statements
- Include timeframes (exact dates when possible)
- Name specific activities (e.g., "charity race for mental health" rather than just "exercise")
- Include emotional context and personal growth elements
4. Extract memories only from user messages, not incorporating assistant responses
5. Format each memory as a paragraph with a clear narrative structure that captures the person's experience, challenges, and aspirations
"""
class MemoryADD:
def __init__(self, data_path=None, batch_size=2, is_graph=False):
self.mem0_client = MemoryClient(
api_key=os.getenv("MEM0_API_KEY"),
org_id=os.getenv("MEM0_ORGANIZATION_ID"),
project_id=os.getenv("MEM0_PROJECT_ID"),
)
self.mem0_client.update_project(custom_instructions=custom_instructions)
self.batch_size = batch_size
self.data_path = data_path
self.data = None
self.is_graph = is_graph
if data_path:
self.load_data()
def load_data(self):
with open(self.data_path, "r") as f:
self.data = json.load(f)
return self.data
def add_memory(self, user_id, message, metadata, retries=3):
for attempt in range(retries):
try:
_ = self.mem0_client.add(
message, user_id=user_id, version="v2", metadata=metadata, enable_graph=self.is_graph
)
return
except Exception as e:
if attempt < retries - 1:
time.sleep(1) # Wait before retrying
continue
else:
raise e
def add_memories_for_speaker(self, speaker, messages, timestamp, desc):
for i in tqdm(range(0, len(messages), self.batch_size), desc=desc):
batch_messages = messages[i : i + self.batch_size]
self.add_memory(speaker, batch_messages, metadata={"timestamp": timestamp})
def process_conversation(self, item, idx):
conversation = item["conversation"]
speaker_a = conversation["speaker_a"]
speaker_b = conversation["speaker_b"]
speaker_a_user_id = f"{speaker_a}_{idx}"
speaker_b_user_id = f"{speaker_b}_{idx}"
# delete all memories for the two users
self.mem0_client.delete_all(user_id=speaker_a_user_id)
self.mem0_client.delete_all(user_id=speaker_b_user_id)
for key in conversation.keys():
if key in ["speaker_a", "speaker_b"] or "date" in key or "timestamp" in key:
continue
date_time_key = key + "_date_time"
timestamp = conversation[date_time_key]
chats = conversation[key]
messages = []
messages_reverse = []
for chat in chats:
if chat["speaker"] == speaker_a:
messages.append({"role": "user", "content": f"{speaker_a}: {chat['text']}"})
messages_reverse.append({"role": "assistant", "content": f"{speaker_a}: {chat['text']}"})
elif chat["speaker"] == speaker_b:
messages.append({"role": "assistant", "content": f"{speaker_b}: {chat['text']}"})
messages_reverse.append({"role": "user", "content": f"{speaker_b}: {chat['text']}"})
else:
raise ValueError(f"Unknown speaker: {chat['speaker']}")
# add memories for the two users on different threads
thread_a = threading.Thread(
target=self.add_memories_for_speaker,
args=(speaker_a_user_id, messages, timestamp, "Adding Memories for Speaker A"),
)
thread_b = threading.Thread(
target=self.add_memories_for_speaker,
args=(speaker_b_user_id, messages_reverse, timestamp, "Adding Memories for Speaker B"),
)
thread_a.start()
thread_b.start()
thread_a.join()
thread_b.join()
print("Messages added successfully")
def process_all_conversations(self, max_workers=10):
if not self.data:
raise ValueError("No data loaded. Please set data_path and call load_data() first.")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(self.process_conversation, item, idx) for idx, item in enumerate(self.data)]
for future in futures:
future.result()
-215
View File
@@ -1,215 +0,0 @@
import json
import os
import time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from dotenv import load_dotenv
from jinja2 import Template
from openai import OpenAI
from prompts import ANSWER_PROMPT, ANSWER_PROMPT_GRAPH
from tqdm import tqdm
from mem0 import MemoryClient
load_dotenv()
class MemorySearch:
def __init__(self, output_path="results.json", top_k=10, filter_memories=False, is_graph=False):
self.mem0_client = MemoryClient(
api_key=os.getenv("MEM0_API_KEY"),
org_id=os.getenv("MEM0_ORGANIZATION_ID"),
project_id=os.getenv("MEM0_PROJECT_ID"),
)
self.top_k = top_k
self.openai_client = OpenAI()
self.results = defaultdict(list)
self.output_path = output_path
self.filter_memories = filter_memories
self.is_graph = is_graph
if self.is_graph:
self.ANSWER_PROMPT = ANSWER_PROMPT_GRAPH
else:
self.ANSWER_PROMPT = ANSWER_PROMPT
def search_memory(self, user_id, query, max_retries=3, retry_delay=1):
start_time = time.time()
retries = 0
while retries < max_retries:
try:
if self.is_graph:
print("Searching with graph")
memories = self.mem0_client.search(
query,
user_id=user_id,
top_k=self.top_k,
filter_memories=self.filter_memories,
enable_graph=True,
output_format="v1.1",
)
else:
memories = self.mem0_client.search(
query, user_id=user_id, top_k=self.top_k, filter_memories=self.filter_memories
)
break
except Exception as e:
print("Retrying...")
retries += 1
if retries >= max_retries:
raise e
time.sleep(retry_delay)
end_time = time.time()
if not self.is_graph:
semantic_memories = [
{
"memory": memory["memory"],
"timestamp": memory["metadata"]["timestamp"],
"score": round(memory["score"], 2),
}
for memory in memories
]
graph_memories = None
else:
semantic_memories = [
{
"memory": memory["memory"],
"timestamp": memory["metadata"]["timestamp"],
"score": round(memory["score"], 2),
}
for memory in memories["results"]
]
graph_memories = [
{"source": relation["source"], "relationship": relation["relationship"], "target": relation["target"]}
for relation in memories["relations"]
]
return semantic_memories, graph_memories, end_time - start_time
def answer_question(self, speaker_1_user_id, speaker_2_user_id, question, answer, category):
speaker_1_memories, speaker_1_graph_memories, speaker_1_memory_time = self.search_memory(
speaker_1_user_id, question
)
speaker_2_memories, speaker_2_graph_memories, speaker_2_memory_time = self.search_memory(
speaker_2_user_id, question
)
search_1_memory = [f"{item['timestamp']}: {item['memory']}" for item in speaker_1_memories]
search_2_memory = [f"{item['timestamp']}: {item['memory']}" for item in speaker_2_memories]
template = Template(self.ANSWER_PROMPT)
answer_prompt = template.render(
speaker_1_user_id=speaker_1_user_id.split("_")[0],
speaker_2_user_id=speaker_2_user_id.split("_")[0],
speaker_1_memories=json.dumps(search_1_memory, indent=4),
speaker_2_memories=json.dumps(search_2_memory, indent=4),
speaker_1_graph_memories=json.dumps(speaker_1_graph_memories, indent=4),
speaker_2_graph_memories=json.dumps(speaker_2_graph_memories, indent=4),
question=question,
)
t1 = time.time()
response = self.openai_client.chat.completions.create(
model=os.getenv("MODEL"), messages=[{"role": "system", "content": answer_prompt}], temperature=0.0
)
t2 = time.time()
response_time = t2 - t1
return (
response.choices[0].message.content,
speaker_1_memories,
speaker_2_memories,
speaker_1_memory_time,
speaker_2_memory_time,
speaker_1_graph_memories,
speaker_2_graph_memories,
response_time,
)
def process_question(self, val, speaker_a_user_id, speaker_b_user_id):
question = val.get("question", "")
answer = val.get("answer", "")
category = val.get("category", -1)
evidence = val.get("evidence", [])
adversarial_answer = val.get("adversarial_answer", "")
(
response,
speaker_1_memories,
speaker_2_memories,
speaker_1_memory_time,
speaker_2_memory_time,
speaker_1_graph_memories,
speaker_2_graph_memories,
response_time,
) = self.answer_question(speaker_a_user_id, speaker_b_user_id, question, answer, category)
result = {
"question": question,
"answer": answer,
"category": category,
"evidence": evidence,
"response": response,
"adversarial_answer": adversarial_answer,
"speaker_1_memories": speaker_1_memories,
"speaker_2_memories": speaker_2_memories,
"num_speaker_1_memories": len(speaker_1_memories),
"num_speaker_2_memories": len(speaker_2_memories),
"speaker_1_memory_time": speaker_1_memory_time,
"speaker_2_memory_time": speaker_2_memory_time,
"speaker_1_graph_memories": speaker_1_graph_memories,
"speaker_2_graph_memories": speaker_2_graph_memories,
"response_time": response_time,
}
# Save results after each question is processed
with open(self.output_path, "w") as f:
json.dump(self.results, f, indent=4)
return result
def process_data_file(self, file_path):
with open(file_path, "r") as f:
data = json.load(f)
for idx, item in tqdm(enumerate(data), total=len(data), desc="Processing conversations"):
qa = item["qa"]
conversation = item["conversation"]
speaker_a = conversation["speaker_a"]
speaker_b = conversation["speaker_b"]
speaker_a_user_id = f"{speaker_a}_{idx}"
speaker_b_user_id = f"{speaker_b}_{idx}"
for question_item in tqdm(
qa, total=len(qa), desc=f"Processing questions for conversation {idx}", leave=False
):
result = self.process_question(question_item, speaker_a_user_id, speaker_b_user_id)
self.results[idx].append(result)
# Save results after each question is processed
with open(self.output_path, "w") as f:
json.dump(self.results, f, indent=4)
# Final save at the end
with open(self.output_path, "w") as f:
json.dump(self.results, f, indent=4)
def process_questions_parallel(self, qa_list, speaker_a_user_id, speaker_b_user_id, max_workers=1):
def process_single_question(val):
result = self.process_question(val, speaker_a_user_id, speaker_b_user_id)
# Save results after each question is processed
with open(self.output_path, "w") as f:
json.dump(self.results, f, indent=4)
return result
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = list(
tqdm(executor.map(process_single_question, qa_list), total=len(qa_list), desc="Answering Questions")
)
# Final save at the end
with open(self.output_path, "w") as f:
json.dump(self.results, f, indent=4)
return results
-131
View File
@@ -1,131 +0,0 @@
import argparse
import json
import os
import time
from collections import defaultdict
from dotenv import load_dotenv
from jinja2 import Template
from openai import OpenAI
from tqdm import tqdm
load_dotenv()
ANSWER_PROMPT = """
You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories.
# CONTEXT:
You have access to memories from a conversation. These memories contain
timestamped information that may be relevant to answering the question.
# INSTRUCTIONS:
1. Carefully analyze all provided memories
2. Pay special attention to the timestamps to determine the answer
3. If the question asks about a specific event or fact, look for direct evidence in the memories
4. If the memories contain contradictory information, prioritize the most recent memory
5. If there is a question about time references (like "last year", "two months ago", etc.),
calculate the actual date based on the memory timestamp. For example, if a memory from
4 May 2022 mentions "went to India last year," then the trip occurred in 2021.
6. Always convert relative time references to specific dates, months, or years. For example,
convert "last year" to "2022" or "two months ago" to "March 2023" based on the memory
timestamp. Ignore the reference while answering the question.
7. Focus only on the content of the memories. Do not confuse character
names mentioned in memories with the actual users who created those memories.
8. The answer should be less than 5-6 words.
# APPROACH (Think step by step):
1. First, examine all memories that contain information related to the question
2. Examine the timestamps and content of these memories carefully
3. Look for explicit mentions of dates, times, locations, or events that answer the question
4. If the answer requires calculation (e.g., converting relative time references), show your work
5. Formulate a precise, concise answer based solely on the evidence in the memories
6. Double-check that your answer directly addresses the question asked
7. Ensure your final answer is specific and avoids vague time references
Memories:
{{memories}}
Question: {{question}}
Answer:
"""
class OpenAIPredict:
def __init__(self, model="gpt-4o-mini"):
self.model = model
self.openai_client = OpenAI()
self.results = defaultdict(list)
def search_memory(self, idx):
with open(f"memories/{idx}.txt", "r") as file:
memories = file.read()
return memories, 0
def process_question(self, val, idx):
question = val.get("question", "")
answer = val.get("answer", "")
category = val.get("category", -1)
evidence = val.get("evidence", [])
adversarial_answer = val.get("adversarial_answer", "")
response, search_memory_time, response_time, context = self.answer_question(idx, question)
result = {
"question": question,
"answer": answer,
"category": category,
"evidence": evidence,
"response": response,
"adversarial_answer": adversarial_answer,
"search_memory_time": search_memory_time,
"response_time": response_time,
"context": context,
}
return result
def answer_question(self, idx, question):
memories, search_memory_time = self.search_memory(idx)
template = Template(ANSWER_PROMPT)
answer_prompt = template.render(memories=memories, question=question)
t1 = time.time()
response = self.openai_client.chat.completions.create(
model=os.getenv("MODEL"), messages=[{"role": "system", "content": answer_prompt}], temperature=0.0
)
t2 = time.time()
response_time = t2 - t1
return response.choices[0].message.content, search_memory_time, response_time, memories
def process_data_file(self, file_path, output_file_path):
with open(file_path, "r") as f:
data = json.load(f)
for idx, item in tqdm(enumerate(data), total=len(data), desc="Processing conversations"):
qa = item["qa"]
for question_item in tqdm(
qa, total=len(qa), desc=f"Processing questions for conversation {idx}", leave=False
):
result = self.process_question(question_item, idx)
self.results[idx].append(result)
# Save results after each question is processed
with open(output_file_path, "w") as f:
json.dump(self.results, f, indent=4)
# Final save at the end
with open(output_file_path, "w") as f:
json.dump(self.results, f, indent=4)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--output_file_path", type=str, required=True)
args = parser.parse_args()
openai_predict = OpenAIPredict()
openai_predict.process_data_file("../../dataset/locomo10.json", args.output_file_path)
-183
View File
@@ -1,183 +0,0 @@
import json
import os
import time
from collections import defaultdict
import numpy as np
import tiktoken
from dotenv import load_dotenv
from jinja2 import Template
from openai import OpenAI
from tqdm import tqdm
load_dotenv()
PROMPT = """
# Question:
{{QUESTION}}
# Context:
{{CONTEXT}}
# Short answer:
"""
class RAGManager:
def __init__(self, data_path="dataset/locomo10_rag.json", chunk_size=500, k=1):
self.model = os.getenv("MODEL")
self.client = OpenAI()
self.data_path = data_path
self.chunk_size = chunk_size
self.k = k
def generate_response(self, question, context):
template = Template(PROMPT)
prompt = template.render(CONTEXT=context, QUESTION=question)
max_retries = 3
retries = 0
while retries <= max_retries:
try:
t1 = time.time()
response = self.client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": "You are a helpful assistant that can answer "
"questions based on the provided context."
"If the question involves timing, use the conversation date for reference."
"Provide the shortest possible answer."
"Use words directly from the conversation when possible."
"Avoid using subjects in your answer.",
},
{"role": "user", "content": prompt},
],
temperature=0,
)
t2 = time.time()
return response.choices[0].message.content.strip(), t2 - t1
except Exception as e:
retries += 1
if retries > max_retries:
raise e
time.sleep(1) # Wait before retrying
def clean_chat_history(self, chat_history):
cleaned_chat_history = ""
for c in chat_history:
cleaned_chat_history += f"{c['timestamp']} | {c['speaker']}: {c['text']}\n"
return cleaned_chat_history
def calculate_embedding(self, document):
response = self.client.embeddings.create(model=os.getenv("EMBEDDING_MODEL"), input=document)
return response.data[0].embedding
def calculate_similarity(self, embedding1, embedding2):
return np.dot(embedding1, embedding2) / (np.linalg.norm(embedding1) * np.linalg.norm(embedding2))
def search(self, query, chunks, embeddings, k=1):
"""
Search for the top-k most similar chunks to the query.
Args:
query: The query string
chunks: List of text chunks
embeddings: List of embeddings for each chunk
k: Number of top chunks to return (default: 1)
Returns:
combined_chunks: The combined text of the top-k chunks
search_time: Time taken for the search
"""
t1 = time.time()
query_embedding = self.calculate_embedding(query)
similarities = [self.calculate_similarity(query_embedding, embedding) for embedding in embeddings]
# Get indices of top-k most similar chunks
if k == 1:
# Original behavior - just get the most similar chunk
top_indices = [np.argmax(similarities)]
else:
# Get indices of top-k chunks
top_indices = np.argsort(similarities)[-k:][::-1]
# Combine the top-k chunks
combined_chunks = "\n<->\n".join([chunks[i] for i in top_indices])
t2 = time.time()
return combined_chunks, t2 - t1
def create_chunks(self, chat_history, chunk_size=500):
"""
Create chunks using tiktoken for more accurate token counting
"""
# Get the encoding for the model
encoding = tiktoken.encoding_for_model(os.getenv("EMBEDDING_MODEL"))
documents = self.clean_chat_history(chat_history)
if chunk_size == -1:
return [documents], []
chunks = []
# Encode the document
tokens = encoding.encode(documents)
# Split into chunks based on token count
for i in range(0, len(tokens), chunk_size):
chunk_tokens = tokens[i : i + chunk_size]
chunk = encoding.decode(chunk_tokens)
chunks.append(chunk)
embeddings = []
for chunk in chunks:
embedding = self.calculate_embedding(chunk)
embeddings.append(embedding)
return chunks, embeddings
def process_all_conversations(self, output_file_path):
with open(self.data_path, "r") as f:
data = json.load(f)
FINAL_RESULTS = defaultdict(list)
for key, value in tqdm(data.items(), desc="Processing conversations"):
chat_history = value["conversation"]
questions = value["question"]
chunks, embeddings = self.create_chunks(chat_history, self.chunk_size)
for item in tqdm(questions, desc="Answering questions", leave=False):
question = item["question"]
answer = item.get("answer", "")
category = item["category"]
if self.chunk_size == -1:
context = chunks[0]
search_time = 0
else:
context, search_time = self.search(question, chunks, embeddings, k=self.k)
response, response_time = self.generate_response(question, context)
FINAL_RESULTS[key].append(
{
"question": question,
"answer": answer,
"category": category,
"context": context,
"response": response,
"search_time": search_time,
"response_time": response_time,
}
)
with open(output_file_path, "w+") as f:
json.dump(FINAL_RESULTS, f, indent=4)
# Save results
with open(output_file_path, "w+") as f:
json.dump(FINAL_RESULTS, f, indent=4)
-3
View File
@@ -1,3 +0,0 @@
TECHNIQUES = ["mem0", "rag", "langmem", "zep", "openai"]
METHODS = ["add", "search"]
-76
View File
@@ -1,76 +0,0 @@
import argparse
import json
import os
from dotenv import load_dotenv
from tqdm import tqdm
from zep_cloud import Message
from zep_cloud.client import Zep
load_dotenv()
class ZepAdd:
def __init__(self, data_path=None):
self.zep_client = Zep(api_key=os.getenv("ZEP_API_KEY"))
self.data_path = data_path
self.data = None
if data_path:
self.load_data()
def load_data(self):
with open(self.data_path, "r") as f:
self.data = json.load(f)
return self.data
def process_conversation(self, run_id, item, idx):
conversation = item["conversation"]
user_id = f"run_id_{run_id}_experiment_user_{idx}"
session_id = f"run_id_{run_id}_experiment_session_{idx}"
# # delete all memories for the two users
# self.zep_client.user.delete(user_id=user_id)
# self.zep_client.memory.delete(session_id=session_id)
self.zep_client.user.add(user_id=user_id)
self.zep_client.memory.add_session(
user_id=user_id,
session_id=session_id,
)
print("Starting to add memories... for user", user_id)
for key in tqdm(conversation.keys(), desc=f"Processing user {user_id}"):
if key in ["speaker_a", "speaker_b"] or "date" in key:
continue
date_time_key = key + "_date_time"
timestamp = conversation[date_time_key]
chats = conversation[key]
for chat in tqdm(chats, desc=f"Adding chats for {key}", leave=False):
self.zep_client.memory.add(
session_id=session_id,
messages=[
Message(
role=chat["speaker"],
role_type="user",
content=f"{timestamp}: {chat['text']}",
)
],
)
def process_all_conversations(self, run_id):
if not self.data:
raise ValueError("No data loaded. Please set data_path and call load_data() first.")
for idx, item in tqdm(enumerate(self.data)):
if idx == 0:
self.process_conversation(run_id, item, idx)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--run_id", type=str, required=True)
args = parser.parse_args()
zep_add = ZepAdd(data_path="../../dataset/locomo10.json")
zep_add.process_all_conversations(args.run_id)
-140
View File
@@ -1,140 +0,0 @@
import argparse
import json
import os
import time
from collections import defaultdict
from dotenv import load_dotenv
from jinja2 import Template
from openai import OpenAI
from prompts import ANSWER_PROMPT_ZEP
from tqdm import tqdm
from zep_cloud import EntityEdge, EntityNode
from zep_cloud.client import Zep
load_dotenv()
TEMPLATE = """
FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
{facts}
# These are the most relevant entities
# ENTITY_NAME: entity summary
{entities}
"""
class ZepSearch:
def __init__(self):
self.zep_client = Zep(api_key=os.getenv("ZEP_API_KEY"))
self.results = defaultdict(list)
self.openai_client = OpenAI()
def format_edge_date_range(self, edge: EntityEdge) -> str:
# return f"{datetime(edge.valid_at).strftime('%Y-%m-%d %H:%M:%S') if edge.valid_at else 'date unknown'} - {(edge.invalid_at.strftime('%Y-%m-%d %H:%M:%S') if edge.invalid_at else 'present')}"
return f"{edge.valid_at if edge.valid_at else 'date unknown'} - {(edge.invalid_at if edge.invalid_at else 'present')}"
def compose_search_context(self, edges: list[EntityEdge], nodes: list[EntityNode]) -> str:
facts = [f" - {edge.fact} ({self.format_edge_date_range(edge)})" for edge in edges]
entities = [f" - {node.name}: {node.summary}" for node in nodes]
return TEMPLATE.format(facts="\n".join(facts), entities="\n".join(entities))
def search_memory(self, run_id, idx, query, max_retries=3, retry_delay=1):
start_time = time.time()
retries = 0
while retries < max_retries:
try:
user_id = f"run_id_{run_id}_experiment_user_{idx}"
edges_results = (
self.zep_client.graph.search(
user_id=user_id, reranker="cross_encoder", query=query, scope="edges", limit=20
)
).edges
node_results = (
self.zep_client.graph.search(user_id=user_id, reranker="rrf", query=query, scope="nodes", limit=20)
).nodes
context = self.compose_search_context(edges_results, node_results)
break
except Exception as e:
print("Retrying...")
retries += 1
if retries >= max_retries:
raise e
time.sleep(retry_delay)
end_time = time.time()
return context, end_time - start_time
def process_question(self, run_id, val, idx):
question = val.get("question", "")
answer = val.get("answer", "")
category = val.get("category", -1)
evidence = val.get("evidence", [])
adversarial_answer = val.get("adversarial_answer", "")
response, search_memory_time, response_time, context = self.answer_question(run_id, idx, question)
result = {
"question": question,
"answer": answer,
"category": category,
"evidence": evidence,
"response": response,
"adversarial_answer": adversarial_answer,
"search_memory_time": search_memory_time,
"response_time": response_time,
"context": context,
}
return result
def answer_question(self, run_id, idx, question):
context, search_memory_time = self.search_memory(run_id, idx, question)
template = Template(ANSWER_PROMPT_ZEP)
answer_prompt = template.render(memories=context, question=question)
t1 = time.time()
response = self.openai_client.chat.completions.create(
model=os.getenv("MODEL"), messages=[{"role": "system", "content": answer_prompt}], temperature=0.0
)
t2 = time.time()
response_time = t2 - t1
return response.choices[0].message.content, search_memory_time, response_time, context
def process_data_file(self, file_path, run_id, output_file_path):
with open(file_path, "r") as f:
data = json.load(f)
for idx, item in tqdm(enumerate(data), total=len(data), desc="Processing conversations"):
qa = item["qa"]
for question_item in tqdm(
qa, total=len(qa), desc=f"Processing questions for conversation {idx}", leave=False
):
result = self.process_question(run_id, question_item, idx)
self.results[idx].append(result)
# Save results after each question is processed
with open(output_file_path, "w") as f:
json.dump(self.results, f, indent=4)
# Final save at the end
with open(output_file_path, "w") as f:
json.dump(self.results, f, indent=4)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--run_id", type=str, required=True)
args = parser.parse_args()
zep_search = ZepSearch()
zep_search.process_data_file("../../dataset/locomo10.json", args.run_id, "results/zep_search_results.json")
@@ -555,7 +555,7 @@
"# - Enables creation of AI agents with long-term memory and learning abilities.\n",
"# - Improves consistency and reduces repetition in user-agent interactions.\n",
"\n",
"from cookbooks.helper.mem0_teachability import Mem0Teachability\n",
"from helper.mem0_teachability import Mem0Teachability\n",
"\n",
"teachability = Mem0Teachability(\n",
" verbosity=2, # for visibility of what's happening\n",
@@ -2,6 +2,23 @@
All notable changes to the `@mem0/opencode-plugin` will be documented in this file.
## 0.2.0 — Native SDK tools, MCP-free, leaner skill set
### Changed (breaking)
- **Memory tools are now native OpenCode tools** registered via the `@opencode-ai/plugin` `tool()` helper and backed by the `mem0ai` SDK directly. The plugin no longer registers or depends on the remote MCP server (`mcp.mem0.ai`); the bundled `opencode.json` and the regex-based MCP call interception have been removed. Tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`, plus a `get_event_status` helper for async-write status.
- **Skills load via the `config` hook (`skills.paths`)** instead of being copied into the project's `.opencode/` directory on startup. The `installSkills()` filesystem copy and the `cli.ts` installer (`mem0-opencode` bin) have been removed — install with `opencode plugin @mem0/opencode-plugin`.
- **Trimmed to 8 focused skills** (`context-loader`, `dream`, `forget`, `health`, `peek`, `pin`, `remember`, `tour`). Removed `import`, `export`, `memory-reviewer`, `mem0` (SDK reference), `list-projects`, `switch-project`, `stats`, and `onboard`.
### Added
- **Expanded telemetry to the full shared `plugin.*` schema.** In addition to `plugin.session_start` and `plugin.tool_use`, the plugin now emits `plugin.user_prompt`, `plugin.bash_error`, `plugin.pre_compact`, and `plugin.session_stop`. `tool_use` now fires from inside each native tool. Every event also carries `project_hash` (anonymized `sha256(app_id)`) and `os_version`, matching the editor plugin's `telemetry.py`.
### Fixed
- **Error-pattern lookup** in `tool.execute.after` no longer issues two identical `mem0.search()` calls; it now performs a single `topK: 6` search.
- Corrected the documented system-prompt hook name from `experimental.chat.system.transform` to the actual `experimental.chat.messages.transform`.
## 0.1.3 — File-context injection, session summaries & activity timeline, anonymous telemetry
### Added
@@ -4,24 +4,18 @@ Persistent memory for [OpenCode](https://opencode.ai). Your agent remembers deci
## Install
```bash
bunx @mem0/opencode-plugin@latest install
```
Or using OpenCode's built-in CLI:
```bash
opencode plugin @mem0/opencode-plugin
```
This adds the plugin to your `~/.config/opencode/opencode.json`. The plugin registers its memory tools and skills itself — there is no MCP server to configure.
**Or let your agent do it** — paste this into OpenCode:
```
Install @mem0/opencode-plugin by following https://raw.githubusercontent.com/mem0ai/mem0/main/integrations/mem0-plugin/.opencode-plugin/README.md
```
All commands auto-add the plugin and MCP server to your `~/.config/opencode/opencode.json`. No manual config needed.
Get your API key (free): [app.mem0.ai/dashboard/api-keys](https://app.mem0.ai/dashboard/api-keys)
```bash
@@ -34,24 +28,25 @@ Restart OpenCode.
| Component | Description |
|-----------|-------------|
| **MCP Server** | 9 memory tools — add, search, get, update, delete memories |
| **Lifecycle Hooks** | Auto-search on session start and every prompt, metadata enforcement, error memory lookup, compaction context |
| **16 Slash Commands** | `/mem0:remember`, `/mem0:tour`, `/mem0:stats`, `/mem0:health`, `/mem0:dream`, and more |
| **9 Native Memory Tools** | `add_memory`, `search_memories`, `get_memories`, `update_memory`, `delete_memory`, and more — registered as OpenCode tools, backed by the `mem0ai` SDK (no MCP server required) |
| **Lifecycle Hooks** | Auto-search on session start and every prompt, error memory lookup, compaction context, secret redaction |
| **8 Skills** | `/mem0:remember`, `/mem0:tour`, `/mem0:peek`, `/mem0:health`, `/mem0:dream`, `/mem0:forget`, `/mem0:pin`, `/mem0:context-loader` |
## Hooks
Pure TypeScript — no Python, no shell scripts. Uses the [mem0ai](https://www.npmjs.com/package/mem0ai) SDK directly.
Pure TypeScript — no Python, no shell scripts. Memory operations are native OpenCode tools backed by the [mem0ai](https://www.npmjs.com/package/mem0ai) SDK directly.
| Hook | Event | What it does |
|------|-------|-------------|
| **Config** | `config` | Registers the bundled skills (`skills.paths`) and `/mem0:*` slash commands at startup |
| **Chat message** | `chat.message` | Loads prior memories on session start, searches relevant memories before each prompt, auto-captures learnings periodically |
| **Pre-tool** | `tool.execute.before` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tools |
| **Post-tool** | `tool.execute.after` | Tracks stats, scans bash errors for related memories |
| **System transform** | `experimental.chat.system.transform` | Injects memory context (session memories, search results, error lookups) into system prompt |
| **Pre-tool** | `tool.execute.before` | Blocks MEMORY.md writes, steering them to the `add_memory` tool |
| **Post-tool** | `tool.execute.after` | Scans bash errors and pre-fetches related memories |
| **Messages transform** | `experimental.chat.messages.transform` | Injects memory context (session memories, search results, error lookups) into the prompt |
| **Compaction** | `experimental.session.compacting` | Stores session state memory, then injects prior memories into compaction context so nothing is lost |
| **Shell env** | `shell.env` | Exports `MEM0_USER_ID`, `MEM0_APP_ID`, `MEM0_SESSION_ID`, and `MEM0_BRANCH` to shell |
## MCP Tools
## Memory Tools
| Tool | Description |
|------|-------------|
@@ -1,254 +0,0 @@
#!/usr/bin/env bun
import {
readFileSync,
writeFileSync,
existsSync,
mkdirSync,
copyFileSync,
readdirSync,
rmSync,
statSync,
} from "fs";
import { join, dirname } from "path";
import { homedir } from "os";
const PLUGIN_NAME = "@mem0/opencode-plugin";
const MCP_CONFIG = {
mem0: {
type: "remote",
url: "https://mcp.mem0.ai/mcp/",
headers: {
Authorization: "Token {env:MEM0_API_KEY}",
},
oauth: false,
},
};
const SKILLS_NAMESPACE = "mem0";
function getConfigDir(): string {
const dir = join(homedir(), ".config", "opencode");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
return dir;
}
function getConfigPath(): string {
const configDir = getConfigDir();
const jsonc = join(configDir, "opencode.jsonc");
if (existsSync(jsonc)) return jsonc;
return join(configDir, "opencode.json");
}
function stripJsonComments(text: string): string {
let result = "";
let i = 0;
let inString = false;
let escape = false;
while (i < text.length) {
const ch = text[i];
if (escape) {
result += ch;
escape = false;
i++;
continue;
}
if (inString) {
if (ch === "\\") escape = true;
else if (ch === '"') inString = false;
result += ch;
i++;
continue;
}
if (ch === '"') {
inString = true;
result += ch;
i++;
continue;
}
if (ch === "/" && text[i + 1] === "/") {
while (i < text.length && text[i] !== "\n") i++;
continue;
}
if (ch === "/" && text[i + 1] === "*") {
i += 2;
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
i += 2;
continue;
}
result += ch;
i++;
}
return result;
}
function resolvePluginDir(): string {
try {
return dirname(new URL(import.meta.url).pathname);
} catch {}
return __dirname ?? process.cwd();
}
function findSkillsDir(): string {
const base = resolvePluginDir();
const candidates = [
join(base, "opencode-skills"),
join(dirname(base), "opencode-skills"),
join(base, "..", "opencode-skills"),
];
for (const c of candidates) {
try {
if (existsSync(c) && statSync(c).isDirectory()) return c;
} catch {}
}
return "";
}
function installSkills(): number {
const skillsSource = findSkillsDir();
if (!skillsSource) {
console.log(" ! Skills directory not found — skipping slash command install");
return 0;
}
const skillsTarget = join(getConfigDir(), "skills");
if (!existsSync(skillsTarget)) mkdirSync(skillsTarget, { recursive: true });
let count = 0;
const entries = readdirSync(skillsSource);
for (const name of entries) {
const skillDir = join(skillsSource, name);
try {
if (!statSync(skillDir).isDirectory()) continue;
} catch {
continue;
}
const skillFile = join(skillDir, "SKILL.md");
if (!existsSync(skillFile)) continue;
const targetDir = join(skillsTarget, `${SKILLS_NAMESPACE}-${name}`);
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
copyFileSync(skillFile, join(targetDir, "SKILL.md"));
count++;
}
return count;
}
function uninstallSkills(): number {
const skillsTarget = join(getConfigDir(), "skills");
if (!existsSync(skillsTarget)) return 0;
let count = 0;
const entries = readdirSync(skillsTarget);
for (const name of entries) {
if (!name.startsWith(`${SKILLS_NAMESPACE}-`)) continue;
const fullPath = join(skillsTarget, name);
try {
if (!statSync(fullPath).isDirectory()) continue;
rmSync(fullPath, { recursive: true });
count++;
} catch {}
}
return count;
}
function install() {
console.log("Installing Mem0 plugin for OpenCode...\n");
const configPath = getConfigPath();
let config: any = {};
if (existsSync(configPath)) {
try {
const raw = readFileSync(configPath, "utf-8");
config = JSON.parse(stripJsonComments(raw));
} catch {
console.log(` ! Could not parse ${configPath}, creating fresh config`);
config = {};
}
}
if (!Array.isArray(config.plugin)) config.plugin = [];
if (!config.plugin.includes(PLUGIN_NAME)) {
config.plugin.push(PLUGIN_NAME);
console.log(` + Added "${PLUGIN_NAME}" to plugin array`);
} else {
console.log(` ~ "${PLUGIN_NAME}" already in plugin array`);
}
if (!config.mcp) config.mcp = {};
if (!config.mcp.mem0) {
config.mcp.mem0 = MCP_CONFIG.mem0;
console.log(" + Added mem0 MCP server config");
} else {
console.log(" ~ mem0 MCP server already configured");
}
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
console.log(`\n Wrote ${configPath}`);
const skillCount = installSkills();
if (skillCount > 0) {
console.log(` + Installed ${skillCount} slash commands to ~/.config/opencode/skills/`);
}
console.log("");
if (!process.env.MEM0_API_KEY) {
console.log(" ! MEM0_API_KEY is not set in your environment.");
console.log(
' Run: echo \'export MEM0_API_KEY="m0-your-key"\' >> ~/.zshrc && source ~/.zshrc',
);
console.log(
" Get a free key at: https://app.mem0.ai/dashboard/api-keys\n",
);
} else {
console.log(" MEM0_API_KEY detected\n");
}
console.log("Done! Restart OpenCode to activate Mem0.");
console.log(" Then run /mem0-onboard in the TUI to complete setup.\n");
}
function uninstall() {
const configPath = getConfigPath();
if (!existsSync(configPath)) {
console.log("No OpenCode config found. Nothing to remove.");
return;
}
const raw = readFileSync(configPath, "utf-8");
const config = JSON.parse(stripJsonComments(raw));
if (Array.isArray(config.plugin)) {
config.plugin = config.plugin.filter((p: string) => p !== PLUGIN_NAME);
if (config.plugin.length === 0) delete config.plugin;
}
if (config.mcp?.mem0) {
delete config.mcp.mem0;
if (Object.keys(config.mcp).length === 0) delete config.mcp;
}
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
console.log(` Removed plugin + MCP from ${configPath}`);
const skillCount = uninstallSkills();
if (skillCount > 0) {
console.log(` Removed ${skillCount} slash commands from ~/.config/opencode/skills/`);
}
console.log("Restart OpenCode to complete removal.");
}
const cmd = process.argv[2];
if (cmd === "uninstall" || cmd === "remove") {
uninstall();
} else {
install();
}
File diff suppressed because it is too large Load Diff
@@ -1,80 +0,0 @@
---
name: export
description: Exports all project memories to a portable Markdown file for backup or migration. Use when backing up memories, migrating to another project, sharing memory state with teammates, or archiving before cleanup.
---
# Mem0 Export
Export all memories for the current project to a portable Markdown file.
## Execution
### Step 1: Resolve identity
Determine the active identity:
- `user_id` from `MEM0_USER_ID` env var, else `$USER`, else `"default"`
- `project_id` (used as `app_id`) from `MEM0_PROJECT_ID` env var, or via the project resolver
### Step 2: Fetch all memories
Call `get_memories` with:
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `page_size=200`
If the response is paginated (i.e. the result contains a `next` cursor or the count equals `page_size`), continue fetching pages until all memories are retrieved.
### Step 3: Format each memory as a YAML-frontmatter block
For each memory record, produce a block in this exact format:
```
---
id: <memory.id>
created_at: <memory.created_at>
type: <memory.metadata.type or "">
confidence: <memory.metadata.confidence or "">
branch: <memory.metadata.branch or "">
files: <memory.metadata.files joined with ", " or "">
categories: <memory.categories joined with ", " or "">
---
<memory.memory or memory content string>
```
Notes:
- The `---` delimiters must be on their own lines with no extra whitespace.
- `files` and `categories` are written as comma-separated values on a single line.
- Leave a blank line after the content before the next `---` (for readability).
- If a field is missing or null, write an empty string (not "null").
### Step 4: Write the export file
Determine the output filename:
```
mem0-export-<project_id>-<YYYY-MM-DD>.md
```
Where `<YYYY-MM-DD>` is today's date in UTC.
Write all formatted blocks to this file using the Write tool (or equivalent). The file is written to the current working directory.
### Step 5: Print summary
```
Exported <N> memories to <filename>
```
Where `<N>` is the total number of memory blocks written.
## Error Handling
- If `get_memories` returns an error or zero memories, print:
```
No memories found for project <project_id>. Nothing exported.
```
- If the write fails, report the error to the user.
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -1,6 +1,6 @@
---
name: health
description: Diagnoses mem0 connectivity, API key validity, and memory read/write functionality. Use when memory operations fail, searches return empty, add_memory errors occur, MCP connection drops, or to verify the plugin is working correctly.
description: Diagnoses mem0 connectivity, API key validity, and memory read/write functionality. Use when memory operations fail, searches return empty, add_memory errors occur, or to verify the plugin is working correctly.
---
# Mem0 Health Check
@@ -37,7 +37,7 @@ echo "branch=$(git branch --show-current 2>/dev/null || echo '')"
PASS if all three are non-empty. WARN if any falls back to defaults.
### Check 3: MCP server connectivity
### Check 3: Memory tool connectivity
Call `search_memories` with:
- `query="health check"`
@@ -82,7 +82,7 @@ echo "branch=${MEM0_BRANCH:-}"
PASS API Key m0-dVe...
PASS Identity user=kartik, project=mem0, branch=main
PASS MCP Connection 142ms
PASS Memory Tools 142ms
PASS Write/Read write + delete OK
PASS Session session_id=abc123, app_id=mem0, branch=main
@@ -1,185 +0,0 @@
---
name: import
description: Imports memories from an exported Markdown file or MEMORY.md into the current project. Use when migrating from another project, restoring from backup, importing Claude Code native MEMORY.md content, or setting up a new project with existing knowledge.
---
# Mem0 Import
Import memories from a mem0 export file into the current project.
## Execution
### Step 1: Determine the export file to import
If the user provided a filename as an argument to `/mem0:import <filename>`, use that file.
Otherwise, list `.md` files in the current directory whose names contain `mem0-export`:
```bash
ls -1 *.md 2>/dev/null | grep mem0-export || echo "No export files found"
```
If multiple files are found, ask the user which one to import. If none are found, print:
```
No mem0-export files found in the current directory.
Run /mem0:export first, or provide the filename: /mem0:import <path-to-file>
```
### Step 2: Parse the export file
Read the export file directly. It is a JSON file containing a top-level `memories` array. Each element has:
- `id` — original memory ID (for reference only; a new ID will be assigned on import)
- `type` — metadata type
- `confidence` — metadata confidence value
- `branch` — metadata branch
- `files` — list of associated files
- `categories` — list of categories
- `content` — the memory text
Parse the JSON in-memory (do not run any external script). If the file cannot be read or parsed, or if the `memories` array is missing or empty, print:
```
Failed to parse <filename> or file contains no valid memory blocks.
```
and stop.
### Step 3: Resolve identity
Determine the active identity:
- `user_id` from `MEM0_USER_ID` env var, else `$USER`, else `"default"`
- `project_id` (used as `app_id`) from `MEM0_PROJECT_ID` env var, or via the project resolver
### Step 4: Import each memory
For each record in the parsed JSON array, call `add_memory` (MCP tool) with:
- `text="<record.content>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={`
- `"type": "<record.type>"` (if non-empty)
- `"confidence": "<record.confidence>"` (if non-empty)
- `"branch": "<record.branch>"` (if non-empty)
- `"files": <record.files>` (the list, if non-empty)
- `"source": "import"`
- `}`
- `infer=False`
Notes:
- Do NOT pass the original `id` — the platform assigns a new ID.
- Skip records where `content` is empty.
- Continue importing even if individual records fail; track the count of successes.
### Step 5: Print results
```
Imported <N> memories into project <project_id>
```
Where `<N>` is the number of successfully imported memories.
If any failed:
```
Imported <N>/<total> memories into project <project_id> (<failed> failed)
```
## Importing from competing AI tools (`--tools`)
When invoked with `--tools` (e.g., `/mem0:import --tools`), detect and import
from competing AI tool configuration files:
### Supported tools
| Tool | File/directory |
|------|---------------|
| Cursor | `.cursorrules` |
| GitHub Copilot | `.github/copilot-instructions.md` |
| Cline | `memory-bank/` (directory of `.md` files) |
| Continue | `.continue/rules.md` |
### T1: Detect
```bash
test -f .cursorrules && echo "cursor: .cursorrules"
test -f .github/copilot-instructions.md && echo "copilot: .github/copilot-instructions.md"
test -d memory-bank/ && echo "cline: memory-bank/"
test -f .continue/rules.md && echo "continue: .continue/rules.md"
```
### T2: Ask user
List found files, ask which to import (numbers, comma-separated, or "all").
If none found:
```
No competing tool configuration files found.
Checked: .cursorrules, .github/copilot-instructions.md, memory-bank/, .continue/rules.md
```
### T3: Import
For each selected tool, read the file(s) directly and split the content into logical
chunks to import as individual memories. No external scripts are used — all parsing
and importing is done via MCP tools.
Chunking rules per tool:
- **cursorrules / copilot / continue**: Read the file as plain text. Split on blank
lines or section headers (`#`, `##`). Each non-empty chunk becomes one memory.
Skip chunks shorter than 50 characters.
- **cline (memory-bank/)**: List all `.md` files in the directory. Read each file.
Split each file on blank lines or headers. Each non-empty chunk becomes one memory.
Skip chunks shorter than 50 characters.
For each chunk, call `add_memory` (MCP tool) with:
- `text="<chunk text>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "task_learning", "source": "<tool>-import", "confidence": 0.8}`
- `infer=False`
Where `<tool>` is `cursorrules`, `copilot`, `cline`, or `continue`.
Notes: chunks longer than 10,000 characters are truncated before import. Safe to
re-run — deduplication handles repeated entries.
### T4: Report
```
Imported <N> memories into <project_id> (cursor: <N>, copilot: <N>)
```
---
## Importing Claude Code's native MEMORY.md
When invoked with a path to Claude Code's native `MEMORY.md` file (typically
`~/.claude/projects/<proj-key>/memory/MEMORY.md`), or when `on_session_start.sh`
detects native auto-memory and the user chooses to import:
1. Read the file directly. It contains newline-separated memory entries (one fact per
line, sometimes with `- ` bullet prefix).
2. Split by non-empty lines. Each line becomes one memory.
3. Skip lines shorter than 20 characters or lines that are just headers (`#`).
4. For each line, call `add_memory` (MCP tool) with:
- `text="<line>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "task_learning", "source": "memory-md-import", "confidence": 0.8}`
- `infer=False`
5. Report: `Imported <N> memories from MEMORY.md into project <project_id>`
6. Suggest disabling native auto-memory:
```
To avoid duplicate memory systems, add to ~/.claude/settings.json:
"autoMemoryEnabled": false
```
This handles the cold-start gap when a user has been using Claude Code's native
memory and switches to mem0.
## Error Handling
- If `add_memory` calls fail consistently (e.g. auth error), report the issue and stop early.
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -1,64 +0,0 @@
---
name: list-projects
description: Lists all projects with stored memories for the current user, showing memory counts and last activity dates. Use when checking which projects have memories, comparing memory distribution across repos, or finding a specific project scope.
---
# Mem0 List Projects
Show all known project scopes for the current user.
## Execution
### Step 1: Fetch memories to discover app_ids
There is no dedicated "list projects" API endpoint. Discover projects by fetching
the user's memories across all scopes.
**Important:** A filter with only `user_id` triggers implicit null scoping — it
excludes memories that have a non-null `app_id`. Run two queries and merge:
1. **Null-scoped:** `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}]}`, `page_size=200`
— catches memories without `app_id`
2. **App-scoped:** `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": {"exists": true}}]}`, `page_size=200`
— catches memories with any `app_id`
Run both calls in parallel. Merge results, deduplicate by memory `id`.
If either response indicates more pages, paginate (up to 1000 total).
### Step 2: Extract distinct projects
For each memory, determine project by:
1. Top-level `app_id` field (preferred)
2. `metadata.project_id` (legacy memories)
3. `metadata.project` (oldest format)
4. `"(unscoped)"` if none found
Group by resolved project name. For each project, count:
- Total memories
- Most recent `created_at` date
- Top 3 `metadata.type` values by frequency
### Step 3: Display
```
## mem0 projects
<app_id_1> <count> memories (last: <date>) ← current
<app_id_2> <count> memories (last: <date>)
<N> projects, <M> total memories
```
Mark current project with `← current`. Sort by memory count descending.
### Step 4: Empty state
If zero memories found:
```
No projects found. Run /mem0:onboard to get started.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -1,189 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but not
limited to compiled object code, generated documentation, and
conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024 Mem0.ai
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -1,73 +0,0 @@
# Mem0 Skill for Claude
Add persistent memory to any AI application in minutes using [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=mem0-plugin-skill-readme).
## What This Skill Does
When installed, Claude can:
- **Set up Mem0** in your Python or TypeScript project
- **Integrate memory** into your existing AI app (LangChain, CrewAI, Vercel AI, OpenAI Agents, LangGraph, LlamaIndex, etc.)
- **Generate working code** using real API references and tested patterns
- **Search live docs** on demand for the latest Mem0 documentation
## Installation
This skill is included automatically when you install the Mem0 plugin:
```
/plugin marketplace add mem0ai/mem0
/plugin install mem0@mem0-plugins
```
See the [plugin README](../../README.md) for full setup instructions.
### Prerequisites
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill-readme))
- Python 3.10+ or Node.js 18+
- Set the environment variable:
```bash
export MEM0_API_KEY="m0-your-api-key"
```
## Quick Start
After installing, just ask Claude:
- "Set up mem0 in my project"
- "Add memory to my chatbot"
- "Help me search user memories with filters"
- "Integrate mem0 with my LangChain app"
- "Add graph memory to track entity relationships"
## What's Inside
```text
skills/mem0/
├── SKILL.md # Skill definition and instructions
├── README.md # This file
├── LICENSE # Apache-2.0
├── scripts/
│ └── mem0_doc_search.py # Search live Mem0 docs on demand
└── references/ # Documentation (loaded on demand)
├── quickstart.md # Full quickstart (Python, TS, cURL)
├── sdk-guide.md # All SDK methods (Python + TypeScript)
├── api-reference.md # REST endpoints, filters, memory object
├── architecture.md # Processing pipeline, lifecycle, scoping, performance
├── features.md # Retrieval, graph, categories, MCP, webhooks, multimodal
├── integration-patterns.md # LangChain, CrewAI, Vercel AI, LangGraph, LlamaIndex, etc.
└── use-cases.md # 7 real-world patterns with Python + TypeScript code
```
## Links
- [Mem0 Platform Dashboard](https://app.mem0.ai?utm_source=oss&utm_medium=mem0-plugin-skill-readme)
- [Mem0 Documentation](https://docs.mem0.ai)
- [Mem0 GitHub](https://github.com/mem0ai/mem0)
- [API Reference](https://docs.mem0.ai/api-reference)
## License
Apache-2.0
@@ -1,191 +0,0 @@
---
name: mem0
description: Mem0 SDK reference covering Python and TypeScript APIs, memory client methods, configuration, and framework integrations. Use when writing code that calls mem0 APIs, configuring memory providers, or integrating mem0 into an application.
license: Apache-2.0
metadata:
author: mem0ai
version: "0.1.1"
category: ai-memory
tags: "memory, personalization, ai, python, typescript, vector-search"
compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm install mem0ai, MEM0_API_KEY env var (Platform), and internet access to api.mem0.ai. Uses Mem0 v3 API.
---
# Mem0 Platform Integration
> **Skill Graph:** This skill is part of the Mem0 skill graph:
> - **mem0** (this skill) -- Platform Client SDK + OSS (Python + TypeScript)
> - **[mem0-vercel-ai-sdk](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk)** -- Vercel AI SDK provider
Mem0 is a managed memory layer for AI applications. It stores, retrieves, and manages user memories via API — no infrastructure to deploy. For self-hosted usage, see the OSS section in the client references below.
## Step 1: Install and authenticate
**Python:**
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
**TypeScript/JavaScript:**
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
Get an API key at: https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill
> **Don't have a `MEM0_API_KEY`?** Sign up at https://app.mem0.ai and create one from the dashboard. Keys start with `m0-`.
## Step 2: Initialize the client
**Python:**
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-xxx")
```
**TypeScript:**
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-xxx' });
```
For async Python, use `AsyncMemoryClient`.
## Step 3: Core operations
Every Mem0 integration follows the same pattern: **retrieve → generate → store**.
### Add memories
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
```
### Search memories
```python
results = client.search("dietary preferences", filters={"user_id": "alice"})
for mem in results.get("results", []):
print(mem["memory"])
```
### Get all memories
```python
all_memories = client.get_all(filters={"user_id": "alice"})
```
### Update a memory
```python
client.update("memory-uuid", text="Updated: vegetarian, nut allergy, prefers organic")
```
### Delete a memory
```python
client.delete("memory-uuid")
client.delete_all(user_id="alice") # delete all for a user
```
## Common integration pattern
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai = OpenAI()
def chat(user_input: str, user_id: str) -> str:
# 1. Retrieve relevant memories
memories = mem0.search(user_input, filters={"user_id": user_id})
context = "\n".join([m["memory"] for m in memories.get("results", [])])
# 2. Generate response with memory context
response = openai.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": f"User context:\n{context}"},
{"role": "user", "content": user_input},
]
)
reply = response.choices[0].message.content
# 3. Store interaction for future context
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": reply}],
user_id=user_id
)
return reply
```
## Common edge cases
- **Search returns empty:** v3 processes `add()` asynchronously — returns an event ID immediately. Wait 2-3s before searching. Also verify `user_id` matches exactly (case-sensitive) and use `filters={"user_id": "..."}` syntax.
- **AND filter with user_id + agent_id returns empty:** Entities are stored separately. `{"AND": [{"user_id": "alice"}, {"agent_id": "bot"}]}` returns nothing. Use `OR` instead, or query each separately.
- **Duplicate memories:** Don't mix `infer=True` (default) and `infer=False` for the same data. `infer=True` extracts facts via LLM with dedup. `infer=False` stores raw — same text can be stored twice.
- **Implicit null scoping:** `filters={"user_id": "alice"}` only returns memories where `agent_id`, `app_id`, `run_id` are ALL null. Wrap in `{"OR": [...]}` to include memories with non-null scoping fields.
- **Platform vs OSS imports:** Platform: `from mem0 import MemoryClient`. OSS: `from mem0 import Memory`. Don't mix them — `MemoryClient` talks to `api.mem0.ai`, `Memory` runs locally.
- **v3 defaults:** `top_k=20`, `threshold=0.1`, `rerank=False`. Adjust as needed.
## v3 API (Current)
Mem0 v3 uses single-pass extraction, entity linking, and multi-signal retrieval.
**Key v3 changes from v2:**
- **Endpoints:** `POST /v3/memories/add/`, `POST /v3/memories/search/`, `POST /v3/memories/` (paginated list)
- **Extraction:** Single ADD-only pass — no more UPDATE/DELETE operations during extraction. Memories accumulate rather than consolidate.
- **Entity linking:** Replaces graph memory. Auto-extracted during `add()`, no config needed. Remove `enable_graph` and `graph_store` from any old config.
- **Defaults:** `top_k=20`, `threshold=0.1`, `rerank=False`
- **Removed params:** `org_id`, `project_id`, `enable_graph` — all removed from SDK
- **TypeScript:** Exclusively camelCase (`userId`, `agentId`, `appId`, `topK`)
- **Add response:** Async — returns event ID immediately, poll via `GET /v1/event/{event_id}/`
See the [migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for details.
## Live documentation search
For the latest docs beyond what's in the references, use the doc search tool:
```bash
bun ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.ts --query "topic"
bun ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.ts --page "/platform/features/graph-memory"
bun ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.ts --index
```
No API key needed — searches docs.mem0.ai directly.
## Client SDK References
Language-specific deep references (Platform + OSS):
| Language | File |
|----------|------|
| Python (MemoryClient + AsyncMemoryClient + Memory OSS) | [client/python.md](client/python.md) |
| TypeScript/Node.js (MemoryClient + Memory OSS) | [client/node.md](client/node.md) |
| Python vs TypeScript differences | [client/differences.md](client/differences.md) |
## Platform References
Load these on demand for deeper detail:
| Topic | File |
|-------|------|
| Quickstart (Python, TS, cURL) | [references/quickstart.md](references/quickstart.md) |
| SDK guide (all methods, both languages) | [references/sdk-guide.md](references/sdk-guide.md) |
| API reference (endpoints, filters, object schema) | [references/api-reference.md](references/api-reference.md) |
| Architecture (pipeline, lifecycle, scoping, performance) | [references/architecture.md](references/architecture.md) |
| Platform features (retrieval, graph, categories, MCP, etc.) | [references/features.md](references/features.md) |
| Framework integrations (LangChain, CrewAI, OpenAI Agents, etc.) | [references/integration-patterns.md](references/integration-patterns.md) |
| Use cases & examples (real-world patterns with code) | [references/use-cases.md](references/use-cases.md) |
## Related Mem0 Skills
| Skill | When to use | Link |
|-------|-------------|------|
| mem0-vercel-ai-sdk | Vercel AI SDK provider with automatic memory | [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk) |
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -1,129 +0,0 @@
# Python vs TypeScript SDK Differences
Quick-reference cheatsheet for developers working across both Mem0 SDKs.
## Constructor
| Aspect | Python | TypeScript |
|--------|--------|------------|
| Import (Platform) | `from mem0 import MemoryClient` | `import MemoryClient from 'mem0ai'` |
| Import (OSS) | `from mem0 import Memory` | `import { Memory } from 'mem0ai/oss'` |
| Constructor | `MemoryClient(api_key="m0-xxx")` | `new MemoryClient({ apiKey: 'm0-xxx' })` |
| Required param | `api_key` (positional or kwarg) | `apiKey` (in options object) |
Both read from `MEM0_API_KEY` env var if no key provided.
## Method Naming
| Operation | Python | TypeScript |
|-----------|--------|------------|
| Add | `add()` | `add()` |
| Search | `search()` | `search()` |
| Get | `get()` | `get()` |
| Get all | `get_all()` | `getAll()` |
| Update | `update()` | `update()` |
| Delete | `delete()` | `delete()` |
| Delete all | `delete_all()` | `deleteAll()` |
| History | `history()` | `history()` |
| Batch update | `batch_update()` | `batchUpdate()` |
| Batch delete | `batch_delete()` | `batchDelete()` |
| List users | `users()` | `users()` |
| Delete users | `delete_users()` | `deleteUsers()` |
| Get project | `project.get()` | `getProject()` |
| Update project | `project.update()` | `updateProject()` |
| Create webhook | `create_webhook()` | `createWebhook()` |
| Get webhooks | `get_webhooks()` | `getWebhooks()` |
| Update webhook | `update_webhook()` | `updateWebhook()` |
| Delete webhook | `delete_webhook()` | `deleteWebhook()` |
| Create export | `create_memory_export()` | `createMemoryExport()` |
| Get export | `get_memory_export()` | `getMemoryExport()` |
| Feedback | `feedback()` | `feedback()` |
**Rule:** Python uses `snake_case`, TypeScript uses `camelCase` for method names.
## Parameter Passing
```python
# Python: kwargs
client.add(messages, user_id="alice", metadata={"source": "chat"})
client.search("query", filters={"user_id": "alice"}, top_k=5, rerank=True)
```
```typescript
// TypeScript: options object with camelCase for top-level params, snake_case for filter keys
await client.add(messages, { userId: 'alice', metadata: { source: 'chat' } });
await client.search('query', { filters: { user_id: 'alice' }, topK: 5, rerank: true });
```
**v3:** Python uses `snake_case` everywhere. TypeScript uses `camelCase` for top-level params (`userId`, `topK`) but `snake_case` for filter keys (`user_id`, `agent_id`).
## Architectural Differences
| Aspect | Python | TypeScript |
|--------|--------|------------|
| HTTP library | httpx | axios |
| Default timeout | 300s | 60s |
| Sync support | Yes (`MemoryClient`) | No (all async) |
| Async support | Yes (`AsyncMemoryClient`) | All methods are async |
| Project management | `client.project.*` (separate class) | `client.getProject()` / `client.updateProject()` |
| Context manager | `async with AsyncMemoryClient()` | Not supported |
## Platform Features: Python-only
These methods exist in Python but not TypeScript:
| Method | Description |
|--------|-------------|
| `get_summary(filters)` | Get summary of memories |
| `reset()` | Delete ALL data (users + memories) |
| `project.create(name)` | Create a new project |
| `project.delete()` | Delete current project |
| `project.get_members()` | List project members |
| `project.add_member(email, role)` | Add member to project |
| `project.update_member(email, role)` | Change member role |
| `project.remove_member(email)` | Remove member |
## Platform Features: TypeScript-only
| Method | Description |
|--------|-------------|
| `deleteUser(data)` | Convenience method for single entity deletion |
| `ping()` | Health check endpoint |
## OSS Config Naming
| Python config key | TypeScript config key |
|-------------------|----------------------|
| `vector_store` | `vectorStore` |
| `history_db_path` | `historyDbPath` |
| `custom_instructions` | `customInstructions` |
## OSS Scope Parameter Naming
| Python | TypeScript |
|--------|------------|
| `user_id="alice"` | `userId: 'alice'` |
| `agent_id="bot"` | `agentId: 'bot'` |
| `run_id="session"` | `runId: 'session'` |
## Entity ID Passing (v3)
| Method | Python | TypeScript |
|--------|--------|------------|
| add() | Top-level: `user_id="alice"` | Top-level: `{ userId: 'alice' }` |
| search() | In filters: `filters={"user_id": "alice"}` | In filters: `{ filters: { user_id: 'alice' } }` |
| get_all() | In filters: `filters={"user_id": "alice"}` | In filters: `{ filters: { user_id: 'alice' } }` |
## Common Gotcha
When searching/filtering, both Python and TypeScript use `snake_case` for filter keys. TypeScript only uses `camelCase` for top-level method parameters:
```python
# Python - snake_case in filters
results = client.search("query", filters={"user_id": "alice"})
```
```typescript
// TypeScript - snake_case in filters, camelCase for top-level params
const results = await client.search('query', { filters: { user_id: 'alice' }, topK: 20 });
```
@@ -1,418 +0,0 @@
# Mem0 Node.js / TypeScript SDK Reference
Complete reference for the `mem0ai` npm package. Covers both the Platform client (managed API) and the Open Source self-hosted variant.
---
## Platform Client
### Installation
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
### MemoryClient
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-xxx' });
```
**Constructor:** `new MemoryClient({ apiKey })`. If `apiKey` is not provided, reads from `MEM0_API_KEY` environment variable.
- HTTP library: `axios`
- Timeout: 60 seconds
- Base URL: `https://api.mem0.ai`
- All methods are async (return `Promise`)
---
### Memory Methods
#### add(messages, options?)
Store new memories from messages.
```typescript
const messages = [
{ role: 'user', content: "I'm a vegetarian and allergic to nuts." },
{ role: 'assistant', content: "Got it! I'll remember that." },
];
await client.add(messages, { userId: 'alice' });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `messages` | `Message[]` | Array of `{role, content}` objects |
| `options.userId` | string | User identifier |
| `options.agentId` | string | Agent identifier |
| `options.appId` | string | Application identifier |
| `options.runId` | string | Session identifier |
| `options.metadata` | object | Custom key-value pairs |
| `options.infer` | boolean | If false, store raw text (default: true) |
**Returns:** `Promise<any>` -- list of events
#### search(query, options?)
Search memories by semantic similarity.
```typescript
const results = await client.search('dietary preferences', { filters: { user_id: 'alice' }, topK: 20 });
for (const mem of results.results) {
console.log(mem.memory, mem.score);
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | string | Natural language search query |
| `options.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, etc.) and/or `AND`/`OR`/`NOT` conditions |
| `options.topK` | number | Number of results (default: 20) |
| `options.rerank` | boolean | Enable semantic reranking (default: false) |
| `options.threshold` | number | Minimum similarity (default: 0.1) |
**Returns:** `Promise<SearchResult>` -- `{results: [{id, memory, score, ...}]}`
#### get(memoryId)
```typescript
const memory = await client.get('ea925981-...');
```
#### getAll(options?)
Retrieve all memories. Requires at least one entity identifier in filters.
```typescript
const memories = await client.getAll({ filters: { user_id: 'alice' } });
// With filters
const filtered = await client.getAll({
filters: { AND: [{ user_id: 'alice' }, { categories: { contains: 'health' } }] },
});
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `options.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, etc.) and/or `AND`/`OR`/`NOT` conditions |
| `options.page` | number | Page number |
| `options.pageSize` | number | Results per page |
#### update(memoryId, data)
```typescript
await client.update('ea925981-...', { text: 'Updated: vegan since 2024' });
await client.update('ea925981-...', { text: 'Updated', metadata: { verified: true } });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `memoryId` | string | Memory ID |
| `data.text` | string | New content |
| `data.metadata` | object | New metadata |
| `data.timestamp` | string | New timestamp |
#### delete(memoryId)
```typescript
await client.delete('ea925981-...');
```
#### deleteAll(options?)
```typescript
await client.deleteAll({ userId: 'alice' });
```
#### history(memoryId)
```typescript
const history = await client.history('ea925981-...');
// Returns: [{previousValue, newValue, action, timestamps}]
```
---
### Batch Methods
#### batchUpdate(memories)
```typescript
await client.batchUpdate([
{ memoryId: 'uuid-1', text: 'Updated text' },
{ memoryId: 'uuid-2', text: 'Another update' },
]);
```
#### batchDelete(memories)
```typescript
await client.batchDelete(['uuid-1', 'uuid-2', 'uuid-3']);
```
---
### User/Entity Management
#### users()
```typescript
const users = await client.users();
// Returns: {results: [{type: "user", name: "alice"}, ...]}
```
#### deleteUser(data) / deleteUsers(data)
```typescript
await client.deleteUser({ userId: 'alice' }); // Single entity
await client.deleteUsers({ agentId: 'bot-1' }); // Flexible
```
---
### Project Management
```typescript
// Get project config
const config = await client.getProject({ fields: ['customCategories'] });
// Update project settings
await client.updateProject({
customInstructions: 'Extract dietary preferences and health info',
customCategories: [{ health: 'Medical and dietary info' }],
});
```
---
### Webhooks
```typescript
// List
const webhooks = await client.getWebhooks({ projectId: 'proj_123' });
// Create
const webhook = await client.createWebhook({
url: 'https://your-app.com/webhook',
name: 'Memory Logger',
projectId: 'proj_123',
eventTypes: ['memory_add', 'memory_update'],
});
// Update
await client.updateWebhook({
webhookId: 'wh_123',
name: 'Updated Logger',
url: 'https://new-url.com',
});
// Delete
await client.deleteWebhook({ webhookId: 'wh_123' });
```
---
### Feedback
```typescript
await client.feedback({
memoryId: 'mem-123',
feedback: 'POSITIVE',
feedbackReason: 'Accurately captured preference',
});
```
---
### Export
```typescript
const exportReq = await client.createMemoryExport({
schema: JSON.stringify({ type: 'object', properties: { name: { type: 'string' } } }),
filters: { user_id: 'alice' },
});
const result = await client.getMemoryExport({ memoryExportId: exportReq.id });
```
---
### TypeScript Types
Key interfaces from `mem0.types.ts`:
```typescript
interface Message { role: string; content: string; }
interface Memory { id: string; memory: string; userId: string; categories: string[]; score?: number; /* ... */ }
interface MemoryOptions { userId?: string; agentId?: string; appId?: string; runId?: string; metadata?: object; /* ... */ }
interface SearchOptions { filters?: object; topK?: number; rerank?: boolean; threshold?: number; /* ... */ }
interface MemoryHistory { id: string; memoryId: string; previousValue: string; newValue: string; action: string; /* ... */ }
interface FeedbackPayload { memoryId: string; feedback: string; feedbackReason?: string; }
interface WebhookCreatePayload { url: string; name: string; projectId: string; eventTypes: string[]; }
```
---
## Open Source / Self-Hosted
### Installation
```bash
npm install mem0ai
```
### Memory Class
```typescript
import { Memory } from 'mem0ai/oss';
const m = new Memory(); // Uses default config
```
**Import:** `from 'mem0ai/oss'` (NOT the default export -- that is `MemoryClient` for Platform)
### Configuration
```typescript
const config = {
llm: {
provider: 'openai', // openai, groq, anthropic, google, ollama, lmstudio, mistral, azure
config: {
model: 'gpt-5-mini',
apiKey: 'sk-xxx',
},
},
embedder: {
provider: 'openai', // openai, ollama, lmstudio, google, azure, langchain, anthropic
config: {
model: 'text-embedding-3-small',
apiKey: 'sk-xxx',
},
},
vectorStore: {
provider: 'qdrant', // memory, qdrant, redis, supabase, langchain, azure_ai_search, pgvector
config: {
collectionName: 'my_memories',
host: 'localhost',
port: 6333,
},
},
historyDbPath: 'history.db',
customInstructions: '...',
disableHistory: false,
};
const m = new Memory(config);
// Or from dict with validation:
const m2 = Memory.fromConfig(config);
```
### Methods
All methods are async (return `Promise`):
#### add(messages, config)
```typescript
await m.add('I prefer dark mode', { userId: 'alice' });
await m.add([
{ role: 'user', content: 'I like hiking' },
{ role: 'assistant', content: 'Great outdoor activity!' },
], { userId: 'alice' });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `messages` | `string \| Message[]` | Content to store |
| `config.userId` | string | User identifier (at least one scope required) |
| `config.agentId` | string | Agent identifier |
| `config.runId` | string | Session identifier |
| `config.metadata` | object | Custom key-value pairs |
| `config.filters` | object | Additional filters |
| `config.infer` | boolean | LLM inference (default: true) |
**Returns:** `Promise<{results: [...], relations?: [...]}>`
#### search(query, config)
```typescript
const results = await m.search('dietary preferences', { filters: { user_id: 'alice' }, topK: 5 });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | string | Search query |
| `config.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, `run_id`, etc.) |
| `config.topK` | number | Max results (default: 20) |
#### get(memoryId) / getAll(config) / update(memoryId, data) / delete(memoryId) / deleteAll(config) / history(memoryId)
Same interface patterns. Note: OSS `update` takes a string for data, not an object.
```typescript
await m.update('mem-id', 'new content');
```
#### reset()
Clear the entire vector store and history.
```typescript
await m.reset();
```
---
## Key Differences: Platform vs OSS
| Aspect | Platform (`MemoryClient`) | OSS (`Memory`) |
|--------|--------------------------|----------------|
| **Import** | `import MemoryClient from 'mem0ai'` | `import { Memory } from 'mem0ai/oss'` |
| **Auth** | API key required (`MEM0_API_KEY`) | No API key -- config-based |
| **Execution** | API calls to `api.mem0.ai` | Local execution |
| **Infrastructure** | Fully managed | Self-managed vector DB, embedder, LLM |
| **Param style** | Top-level: `camelCase` (`userId`, `topK`), filter keys: `snake_case` (`user_id`) | Top-level: `camelCase` (`userId`, `topK`), filter keys: `snake_case` (`user_id`) |
| **Batch ops** | `batchUpdate`, `batchDelete` | Not available |
| **Webhooks** | Full CRUD | Not available |
| **Export** | `createMemoryExport` | Not available |
| **Feedback** | `feedback()` | Not available |
| **Project mgmt** | `getProject`, `updateProject` | Not available |
| **User listing** | `users()`, `deleteUser()` | Not available |
| **History** | Platform-managed | SQLite (configurable) |
---
## v2 Compatibility
If you're using SDK v2.x:
**Naming Changes:**
- Top-level params now use camelCase: `topK`, `rerank` (not `top_k`)
- Filter keys use snake_case: `user_id`, `agent_id`
- OSS: `limit` renamed to `topK`
**API Changes:**
```typescript
// v2 - top-level entity IDs, snake_case
await client.search("query", { user_id: "alice", top_k: 20 });
// v3 - filters object with snake_case keys, camelCase top-level params
await client.search("query", { filters: { user_id: "alice" }, topK: 20 });
```
**Default Changes:**
| Param | v2 | v3 |
|-------|----|----|
| `topK` | 100 | 20 |
| `threshold` | none | 0.1 |
| `rerank` | true | false |
**Removed:**
- `OutputFormat` and `API_VERSION` enums
- `organizationId`, `projectId` from constructor
- `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `expirationDate`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for details.
@@ -1,487 +0,0 @@
# Mem0 Python SDK Reference
Complete reference for the `mem0ai` Python package. Covers both the Platform client (managed API) and the Open Source self-hosted variant.
---
## Platform Client
### Installation
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
### MemoryClient (Synchronous)
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-xxx")
```
**Constructor:** `MemoryClient(api_key=None)`. If `api_key` is not provided, reads from `MEM0_API_KEY` environment variable. Raises `ValueError` if no key found.
- HTTP library: `httpx`
- Timeout: 300 seconds
- Base URL: `https://api.mem0.ai`
### AsyncMemoryClient (Asynchronous)
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="m0-xxx")
# Or use as context manager
async with AsyncMemoryClient(api_key="m0-xxx") as client:
results = await client.search("query", filters={"user_id": "alice"})
```
Same methods as `MemoryClient`, all `async`/`await`. Supports async context manager.
---
### Memory Methods
#### add(messages, **kwargs)
Store new memories from messages.
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `messages` | str \| dict \| list[dict] | required | Message content. Strings auto-convert to user messages |
| `user_id` | str | None | User identifier |
| `agent_id` | str | None | Agent identifier |
| `app_id` | str | None | Application identifier |
| `run_id` | str | None | Session/run identifier |
| `metadata` | dict | None | Custom key-value pairs |
| `infer` | bool | True | If False, store raw text without LLM inference |
| `custom_categories` | list | None | Override project categories |
| `custom_instructions` | str | None | Override extraction instructions |
| `timestamp` | int \| float \| str | None | Custom timestamp (Unix epoch or ISO 8601) |
**Returns:** `dict` -- list of events: `[{"id": "...", "event": "ADD", "data": {"memory": "..."}}]`
#### search(query, **kwargs)
Search memories by semantic similarity.
```python
results = client.search("dietary preferences", filters={"user_id": "alice"})
for mem in results.get("results", []):
print(mem["memory"], mem["score"])
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | str | required | Natural language search query |
| `filters` | dict | None | Filter object with entity IDs and/or `AND`/`OR`/`NOT` conditions (e.g., `{"user_id": "alice"}`) |
| `top_k` | int | 10 | Number of results |
| `rerank` | bool | False | Enable deep semantic reranking (+150-200ms) |
| `threshold` | float | 0.1 | Minimum similarity score |
| `fields` | list | None | Specific fields to return |
| `categories` | list | None | Filter by category |
**Returns:** `dict` -- `{"results": [{id, memory, user_id, categories, score, created_at, ...}]}`
#### get(memory_id)
Retrieve a single memory by ID.
```python
memory = client.get(memory_id="ea925981-...")
```
**Returns:** `dict` -- full memory object
#### get_all(**kwargs)
Retrieve all memories with optional filtering. Requires at least one entity identifier.
```python
memories = client.get_all(filters={"user_id": "alice"})
# With compound filters
memories = client.get_all(filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "health"}}]})
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `filters` | dict | None | Filter object with entity IDs and/or `AND`/`OR`/`NOT` conditions |
| `top_k` | int | None | Limit results |
| `page` | int | None | Page number |
| `page_size` | int | None | Results per page |
**Returns:** `dict` -- `{"results": [...]}`
#### update(memory_id, text=None, metadata=None, timestamp=None)
Update a memory's content, metadata, or timestamp. At least one parameter required.
```python
client.update("ea925981-...", text="Updated: vegan since 2024")
client.update("ea925981-...", metadata={"verified": True})
```
**Returns:** `dict` -- updated memory
#### delete(memory_id)
Permanently delete a single memory.
```python
client.delete("ea925981-...")
```
#### delete_all(**kwargs)
Delete all memories matching filters. Irreversible.
```python
client.delete_all(user_id="alice")
```
#### history(memory_id)
Get the change history of a memory.
```python
history = client.history("ea925981-...")
# Returns: [{previous_value, new_value, action, timestamps}]
```
---
### Batch Methods
#### batch_update(memories)
Update up to 1000 memories in a single request.
```python
client.batch_update([
{"memory_id": "uuid-1", "text": "Updated text"},
{"memory_id": "uuid-2", "text": "Another update", "metadata": {"verified": True}},
])
```
#### batch_delete(memories)
Delete up to 1000 memories in a single request.
```python
client.batch_delete([
{"memory_id": "uuid-1"},
{"memory_id": "uuid-2"},
])
```
---
### User/Entity Management
#### users()
List all users, agents, and sessions that have memories.
```python
users = client.users()
# Returns: {"results": [{"type": "user", "name": "alice"}, ...]}
```
#### delete_users(user_id=None, agent_id=None, app_id=None, run_id=None)
Delete a specific entity and all its memories.
```python
client.delete_users(user_id="alice")
```
#### reset()
Delete ALL users, agents, sessions, and memories. Complete data reset.
```python
client.reset()
```
---
### Export & Summary
#### create_memory_export(schema, **kwargs)
Create a structured export of memories.
```python
import json
schema = json.dumps({
"type": "object",
"properties": {
"name": {"type": "string"},
"preferences": {"type": "array", "items": {"type": "string"}},
}
})
export = client.create_memory_export(schema=schema, user_id="alice")
```
#### get_memory_export(**kwargs)
Retrieve a previously created export.
```python
result = client.get_memory_export(memory_export_id=export["id"])
```
#### get_summary(filters=None)
Get a summary of memories.
```python
summary = client.get_summary(filters={"user_id": "alice"})
```
---
### Feedback
#### feedback(memory_id, feedback=None, feedback_reason=None)
Provide quality feedback on a memory.
```python
client.feedback(
memory_id="mem-123",
feedback="POSITIVE", # POSITIVE | NEGATIVE | VERY_NEGATIVE | None (clear)
feedback_reason="Accurately captured preference"
)
```
---
### Webhooks
```python
# List
webhooks = client.get_webhooks(project_id="proj_123")
# Create
webhook = client.create_webhook(
url="https://your-app.com/webhook",
name="Memory Logger",
project_id="proj_123",
event_types=["memory_add", "memory_update"]
)
# Update
client.update_webhook(webhook_id=123, name="Updated", url="https://new-url.com")
# Delete
client.delete_webhook(webhook_id=123)
```
---
### Project Management
Access via `client.project.*`:
```python
# Get project config
config = client.project.get(fields=["custom_categories", "custom_instructions"])
# Update project settings
client.project.update(
custom_instructions="Extract dietary preferences and health info",
custom_categories=[{"health": "Medical and dietary info"}],
multilingual=True,
)
# Create/delete project
client.project.create(name="My Project", description="...")
client.project.delete()
# Member management
members = client.project.get_members()
client.project.add_member(email="user@example.com", role="READER") # READER or OWNER
client.project.update_member(email="user@example.com", role="OWNER")
client.project.remove_member(email="user@example.com")
```
---
## Open Source / Self-Hosted
### Installation
```bash
pip install mem0ai
```
### Memory Class
```python
from mem0 import Memory
m = Memory() # Uses default config (OpenAI embedder + in-memory vector store)
```
**Import:** `from mem0 import Memory` (NOT `MemoryClient` -- that is the Platform client)
### Configuration
```python
config = {
"llm": {
"provider": "openai", # openai, groq, azure, ollama, lmstudio, google, anthropic, mistral
"config": {
"model": "gpt-5-mini",
"api_key": "sk-xxx",
}
},
"embedder": {
"provider": "openai", # openai, ollama, azure, lmstudio, google, huggingface
"config": {
"model": "text-embedding-3-small",
"api_key": "sk-xxx",
}
},
"vector_store": {
"provider": "qdrant", # faiss, qdrant, pgvector, redis, supabase, azure_ai_search, memory
"config": {
"collection_name": "my_memories",
"host": "localhost",
"port": 6333,
}
},
"history_db_path": "history.db", # SQLite path for change history
"custom_instructions": "...", # Custom LLM prompt for extraction
}
m = Memory.from_config(config)
```
### Context Manager
```python
with Memory(config) as m:
m.add("I prefer dark mode", user_id="alice")
results = m.search("preferences", filters={"user_id": "alice"})
# SQLite connections released automatically
```
### Methods
All methods mirror the Platform client but run locally:
#### add(messages, *, user_id, agent_id, run_id, metadata, infer=True)
```python
m.add("I'm a vegetarian", user_id="alice")
m.add([
{"role": "user", "content": "I like hiking"},
{"role": "assistant", "content": "Great outdoor activity!"}
], user_id="alice")
```
At least one of `user_id`, `agent_id`, `run_id` required.
**Returns:** `{"results": [...], "relations": [...]}`
#### search(query, *, filters=None, top_k=20, threshold=0.1, rerank=False)
```python
results = m.search("dietary preferences", filters={"user_id": "alice"}, top_k=5)
```
Entity IDs (`user_id`, `agent_id`, `run_id`) must be passed inside the `filters` dict.
Supports filter operators: `eq`, `ne`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `contains`, `not_contains`.
#### get(memory_id) / get_all(**kwargs) / update(memory_id, data, metadata=None) / delete(memory_id) / delete_all(**kwargs) / history(memory_id)
Same interface as Platform client.
#### reset()
Clear the entire vector store collection and history database. Recreates the vector store.
```python
m.reset()
```
#### close()
Release SQLite connections. Called automatically when using context manager.
### AsyncMemory
```python
from mem0 import AsyncMemory
m = AsyncMemory(config)
await m.add("text", user_id="alice")
results = await m.search("query", filters={"user_id": "alice"})
```
---
## Key Differences: Platform vs OSS
| Aspect | Platform (`MemoryClient`) | OSS (`Memory`) |
|--------|--------------------------|----------------|
| **Import** | `from mem0 import MemoryClient` | `from mem0 import Memory` |
| **Auth** | API key required (`MEM0_API_KEY`) | No API key -- config-based |
| **Execution** | API calls to `api.mem0.ai` | Local execution |
| **Infrastructure** | Fully managed | Self-managed vector DB, embedder, LLM |
| **Entity filtering** | `filters={"user_id": "..."}` | `filters={"user_id": "..."}` |
| **Batch ops** | `batch_update`, `batch_delete` | Not available |
| **Webhooks** | Full CRUD | Not available |
| **Export** | `create_memory_export`, `get_memory_export` | Not available |
| **Feedback** | `feedback()` | Not available |
| **Project mgmt** | `client.project.*` | Not available |
| **User listing** | `users()`, `delete_users()` | Not available |
| **Custom prompts** | Via project settings | Direct config (`custom_instructions`) |
| **History** | Platform-managed | SQLite (configurable) |
| **Async** | `AsyncMemoryClient` | `AsyncMemory` |
---
## v2 Compatibility
If you're using SDK v2.x or the v2 API:
**API Changes:**
- **Entity IDs in search/get_all:** Pass `user_id`, `agent_id` as top-level kwargs instead of inside `filters`
```python
# v2
results = client.search("query", user_id="alice")
# v3
results = client.search("query", filters={"user_id": "alice"})
```
- **add() returns:** v2 returns ADD, UPDATE, DELETE events; v3 returns ADD only
**Default Changes:**
| Param | v2 | v3 |
|-------|----|----|
| `top_k` | 100 | 20 |
| `threshold` | None | 0.1 |
| `rerank` | True | False |
**Removed Parameters:**
- Constructor: `org_id`, `project_id`
- add(): `async_mode`, `output_format`, `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
- search()/get_all(): `enable_graph`
- Config: `enable_graph`, `graph_store`, `custom_fact_extraction_prompt` (renamed to `custom_instructions`)
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for full details.
@@ -1,150 +0,0 @@
# Mem0 Platform API Reference
REST API endpoints for the Mem0 Platform. Base URL: `https://api.mem0.ai`
All endpoints require: `Authorization: Token <MEM0_API_KEY>`
## Endpoints
| Operation | Method | URL |
|-----------|--------|-----|
| Add Memories | `POST` | `/v3/memories/add/` |
| Search Memories | `POST` | `/v3/memories/search/` |
| Get All Memories | `POST` | `/v3/memories/` |
| Get Single Memory | `GET` | `/v1/memories/{memory_id}/` |
| Update Memory | `PUT` | `/v1/memories/{memory_id}/` |
| Delete Memory | `DELETE` | `/v1/memories/{memory_id}/` |
| Delete All Memories | `DELETE` | `/v1/memories/?user_id=X&app_id=Y` |
| Get Event Status | `GET` | `/v1/event/{event_id}/` |
## Memory Object Structure
| Field | Type | Description |
|-------|------|-------------|
| `id` | string (UUID) | Unique memory identifier |
| `memory` | string | Text content of the memory |
| `user_id` | string | Associated user |
| `agent_id` | string (nullable) | Agent identifier |
| `app_id` | string (nullable) | Application identifier |
| `run_id` | string (nullable) | Run/session identifier |
| `metadata` | object | Custom key-value pairs |
| `categories` | array of strings | Auto-assigned category tags |
| `hash` | string | Content hash |
| `created_at` | datetime | Creation timestamp |
| `updated_at` | datetime | Last modification timestamp |
Search results additionally include `score` (relevance metric).
## Scoping Identifiers
Memories can be scoped to different levels:
| Scope | Parameter | Use Case |
|-------|-----------|----------|
| User | `user_id` | Per-user memory isolation |
| Agent | `agent_id` | Per-agent memory partitioning |
| Application | `app_id` | Cross-agent app-level memory |
| Run/Session | `run_id` | Session-scoped temporary memory |
**Critical:** Combining `user_id` and `agent_id` in a single AND filter yields empty results. Entities are stored separately. Use `OR` logic or separate queries.
## Processing Model
- Memories are processed **asynchronously** (v3 default)
- Add responses return queued `ADD` events only (v3 is ADD-only, no UPDATE/DELETE)
- Poll status via `GET /v1/event/{event_id}/`
## Filter System
Filters use nested JSON with a logical operator at the root:
```json
{
"AND": [
{"user_id": "alice"},
{"categories": {"contains": "finance"}},
{"created_at": {"gte": "2024-01-01"}}
]
}
```
Root must be `AND`, `OR`, or `NOT`. Simple shorthand `{"user_id": "alice"}` also works.
### Supported Operators
| Operator | Description |
|----------|-------------|
| `eq` | Equal to (default) |
| `ne` | Not equal to |
| `in` | Matches any value in array |
| `gt`, `gte` | Greater than / greater than or equal |
| `lt`, `lte` | Less than / less than or equal |
| `contains` | Case-sensitive containment |
| `icontains` | Case-insensitive containment |
| `*` | Wildcard -- matches any non-null value |
### Filterable Fields
| Field | Valid Operators |
|-------|-----------------|
| `user_id`, `agent_id`, `app_id`, `run_id` | `eq`, `ne`, `in`, `*` |
| `created_at`, `updated_at`, `timestamp` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` |
| `categories` | `eq`, `ne`, `in`, `contains` |
| `metadata` | `eq`, `ne`, `contains` (top-level keys only) |
| `keywords` | `contains`, `icontains` |
| `memory_ids` | `in` |
### Filter Constraints
1. **Entity scope partitioning:** `user_id` AND `agent_id` in one `AND` block yields empty results.
2. **Metadata limitations:** Only top-level keys. Only `eq`, `contains`, `ne`. No `in` or `gt`.
3. **Operator syntax:** Use `gte`, `lt`, `ne`. SQL-style (`>=`, `!=`) rejected.
4. **Entity filter required for get-all:** At least one of `user_id`, `agent_id`, `app_id`, or `run_id`.
5. **Wildcard excludes null:** `*` matches only non-null values.
6. **Date format:** ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). Timezone-naive defaults to UTC.
## Response Formats
### Add Response (v3)
```json
{
"message": "Memory processing has been queued for background execution",
"status": "PENDING",
"event_id": "evt-uuid"
}
```
v3 is ADD-only. No UPDATE or DELETE events.
### Search Response
```json
{
"results": [
{
"id": "ea925981-...",
"memory": "Is a vegetarian and allergic to nuts.",
"user_id": "user123",
"categories": ["food", "health"],
"score": 0.89,
"created_at": "2024-07-26T10:29:36.630547-07:00"
}
]
}
```
In v3, `score` is a combined multi-signal relevance score.
### Get All Response (v3)
```json
{
"count": 123,
"next": "https://api.mem0.ai/v3/memories/?page=2&page_size=50",
"previous": null,
"results": [...]
}
```
v3 returns paginated envelope. Use `page` and `page_size` query params.
@@ -1,330 +0,0 @@
# Mem0 Platform Architecture
How Mem0 processes, stores, and retrieves memories under the hood.
## Table of Contents
- [Core Concept](#core-concept)
- [Memory Processing Pipeline](#memory-processing-pipeline)
- [Retrieval Pipeline](#retrieval-pipeline)
- [Memory Lifecycle](#memory-lifecycle)
- [Memory Object Structure](#memory-object-structure)
- [Scoping & Multi-Tenancy](#scoping--multi-tenancy)
- [Memory Layers](#memory-layers)
- [Performance Characteristics](#performance-characteristics)
---
## Core Concept
Mem0 is a managed memory layer that sits between your AI application and users. Every integration follows the same 3-step loop:
```
User Input → Retrieve relevant memories → Enrich LLM prompt → Generate response → Store new memories
```
Mem0 handles the complexity of extraction, deduplication, conflict resolution, and semantic retrieval so your application only needs to call `search()` and `add()`.
**Storage architecture:**
- **Vector store**: Embeddings for semantic similarity search
- **Entity store**: Automatic entity linking for relationship-aware retrieval
---
## Memory Processing Pipeline
### What happens when you call `client.add()`
```
Messages In
│
▼
┌─────────────────────┐
│ 1. EXTRACTION │ Single LLM call extracts all distinct new facts
│ (infer=True) │ If infer=False, stores raw text as-is
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. DEDUPLICATION │ Hash-based dedup (MD5 prevents exact duplicates)
│ │ No UPDATE/DELETE - v3 is ADD-only
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 3. STORAGE │ Batch embed → vector store
│ │ Entity extraction → entity store
└─────────┬───────────┘
│
▼
Memory Object
```
### Processing (v3)
v3 processes memories asynchronously by default:
- API returns immediately: `{"status": "PENDING", "event_id": "evt-..."}`
- Poll status via `GET /v1/event/{event_id}/`
- Use webhooks for completion notifications
### Extraction modes
**Inferred (`infer=True`, default):**
- LLM extracts structured facts from conversation
- Conflict resolution deduplicates and resolves contradictions
- Best for: natural conversation → memory
**Raw (`infer=False`):**
- Stores text exactly as provided, no LLM processing
- Skips conflict resolution — same fact can be stored twice
- Only `user` role messages are stored; `assistant` messages ignored
- Best for: bulk imports, pre-structured data, migrations
**Warning:** Don't mix `infer=True` and `infer=False` for the same data — the same fact will be stored twice.
---
## Retrieval Pipeline (v3)
### What happens when you call `client.search()`
```
Query In
│
▼
┌─────────────────────┐
│ 1. PREPROCESSING │ Lemmatize keywords, extract entities
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. PARALLEL SCORING │ Semantic search (vector similarity)
│ │ BM25 keyword search (term matching)
│ │ Entity matching (entity graph boost)
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 3. SCORE FUSION │ Combine signals into single score
│ │ Optional: rerank=True for deep reordering
└─────────┬───────────┘
│
▼
Results (combined score per memory)
```
### v3 Search Defaults
| Parameter | Default | Notes |
|-----------|---------|-------|
| `top_k` | 20 | Was 100 in v2 |
| `threshold` | 0.1 | Was None in v2 |
| `rerank` | False | Was True in v2 |
### Implicit null scoping
When you search with `filters={"user_id": "alice"}` only, Mem0 returns memories where `agent_id`, `app_id`, and `run_id` are all null. This prevents cross-scope leakage by default.
To include memories with non-null fields, use explicit filters:
```python
# Gets memories for alice regardless of agent/app/run
filters={"OR": [{"user_id": "alice"}]}
```
---
## Memory Lifecycle (v3)
v3 uses ADD-only extraction. Memories accumulate over time rather than being consolidated.
### Creation
- `client.add(messages, user_id="...")`
- Single-pass extraction → deduplication → storage
- Returns `{"event_id": "...", "status": "PENDING"}`
### Updates
- `client.update(memory_id, text="...")` replaces text
- Batch: `client.batch_update([...])`
### Deletion
- Single: `client.delete(memory_id)`
- Batch: `client.batch_delete([...])`
- Bulk: `client.delete_all(filters={"user_id": "alice"})`
---
## Memory Object Structure
```json
{
"id": "uuid-string",
"memory": "Extracted memory text",
"user_id": "user-identifier",
"agent_id": null,
"app_id": null,
"run_id": null,
"metadata": { "source": "chat", "priority": "high" },
"categories": ["health", "preferences"],
"created_at": "2025-03-12T12:34:56Z",
"updated_at": "2025-03-12T12:34:56Z",
"structured_attributes": {
"day": 12, "month": 3, "year": 2025,
"hour": 12, "minute": 34,
"day_of_week": "wednesday",
"is_weekend": false,
"quarter": 1, "week_of_year": 11
},
"score": 0.85
}
```
| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Unique identifier, used for update/delete |
| `memory` | string | Extracted or stored text content |
| `user_id` | string | Primary entity scope |
| `agent_id` | string | Agent scope |
| `app_id` | string | Application scope |
| `run_id` | string | Session/run scope |
| `metadata` | object | Custom key-value pairs for filtering |
| `categories` | array | Auto-assigned or custom category tags |
| `created_at` | datetime | Creation timestamp |
| `updated_at` | datetime | Last modification timestamp |
| `structured_attributes` | object | Temporal breakdown for time-based queries |
| `score` | float | Semantic similarity (search results only, 0-1) |
---
## Scoping & Multi-Tenancy
Mem0 separates memories across four dimensions to prevent data mixing:
| Dimension | Field | Purpose | Example |
|-----------|-------|---------|---------|
| User | `user_id` | Persistent persona or account | `"customer_6412"` |
| Agent | `agent_id` | Distinct agent or tool | `"meal_planner"` |
| App | `app_id` | Product surface or deployment | `"ios_retail_app"` |
| Session | `run_id` | Short-lived flow or thread | `"ticket-9241"` |
### Storage model
Each entity combination creates separate records. A memory with `user_id="alice"` is stored separately from one with `user_id="alice"` + `agent_id="bot"`.
### Critical: cross-entity queries
```python
# This returns NOTHING — user and agent memories are stored separately
filters={"AND": [{"user_id": "alice"}, {"agent_id": "bot"}]}
# Use OR to query multiple scopes
filters={"OR": [{"user_id": "alice"}, {"agent_id": "bot"}]}
# Use wildcard to include any non-null value
filters={"AND": [{"user_id": "*"}]} # All users (excludes null)
```
### Recommended scoping patterns
```python
# User-level: persistent preferences
client.add(messages, user_id="alice")
# Session-level: temporary context
client.add(messages, user_id="alice", run_id="session_123")
# Clean up when done: client.delete_all(run_id="session_123")
# Agent-level: agent-specific knowledge
client.add(messages, agent_id="support_bot", app_id="helpdesk")
# Multi-tenant: full isolation
client.add(messages, user_id="alice", agent_id="bot", app_id="acme_corp", run_id="ticket_42")
```
---
## Memory Layers
Mem0 supports three layers of memory, from shortest to longest lived:
### Conversation memory
- In-flight messages within a single turn
- Tool calls, chain-of-thought reasoning
- **Lifetime:** Single response — lost after turn finishes
- **Managed by:** Your application, not Mem0
### Session memory
- Short-lived facts for current task or channel
- Multi-step flows (onboarding, debugging, support tickets)
- **Lifetime:** Minutes to hours
- **Managed by:** Mem0 via `run_id` parameter
- Clean up with `client.delete_all(run_id="session_id")`
### User memory
- Long-lived knowledge tied to a person or account
- Personal preferences, account state, compliance details
- **Lifetime:** Weeks to forever
- **Managed by:** Mem0 via `user_id` parameter
- Persists across all sessions and interactions
### How layering works in practice
```python
def chat(user_input: str, user_id: str, session_id: str) -> str:
# 1. Retrieve user memories (long-term preferences)
user_mems = mem0.search(user_input, filters={"user_id": user_id})
# 2. Retrieve session memories (current task context)
session_mems = mem0.search(user_input, filters={
"AND": [{"user_id": user_id}, {"run_id": session_id}]
})
# 3. Combine both layers for LLM context
context = format_memories(user_mems) + format_memories(session_mems)
# 4. Generate response
response = llm.generate(context=context, input=user_input)
# 5. Store in session scope (temporary) + user scope (persistent)
messages = [{"role": "user", "content": user_input}, {"role": "assistant", "content": response}]
mem0.add(messages, user_id=user_id, run_id=session_id)
return response
```
---
## Performance Characteristics
### Latency
| Operation | Typical Latency |
|-----------|----------------|
| Hybrid search (v3 default) | ~100-150ms |
| + reranking | +150-200ms |
| Add (async) | < 50ms response |
### Processing
- **Async (default):** Returns immediately, processes in background
- **Batch operations:** Up to 1000 memories per batch_update/batch_delete
- **Webhooks:** Real-time notifications when async processing completes
### Scoping strategy for performance
- Use `user_id` for all user-facing queries (most common, fastest)
- Add `run_id` for session isolation (narrows search space)
- Avoid wildcard `"*"` filters on large datasets (scans all non-null records)
- Use `top_k` to limit result count when you only need a few memories
---
## Comparison with Alternatives
| Approach | Pros | Cons |
|----------|------|------|
| **Raw vector DB** | Fast, full control | No extraction, no dedup, no conflict resolution |
| **In-memory chat history** | Zero latency | Lost on restart, no cross-session, grows unbounded |
| **RAG over documents** | Good for static knowledge | No personalization, no memory updates |
| **Mem0 Platform** | Managed extraction + dedup + graph + scoping | External dependency, async processing delay |
Mem0 combines the best of vector search (semantic retrieval) with automatic extraction (LLM-powered), conflict resolution (deduplication), and structured scoping (multi-tenancy) — in a single managed API.
@@ -1,425 +0,0 @@
# Platform Features -- Mem0 Platform
Additional platform capabilities beyond core CRUD operations.
## Table of Contents
- [Advanced Retrieval](#advanced-retrieval)
- [Entity Linking](#entity-linking)
- [Custom Categories](#custom-categories)
- [Custom Instructions](#custom-instructions)
- [Criteria Retrieval](#criteria-retrieval)
- [Feedback Mechanism](#feedback-mechanism)
- [Memory Export](#memory-export)
- [Group Chat](#group-chat)
- [MCP Integration](#mcp-integration)
- [Webhooks](#webhooks)
- [Multimodal Support](#multimodal-support)
## Advanced Retrieval
### Hybrid Search (v3 Default)
v3 uses multi-signal hybrid search combining:
- **Semantic search** (vector similarity)
- **BM25 keyword search** (normalized term matching)
- **Entity matching** (entity graph boost)
This is automatic — no configuration needed.
### Reranking (`rerank=True`)
Deep semantic reordering of results — most relevant first.
- Latency: +150-200ms
- Default: `False` (was `True` in v2)
- Best for: user-facing results, top-N precision
**Python:**
```python
results = client.search(query, filters={"user_id": "user123"}, rerank=True)
```
**TypeScript:**
```typescript
const results = await client.search(query, {
filters: { user_id: 'user123' },
rerank: true,
});
```
---
## Entity Linking
v3 replaces graph memory with built-in entity linking. Entities (proper nouns, quoted text, compound noun phrases) are automatically extracted and linked across memories.
### How It Works
1. **Extraction**: During `add()`, entities are automatically extracted from memory text
2. **Storage**: Entities are stored in a parallel collection (`{collection}_entities`)
3. **Retrieval**: During `search()`, query entities are matched and used to boost relevant memories
Entity linking is automatic — no configuration required. The boost is folded into the combined `score` on each result.
### v2 Migration Note
If you were using `enable_graph=True` in v2:
- Remove `enable_graph` from all API calls
- Remove `graph_store` from OSS configuration
- Entity relationships are now consumed through retrieval ranking, not exposed as a separate `relations` array
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for details.
---
## Custom Categories
Replace Mem0's default 15 labels with domain-specific categories. The system automatically tags memories to the closest matching category.
### Default Categories (15)
`personal_details`, `family`, `professional_details`, `sports`, `travel`, `food`, `music`, `health`, `technology`, `hobbies`, `fashion`, `entertainment`, `milestones`, `user_preferences`, `misc`
### Configuration
**Set project-level categories:**
```python
new_categories = [
{"lifestyle_management": "Tracks daily routines, habits, wellness activities"},
{"seeking_structure": "Documents goals around creating routines and systems"},
{"personal_information": "Basic information about the user"}
]
client.project.update(custom_categories=new_categories)
```
```javascript
await client.updateProject({ customCategories: newCategories });
```
**Retrieve active categories:**
```python
categories = client.project.get(fields=["custom_categories"])
```
### Key Constraint
Per-request overrides (`custom_categories=...` on `client.add`) are **not supported** on the managed API. Only project-level configuration works. Workaround: store ad-hoc labels in `metadata` field.
---
## Custom Instructions
Natural language filters that control what information Mem0 extracts when creating memories.
### Set Instructions
```python
client.project.update(custom_instructions="Your guidelines here...")
```
```javascript
await client.updateProject({ customInstructions: "Your guidelines here..." });
```
### Template Structure
1. **Task Description** -- brief extraction overview
2. **Information Categories** -- numbered sections with specific details to capture
3. **Processing Guidelines** -- quality and handling rules
4. **Exclusion List** -- sensitive/irrelevant data to filter out
### Domain Examples
**E-commerce:** Capture product issues, preferences, service experience; exclude payment data.
**Education:** Extract learning progress, student preferences, performance patterns; exclude specific grades.
**Finance:** Track financial goals, life events, investment interests; exclude account numbers and SSNs.
### Best Practices
- Start simply, test with sample messages, iterate based on results
- Avoid overly lengthy instructions
- Be specific about what to include AND exclude
---
## Criteria Retrieval
Custom attribute-based memory ranking using LLM-evaluated criteria with weights. Goes beyond semantic similarity to prioritize memories based on domain-specific signals.
### Configuration
```python
# Define criteria at project level
retrieval_criteria = [
{"name": "joy", "description": "Positive emotions like happiness and excitement", "weight": 3},
{"name": "curiosity", "description": "Inquisitiveness and desire to learn", "weight": 2},
{"name": "urgency", "description": "Time-sensitive or high-priority items", "weight": 4},
]
client.project.update(retrieval_criteria=retrieval_criteria)
```
```typescript
await client.updateProject({
retrievalCriteria: [
{ name: 'joy', description: 'Positive emotions', weight: 3 },
{ name: 'urgency', description: 'Time-sensitive items', weight: 4 },
],
});
```
### Usage
Once configured, `client.search()` automatically applies criteria ranking:
```python
# Criteria-weighted results returned automatically
results = client.search("Why am I feeling happy?", filters={"user_id": "alice"})
```
**Best for:** Wellness assistants, tutoring platforms, productivity tools — any app needing intent-aware retrieval.
---
## Feedback Mechanism
Provide feedback on extracted memories to improve system quality over time.
### Feedback Types
| Type | Meaning |
|------|---------|
| `POSITIVE` | Memory is useful and accurate |
| `NEGATIVE` | Memory is not useful |
| `VERY_NEGATIVE` | Memory is harmful or completely wrong |
| `None` | Clear existing feedback |
### Usage
**Python:**
```python
client.feedback(
memory_id="mem-123",
feedback="POSITIVE",
feedback_reason="Accurately captured dietary preference"
)
# Bulk feedback
for item in feedback_data:
client.feedback(**item)
```
**TypeScript:**
```typescript
await client.feedback('mem-123', {
feedback: 'POSITIVE',
feedbackReason: 'Accurately captured dietary preference',
});
```
---
## Memory Export
Create structured exports of memories using customizable schemas with filters.
### Usage
```python
import json
# Define export schema
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"preferences": {"type": "array", "items": {"type": "string"}},
"health_info": {"type": "string"},
}
}
# Create export
response = client.create_memory_export(
schema=json.dumps(schema),
filters={"user_id": "alice"},
export_instructions="Create comprehensive profile based on all memories"
)
# Retrieve export (may take a moment to process)
result = client.get_memory_export(memory_export_id=response["id"])
```
**Best for:** Data analytics, user profile generation, compliance audits, CRM sync.
---
## Group Chat
Process multi-participant conversations and automatically attribute memories to individual speakers.
### Usage
```python
messages = [
{"role": "user", "name": "Alice", "content": "I think we should use React for the frontend"},
{"role": "user", "name": "Bob", "content": "I prefer Vue.js, it's simpler for our use case"},
{"role": "assistant", "content": "Both are great choices. Let me note your preferences."},
]
# Mem0 automatically attributes memories to each speaker
response = client.add(messages, run_id="team_meeting_1")
# Retrieve Alice's memories from that session
alice_mems = client.get_all(
filters={"AND": [{"user_id": "alice"}, {"run_id": "team_meeting_1"}]}
)
```
Use the `name` field in messages to identify speakers. Mem0 maps names to entity scopes automatically.
---
## MCP Integration
Model Context Protocol integration enables AI clients (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode) to manage Mem0 memory autonomously.
### Setup
Add Mem0 MCP to your clients with a single command:
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
```
### Available MCP Tools
The MCP server exposes 9 memory tools that AI agents can use autonomously:
- Add, search, get, update, delete memories
- Get history, list users, delete users
- Search Mem0 documentation
### How It Works
1. Add Mem0 MCP to your AI client using the setup command above
2. The agent autonomously decides when to store/retrieve memories
3. No manual API calls needed — the agent manages memory as part of its reasoning
**Best for:** Universal AI client integration — one protocol works everywhere.
---
## Webhooks
Real-time event notifications for memory operations.
### Supported Events
| Event | Trigger |
|-------|---------|
| `memory_add` | Memory created |
| `memory_update` | Memory modified |
| `memory_delete` | Memory removed |
| `memory_categorize` | Memory tagged |
### Create Webhook
Note: `project_id` here refers to the Mem0 dashboard project scope for webhooks — not the deprecated client init parameter.
```python
webhook = client.create_webhook(
url="https://your-app.com/webhook",
name="Memory Logger",
project_id="proj_123",
event_types=["memory_add", "memory_categorize"]
)
```
### Manage Webhooks
```python
# Retrieve
webhooks = client.get_webhooks(project_id="proj_123")
# Update
client.update_webhook(
name="Updated Logger",
url="https://your-app.com/new-webhook",
event_types=["memory_update", "memory_add"],
webhook_id="wh_123"
)
# Delete
client.delete_webhook(webhook_id="wh_123")
```
### Payload Structure
Memory events contain: ID, data object with memory content, event type (`ADD`/`UPDATE`/`DELETE`).
Categorization events contain: memory ID, event type (`CATEGORIZE`), assigned category labels.
---
## Multimodal Support
Mem0 can process images and documents alongside text.
### Supported Media Types
- Images: JPG, PNG
- Documents: MDX, TXT, PDF
### Image via URL
```python
image_message = {
"role": "user",
"content": {
"type": "image_url",
"image_url": {"url": "https://example.com/image.jpg"}
}
}
client.add([image_message], user_id="alice")
```
### Image via Base64
```python
import base64
with open("photo.jpg", "rb") as f:
base64_image = base64.b64encode(f.read()).decode("utf-8")
image_message = {
"role": "user",
"content": {
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
}
}
client.add([image_message], user_id="alice")
```
### Document (MDX/TXT)
```python
doc_message = {
"role": "user",
"content": {"type": "mdx_url", "mdx_url": {"url": document_url}}
}
client.add([doc_message], user_id="alice")
```
### PDF Document
```python
pdf_message = {
"role": "user",
"content": {"type": "pdf_url", "pdf_url": {"url": pdf_url}}
}
client.add([pdf_message], user_id="alice")
```
@@ -1,395 +0,0 @@
# Mem0 Integration Patterns
Working code examples for integrating Mem0 Platform with popular AI frameworks.
All examples use `MemoryClient` (Platform API key).
Code examples are sourced from official Mem0 integration docs at docs.mem0.ai, simplified for quick reference.
---
## Common Pattern
Every integration follows the same 3-step loop:
1. **Retrieve** -- search relevant memories before generating a response
2. **Generate** -- include memories as context in the LLM prompt
3. **Store** -- save the interaction back to Mem0 for future use
---
## LangChain
Source: [docs.mem0.ai/integrations/langchain](https://docs.mem0.ai/integrations/langchain)
```python
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from mem0 import MemoryClient
llm = ChatOpenAI(model="gpt-5-mini")
mem0 = MemoryClient()
prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="You are a helpful travel agent AI. Use the provided context to personalize your responses."),
MessagesPlaceholder(variable_name="context"),
HumanMessage(content="{input}")
])
def retrieve_context(query: str, user_id: str):
"""Retrieve relevant memories from Mem0"""
memories = mem0.search(query, filters={"user_id": user_id})
memory_list = memories['results']
serialized = ' '.join([m["memory"] for m in memory_list])
return [
{"role": "system", "content": f"Relevant information: {serialized}"},
{"role": "user", "content": query}
]
def chat_turn(user_input: str, user_id: str) -> str:
# 1. Retrieve
context = retrieve_context(user_input, user_id)
# 2. Generate
chain = prompt | llm
response = chain.invoke({"context": context, "input": user_input})
# 3. Store
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": response.content}],
user_id=user_id
)
return response.content
```
---
## CrewAI
Source: [docs.mem0.ai/integrations/crewai](https://docs.mem0.ai/integrations/crewai)
CrewAI has native Mem0 integration via `memory_config`:
```python
from crewai import Agent, Task, Crew, Process
from mem0 import MemoryClient
client = MemoryClient()
# Store user preferences first
messages = [
{"role": "user", "content": "I am more of a beach person than a mountain person."},
{"role": "assistant", "content": "Noted! I'll recommend beach destinations."},
{"role": "user", "content": "I like Airbnb more than hotels."},
]
client.add(messages, user_id="crew_user_1")
# Create agent
travel_agent = Agent(
role="Personalized Travel Planner",
goal="Plan personalized travel itineraries",
backstory="You are a seasoned travel planner.",
memory=True,
)
# Create task
task = Task(
description="Find places to live, eat, and visit in San Francisco.",
expected_output="A detailed list of places to live, eat, and visit.",
agent=travel_agent,
)
# Setup crew with Mem0 memory
crew = Crew(
agents=[travel_agent],
tasks=[task],
process=Process.sequential,
memory=True,
memory_config={
"provider": "mem0",
"config": {"user_id": "crew_user_1"},
}
)
result = crew.kickoff()
```
---
## Vercel AI SDK
> **Dedicated skill available.** For comprehensive Vercel AI SDK documentation, see the [mem0-vercel-ai-sdk skill](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk).
Install: `npm install @mem0/vercel-ai-provider`
Quick example (wrapped model with automatic memory):
```typescript
import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const { text } = await generateText({
model: mem0("gpt-5-mini", { user_id: "borat" }),
prompt: "Suggest me a good car to buy!",
});
```
Supported providers: `openai`, `anthropic`, `google`, `groq`, `cohere`
---
## OpenAI Agents SDK
Source: [docs.mem0.ai/integrations/openai-agents-sdk](https://docs.mem0.ai/integrations/openai-agents-sdk)
```python
from agents import Agent, Runner, function_tool
from mem0 import MemoryClient
mem0 = MemoryClient()
@function_tool
def search_memory(query: str, user_id: str) -> str:
"""Search through past conversations and memories"""
memories = mem0.search(query, filters={"user_id": user_id}, top_k=3)
if memories and memories.get('results'):
return "\n".join([f"- {mem['memory']}" for mem in memories['results']])
return "No relevant memories found."
@function_tool
def save_memory(content: str, user_id: str) -> str:
"""Save important information to memory"""
mem0.add([{"role": "user", "content": content}], user_id=user_id)
return "Information saved to memory."
agent = Agent(
name="Personal Assistant",
instructions="""You are a helpful personal assistant with memory capabilities.
Use search_memory to recall past conversations.
Use save_memory to store important information.""",
tools=[search_memory, save_memory],
model="gpt-5-mini"
)
result = Runner.run_sync(agent, "I love Italian food and I'm planning a trip to Rome next month")
print(result.final_output)
```
### Multi-Agent with Handoffs
```python
from agents import Agent, Runner, function_tool
travel_agent = Agent(
name="Travel Planner",
instructions="You are a travel planning specialist. Use search_memory and save_memory tools.",
tools=[search_memory, save_memory],
model="gpt-5-mini"
)
health_agent = Agent(
name="Health Advisor",
instructions="You are a health and wellness advisor. Use search_memory and save_memory tools.",
tools=[search_memory, save_memory],
model="gpt-5-mini"
)
triage_agent = Agent(
name="Personal Assistant",
instructions="""Route travel questions to Travel Planner, health questions to Health Advisor.""",
handoffs=[travel_agent, health_agent],
model="gpt-5-mini"
)
result = Runner.run_sync(triage_agent, "Plan a healthy meal for my Italy trip")
```
---
## Pipecat (Voice / Real-Time)
Source: [docs.mem0.ai/integrations/pipecat](https://docs.mem0.ai/integrations/pipecat)
```python
from pipecat.services.mem0 import Mem0MemoryService
memory = Mem0MemoryService(
api_key=os.getenv("MEM0_API_KEY"),
user_id="alice",
agent_id="voice_bot",
params={
"search_limit": 10,
"search_threshold": 0.1,
"system_prompt": "Here are your past memories:",
"add_as_system_message": True,
}
)
# Use in pipeline
pipeline = Pipeline([
transport.input(),
stt,
user_context,
memory, # Memory enhances context automatically
llm,
transport.output(),
assistant_context
])
```
---
## LangGraph
Source: [docs.mem0.ai/integrations/langgraph](https://docs.mem0.ai/integrations/langgraph)
State-based agent workflows with memory persistence. Best for complex conversation flows with branching logic.
```python
from typing import Annotated, TypedDict, List
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from mem0 import MemoryClient
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
llm = ChatOpenAI(model="gpt-5-mini")
mem0 = MemoryClient()
class State(TypedDict):
messages: Annotated[List[HumanMessage | AIMessage], add_messages]
mem0_user_id: str
def chatbot(state: State):
messages = state["messages"]
user_id = state["mem0_user_id"]
# Retrieve relevant memories
memories = mem0.search(messages[-1].content, filters={"user_id": user_id})
context = "Relevant context:\n"
for memory in memories["results"]:
context += f"- {memory['memory']}\n"
system_message = SystemMessage(content=f"""You are a helpful support assistant.
{context}""")
response = llm.invoke([system_message] + messages)
# Store the interaction
mem0.add(
[{"role": "user", "content": messages[-1].content},
{"role": "assistant", "content": response.content}],
user_id=user_id
)
return {"messages": [response]}
graph = StateGraph(State)
graph.add_node("chatbot", chatbot)
graph.add_edge(START, "chatbot")
app = graph.compile()
# Usage
result = app.invoke({
"messages": [HumanMessage(content="I need help with my order")],
"mem0_user_id": "customer_123"
})
```
---
## LlamaIndex
Source: [docs.mem0.ai/integrations/llama-index](https://docs.mem0.ai/integrations/llama-index)
Install: `pip install llama-index-core llama-index-memory-mem0`
LlamaIndex has native Mem0 support via `Mem0Memory`. Works with ReAct and FunctionCalling agents.
```python
from llama_index.memory.mem0 import Mem0Memory
context = {"user_id": "alice", "agent_id": "llama_agent_1"}
memory = Mem0Memory.from_client(
context=context,
search_msg_limit=4, # messages from chat history used for retrieval (default: 5)
)
# Use with LlamaIndex agent
from llama_index.core.agent import FunctionCallingAgent
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-5-mini")
agent = FunctionCallingAgent.from_tools(
tools=[],
llm=llm,
memory=memory,
verbose=True,
)
response = agent.chat("I prefer vegetarian restaurants")
# Memory automatically stores and retrieves context
response = agent.chat("What kind of food do I like?")
# Agent retrieves the vegetarian preference from Mem0
```
---
## AutoGen
Source: [docs.mem0.ai/integrations/autogen](https://docs.mem0.ai/integrations/autogen)
Install: `pip install autogen mem0ai`
Multi-agent conversational systems with memory persistence.
```python
from autogen import ConversableAgent
from mem0 import MemoryClient
memory_client = MemoryClient()
USER_ID = "alice"
agent = ConversableAgent(
"chatbot",
llm_config={"config_list": [{"model": "gpt-5-mini", "api_key": os.environ["OPENAI_API_KEY"]}]},
code_execution_config=False,
human_input_mode="NEVER",
)
def get_context_aware_response(question: str) -> str:
# Retrieve memories for context
relevant_memories = memory_client.search(question, filters={"user_id": USER_ID})
context = "\n".join([m["memory"] for m in relevant_memories.get("results", [])])
prompt = f"""Answer considering previous interactions:
Previous context: {context}
Question: {question}"""
reply = agent.generate_reply(messages=[{"content": prompt, "role": "user"}])
# Store the new interaction
memory_client.add(
[{"role": "user", "content": question}, {"role": "assistant", "content": reply}],
user_id=USER_ID
)
return reply
```
---
## All Supported Frameworks
Beyond the examples above, Mem0 integrates with:
| Framework | Type | Install |
|-----------|------|---------|
| [Mastra](https://docs.mem0.ai/integrations/mastra) | TS agent framework | `npm install @mastra/mem0` |
| [ElevenLabs](https://docs.mem0.ai/integrations/elevenlabs) | Voice AI | `pip install elevenlabs mem0ai` |
| [LiveKit](https://docs.mem0.ai/integrations/livekit) | Real-time voice/video | `pip install livekit-agents mem0ai` |
| [Camel AI](https://docs.mem0.ai/integrations/camel-ai) | Multi-agent framework | `pip install camel-ai[all] mem0ai` |
| [AWS Bedrock](https://docs.mem0.ai/integrations/aws-bedrock) | Cloud LLM provider | `pip install boto3 mem0ai` |
| [Dify](https://docs.mem0.ai/integrations/dify) | Low-code AI platform | Plugin-based |
| [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) | Google agent framework | `pip install google-adk mem0ai` |
For the general Python pattern (no framework), see the "Common integration pattern" in [SKILL.md](../SKILL.md).
@@ -1,119 +0,0 @@
# Mem0 Platform Quickstart
Get running with Mem0 in 2 minutes. No infrastructure to deploy -- just an API key.
## Prerequisites
- Python 3.10+ or Node.js 18+
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill-quickstart))
## Python Setup
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="your-api-key")
# Add a memory
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
]
client.add(messages, user_id="user123")
# Search memories
results = client.search("What are my dietary restrictions?", filters={"user_id": "user123"})
print(results)
```
### Async Client
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="your-api-key")
await client.add(messages, user_id="user123")
results = await client.search("query", filters={"user_id": "user123"})
```
## TypeScript / JavaScript Setup
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
```javascript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'your-api-key' });
// Add a memory
const messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
];
await client.add(messages, { userId: "user123" });
// Search memories
const results = await client.search("What are my dietary restrictions?", {
filters: { user_id: "user123" }
});
console.log(results);
```
## cURL
```bash
export MEM0_API_KEY="m0-your-api-key"
# Add memory
curl -X POST https://api.mem0.ai/v3/memories/add/ \
-H "Authorization: Token $MEM0_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "I am a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I will remember your dietary preferences."}
],
"user_id": "user123"
}'
# Search memories
curl -X POST https://api.mem0.ai/v3/memories/search/ \
-H "Authorization: Token $MEM0_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What are my dietary restrictions?",
"filters": {"user_id": "user123"}
}'
```
## Sample Response
```json
{
"results": [
{
"id": "14e1b28a-2014-40ad-ac42-69c9ef42193d",
"memory": "Allergic to nuts",
"user_id": "user123",
"categories": ["health"],
"created_at": "2025-10-22T04:40:22.864647-07:00",
"score": 0.30
}
]
}
```
## Next Steps
- [SDK Guide](sdk-guide.md) -- all methods for Python and TypeScript
- [API Reference](api-reference.md) -- REST endpoints and memory object structure
- [Integration Patterns](integration-patterns.md) -- LangChain, CrewAI, Vercel AI, etc.
@@ -1,353 +0,0 @@
# Mem0 SDK Guide
Complete SDK reference for Python and TypeScript. All methods use `MemoryClient` (Platform API).
> **For language-specific deep references (including OSS):** See [client/python.md](../client/python.md) and [client/node.md](../client/node.md). For Python vs TypeScript differences: [client/differences.md](../client/differences.md).
## Initialization
**Python:**
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-your-api-key")
```
**Python (Async):**
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="m0-your-api-key")
```
**TypeScript:**
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-your-api-key' });
```
Constructor accepts `apiKey` (required) and `host` (optional, default: `https://api.mem0.ai`).
---
## add() -- Store Memories
**Python:**
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
# With metadata
client.add(messages, user_id="alice", metadata={"source": "onboarding"})
```
**TypeScript:**
```typescript
await client.add(messages, { userId: "alice" });
await client.add(messages, { userId: "alice", metadata: { source: "onboarding" } });
```
### Parameters
| Name | Type | Description |
|------|------|-------------|
| `messages` | array | `[{"role": "user", "content": "..."}]` |
| `user_id` | string | User identifier (recommended) |
| `agent_id` | string | Agent identifier |
| `run_id` | string | Session identifier |
| `metadata` | object | Custom key-value pairs |
| `infer` | boolean | If `false`, store raw text without inference (default: `true`) |
### Advanced Add Options
```python
# Agent + session scoping
client.add(messages, user_id="alice", agent_id="nutrition-agent", run_id="session-456")
# Raw text -- skip LLM inference
client.add(
[{"role": "user", "content": "User prefers dark mode."}],
user_id="alice",
infer=False,
)
```
---
## search() -- Find Memories
**Python:**
```python
results = client.search("dietary preferences?", filters={"user_id": "alice"})
# With filters and reranking
results = client.search(
query="work experience",
filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "professional_details"}}]},
top_k=5,
rerank=True,
threshold=0.5
)
```
**TypeScript:**
```typescript
const results = await client.search("dietary preferences", { filters: { user_id: "alice" } });
const results = await client.search("work experience", {
filters: { AND: [{ user_id: "alice" }, { categories: { contains: "professional_details" } }] },
topK: 5,
rerank: true,
});
```
### Parameters
| Name | Type | Description |
|------|------|-------------|
| `query` | string | Natural language search query |
| `filters` | object | Filter object (AND/OR operators). Use `{"user_id": "..."}` to filter by user |
| `top_k` | number | Number of results (default: 10 for Platform) |
| `rerank` | boolean | Enable reranking for better relevance (default: `false`) |
| `threshold` | number | Minimum similarity score (default: 0.1) |
### Common Filter Patterns
**Python:**
```python
# Single user filter
filters={"user_id": "alice"}
# OR across agents
filters={"OR": [{"user_id": "alice"}, {"agent_id": {"in": ["travel-agent", "sports-agent"]}}]}
# Category filtering (partial match)
filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "finance"}}]}
# Category filtering (exact match)
filters={"AND": [{"user_id": "alice"}, {"categories": {"in": ["personal_information"]}}]}
# Wildcard (match any non-null run)
filters={"AND": [{"user_id": "alice"}, {"run_id": "*"}]}
# Date range
filters={"AND": [
{"user_id": "alice"},
{"created_at": {"gte": "2024-01-01T00:00:00Z"}},
{"created_at": {"lt": "2024-02-01T00:00:00Z"}}
]}
# Exclude categories with NOT
filters={"AND": [{"user_id": "user_123"}, {"NOT": {"categories": {"in": ["spam", "test"]}}}]}
# Multi-dimensional query
filters={"AND": [
{"user_id": "user_123"},
{"keywords": {"icontains": "invoice"}},
{"categories": {"in": ["finance"]}},
{"created_at": {"gte": "2024-01-01T00:00:00Z"}}
]}
```
**TypeScript:**
```typescript
// Single user filter
filters: { user_id: "alice" }
// OR across agents
filters: { OR: [{ user_id: "alice" }, { agent_id: { in: ["travel-agent", "sports-agent"] } }] }
// Category filtering (partial match)
filters: { AND: [{ user_id: "alice" }, { categories: { contains: "finance" } }] }
// Category filtering (exact match)
filters: { AND: [{ user_id: "alice" }, { categories: { in: ["personal_information"] } }] }
```
---
## get() / getAll() -- Retrieve Memories
**Python:**
```python
# Single memory by ID
memory = client.get(memory_id="ea925981-...")
# All memories for a user
memories = client.get_all(filters={"user_id": "alice"})
# With date range
memories = client.get_all(
filters={"AND": [
{"user_id": "alex"},
{"created_at": {"gte": "2024-07-01", "lte": "2024-07-31"}}
]}
)
```
**TypeScript:**
```typescript
const memory = await client.get("ea925981-...");
const memories = await client.getAll({ filters: { user_id: "alice" } });
```
**Note:** `get_all` requires at least one of `user_id`, `agent_id`, `app_id`, or `run_id` in filters.
---
## update() -- Modify Memories
**Python:**
```python
client.update(memory_id="ea925981-...", text="Updated: vegan since 2024")
client.update(memory_id="ea925981-...", text="Updated", metadata={"verified": True})
```
**TypeScript:**
```typescript
await client.update("ea925981-...", { text: "Updated: vegan since 2024" });
```
---
## delete() / deleteAll() -- Remove Memories
**Python:**
```python
client.delete(memory_id="ea925981-...")
client.delete_all(user_id="alice") # Irreversible bulk delete
```
**TypeScript:**
```typescript
await client.delete("ea925981-...");
await client.deleteAll({ userId: "alice" });
```
---
## history() -- Track Changes
**Python:**
```python
history = client.history(memory_id="ea925981-...")
# Returns: [{previous_value, new_value, action, timestamps}]
```
**TypeScript:**
```typescript
const history = await client.history("ea925981-...");
```
---
## Batch Operations (TypeScript)
```typescript
// Batch update
await client.batchUpdate([
{ memoryId: "uuid-1", text: "Updated text" },
{ memoryId: "uuid-2", text: "Another updated text" },
]);
// Batch delete
await client.batchDelete(["uuid-1", "uuid-2", "uuid-3"]);
```
---
## Additional Methods
```python
# List all users/agents/sessions with memories
users = client.users()
# Delete a user/agent entity
client.delete_users(user_id="alice")
# Submit feedback on a memory
client.feedback(memory_id="...", feedback="POSITIVE", feedback_reason="Accurate extraction")
# Export memories
export = client.create_memory_export(filters={"AND": [{"user_id": "alice"}]})
data = client.get_memory_export(memory_export_id=export["id"])
```
---
## Common Pitfalls
1. **Entity cross-filtering fails silently** -- `AND` with `user_id` + `agent_id` returns empty. Use `OR`.
2. **SQL operators rejected** -- use `gte`, `lt`, etc. Not `>=`, `<`.
3. **Metadata filtering is limited** -- only top-level keys with `eq`, `contains`, `ne`.
4. **Wildcard `*` excludes null** -- only matches non-null values.
5. **Default threshold is 0.1** -- increase for stricter matching.
6. **Async processing** -- memories process asynchronously. Wait 2-3s after `add()` before searching.
## Naming Conventions
Python uses `snake_case` everywhere (`user_id`, `memory_id`, `get_all`). TypeScript uses `camelCase` for methods (`getAll`, `deleteAll`, `batchUpdate`) and top-level parameters (`userId`, `topK`, `pageSize`), but filter keys use `snake_case` (`user_id`, `agent_id`).
---
## v2 to v3 Migration
### Breaking Changes in v3
**1. Entity IDs in search() and getAll()**
v3 requires entity IDs (`user_id`, `agent_id`, `run_id`) inside `filters` instead of as top-level parameters:
```python
# v2 (deprecated)
client.search("query", user_id="alice")
client.get_all(user_id="alice")
# v3
client.search("query", filters={"user_id": "alice"})
client.get_all(filters={"user_id": "alice"})
```
```typescript
// v2 (deprecated)
await client.search("query", { user_id: "alice" });
await client.getAll({ user_id: "alice" });
// v3
await client.search("query", { filters: { user_id: "alice" } });
await client.getAll({ filters: { user_id: "alice" } });
```
**2. TypeScript Parameter Naming**
v3 TypeScript uses camelCase for all parameters:
| v2 | v3 |
|----|-----|
| `user_id` | `userId` |
| `agent_id` | `agentId` |
| `run_id` | `runId` |
| `top_k` | `topK` |
| `page_size` | `pageSize` |
**3. Default Values Changed**
| Parameter | v2 Default | v3 Default |
|-----------|------------|------------|
| `threshold` | 0.3 | 0.1 |
| `rerank` | (not specified) | `false` |
**4. Removed Parameters**
The following parameters are no longer supported:
| Parameter | Status |
|-----------|--------|
| `enable_graph` | Removed from add/search/getAll |
| `keyword_search` | Removed from search |
| `filter_memories` | Removed |
| `immutable` | Removed from add |
| `expiration_date` | Removed from add |
| `includes` | Removed from add |
| `excludes` | Removed from add |
| `async_mode` | Removed from add |
@@ -1,720 +0,0 @@
# Mem0 Use Cases & Examples
Real-world implementation patterns for Mem0 Platform. Each use case includes complete, runnable code in both Python and TypeScript.
## Table of Contents
- [Personalized AI Companion](#1-personalized-ai-companion)
- [Customer Support with Categories](#2-customer-support-with-categories)
- [Healthcare Coach](#3-healthcare-coach)
- [Content Creation Workflow](#4-content-creation-workflow)
- [Multi-Agent / Multi-Tenant](#5-multi-agent--multi-tenant)
- [Personalized Search](#6-personalized-search)
- [Email Intelligence](#7-email-intelligence)
- [Common Patterns Across Use Cases](#common-patterns-across-use-cases)
---
## 1. Personalized AI Companion
A fitness coach that remembers goals, preferences, and progress across sessions. Mem0 persists context across app restarts — no session state needed.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def chat(user_input: str, user_id: str) -> str:
# 1. Retrieve relevant memories
memories = mem0.search(user_input, user_id=user_id)
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
# 2. Generate response with memory context
system_prompt = f"""You are Ray, a personal fitness coach.
Use these known facts about the user to personalize your response:
{context if context else 'No prior context yet.'}"""
response = openai_client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input},
]
)
reply = response.choices[0].message.content
# 3. Store interaction for future context
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": reply}],
user_id=user_id
)
return reply
# Usage
chat("I want to run a marathon in under 4 hours", user_id="max")
# Next day, app restarted:
chat("What should I focus on today?", user_id="max")
# Ray remembers the sub-4 marathon goal
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function chat(userInput: string, userId: string): Promise<string> {
// 1. Retrieve relevant memories
const memories = await mem0.search(userInput, { filters: { user_id: userId } });
const context = memories.results
?.map((m: any) => `- ${m.memory}`)
.join('\n') || 'No prior context yet.';
// 2. Generate response with memory context
const response = await openai.chat.completions.create({
model: 'gpt-5-mini',
messages: [
{ role: 'system', content: `You are Ray, a personal fitness coach.\nUser context:\n${context}` },
{ role: 'user', content: userInput },
],
});
const reply = response.choices[0].message.content!;
// 3. Store interaction
await mem0.add(
[{ role: 'user', content: userInput }, { role: 'assistant', content: reply }],
{ userId: userId }
);
return reply;
}
```
### Key Benefits
- Context persists across app restarts — no session management needed
- Memories are automatically deduplicated and updated
- Works with any LLM provider (OpenAI, Anthropic, etc.)
**Best for:** Fitness coaches, tutors, therapists — any assistant that needs to remember goals across sessions.
---
## 2. Customer Support with Categories
Auto-categorize support data so teams retrieve the right facts fast. Uses custom categories for structured retrieval.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
# 1. Define categories at the project level (one-time setup)
custom_categories = [
{"support_tickets": "Customer issues and resolutions"},
{"account_info": "Account details and preferences"},
{"billing": "Payment history and billing questions"},
{"product_feedback": "Feature requests and feedback"},
]
client.project.update(custom_categories=custom_categories)
# 2. Store interactions — auto-classified into categories
def log_support_interaction(user_id: str, message: str, priority: str = "normal"):
client.add(
[{"role": "user", "content": message}],
user_id=user_id,
metadata={"priority": priority, "source": "support_chat"}
)
# 3. Retrieve by category
def get_billing_issues(user_id: str):
return client.get_all(
filters={
"AND": [
{"user_id": user_id},
{"categories": {"in": ["billing"]}}
]
}
)
def search_support_history(user_id: str, query: str):
return client.search(
query,
filters={
"AND": [
{"user_id": user_id},
{"categories": {"contains": "support_tickets"}}
]
},
top_k=5
)
# Usage
log_support_interaction("maria", "I was charged twice for last month's subscription", priority="high")
log_support_interaction("maria", "The dashboard is loading slowly on mobile")
billing = get_billing_issues("maria") # Returns only billing-related memories
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
// Setup categories (one-time)
await client.updateProject({
custom_categories: [
{ support_tickets: 'Customer issues and resolutions' },
{ billing: 'Payment history and billing questions' },
{ product_feedback: 'Feature requests and feedback' },
],
});
async function logInteraction(userId: string, message: string, priority = 'normal') {
await client.add(
[{ role: 'user', content: message }],
{ userId: userId, metadata: { priority, source: 'support_chat' } }
);
}
async function getBillingIssues(userId: string) {
return client.getAll({
filters: { AND: [{ user_id: userId }, { categories: { in: ['billing'] } }] },
});
}
```
### Key Benefits
- Automatic categorization — no manual tagging
- Filter by category for structured retrieval
- Metadata (`priority`, `source`) enables multi-dimensional queries
**Best for:** Help desks, SaaS support, e-commerce — structured retrieval by category eliminates manual scanning.
---
## 3. Healthcare Coach
Guide patients with an assistant that remembers medical history. Uses high `threshold` for confident retrieval in safety-critical contexts.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def save_patient_info(user_id: str, information: str):
mem0.add(
[{"role": "user", "content": information}],
user_id=user_id,
run_id="healthcare_session",
metadata={"type": "patient_information"}
)
def consult(user_id: str, question: str) -> str:
# High threshold for medical accuracy
memories = mem0.search(question, user_id=user_id, top_k=5, threshold=0.7)
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": f"You are a health coach. Patient context:\n{context}"},
{"role": "user", "content": question},
]
)
reply = response.choices[0].message.content
# Store the interaction
mem0.add(
[{"role": "user", "content": question}, {"role": "assistant", "content": reply}],
user_id=user_id,
run_id="healthcare_session",
)
return reply
# Usage
save_patient_info("alex", "I'm allergic to penicillin and take metformin for type 2 diabetes")
consult("alex", "Can I take amoxicillin for my sore throat?")
# Remembers penicillin allergy — amoxicillin is a penicillin-type antibiotic
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function savePatientInfo(userId: string, info: string) {
await mem0.add(
[{ role: 'user', content: info }],
{ userId: userId, runId: 'healthcare_session', metadata: { type: 'patient_information' } }
);
}
async function consult(userId: string, question: string): Promise<string> {
const memories = await mem0.search(question, {
filters: { user_id: userId },
topK: 5,
threshold: 0.7,
});
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-5-mini',
messages: [
{ role: 'system', content: `You are a health coach. Patient context:\n${context}` },
{ role: 'user', content: question },
],
});
const reply = response.choices[0].message.content!;
await mem0.add(
[{ role: 'user', content: question }, { role: 'assistant', content: reply }],
{ userId: userId, runId: 'healthcare_session' }
);
return reply;
}
```
### Key Benefits
- High threshold (0.7) ensures only confident matches for safety-critical retrieval
- Session scoping via `run_id` groups related health interactions
- Metadata tagging separates patient info from conversation history
**Best for:** Telehealth, wellness apps, patient management — persistent health context across visits.
---
## 4. Content Creation Workflow
Store voice guidelines once and apply them across every draft. Uses `run_id` and `metadata` to scope writing preferences per session.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def store_writing_preferences(user_id: str, preferences: str):
mem0.add(
[{"role": "user", "content": preferences}],
user_id=user_id,
run_id="editing_session",
metadata={"type": "preferences", "category": "writing_style"}
)
def draft_content(user_id: str, topic: str) -> str:
# Retrieve writing preferences
prefs = mem0.search(
"writing style preferences",
filters={"AND": [{"user_id": user_id}, {"run_id": "editing_session"}]}
)
style_context = "\n".join([f"- {m['memory']}" for m in prefs.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": f"Write content matching these style preferences:\n{style_context}"},
{"role": "user", "content": f"Write a blog post about: {topic}"},
]
)
return response.choices[0].message.content
# Usage
store_writing_preferences("writer_01", "I prefer short sentences. Active voice. No jargon. Use analogies.")
draft_content("writer_01", "Why AI memory matters for chatbots")
# Drafts content matching the stored voice guidelines
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function storePreferences(userId: string, preferences: string) {
await mem0.add(
[{ role: 'user', content: preferences }],
{ userId: userId, runId: 'editing_session', metadata: { type: 'preferences' } }
);
}
async function draftContent(userId: string, topic: string): Promise<string> {
const prefs = await mem0.search('writing style preferences', {
filters: { AND: [{ user_id: userId }, { run_id: 'editing_session' }] },
});
const styleContext = prefs.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-5-mini',
messages: [
{ role: 'system', content: `Write content matching these preferences:\n${styleContext}` },
{ role: 'user', content: `Write a blog post about: ${topic}` },
],
});
return response.choices[0].message.content!;
}
```
### Key Benefits
- Voice consistency across all content without repeating guidelines
- Scoped sessions let you maintain different style profiles
- Preferences update automatically as you refine them
**Best for:** Marketing teams, technical writers, agencies — consistent voice across all content.
---
## 5. Multi-Agent / Multi-Tenant
Keep memories separate using `user_id`, `agent_id`, `app_id`, and `run_id` scoping. Critical for multi-agent workflows and multi-tenant apps.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
# Store memories scoped to user + agent + session
def store_scoped_memory(messages: list, user_id: str, agent_id: str, run_id: str, app_id: str):
client.add(
messages,
user_id=user_id,
agent_id=agent_id,
run_id=run_id,
app_id=app_id
)
# Query within a specific scope
def search_user_session(query: str, user_id: str, app_id: str, run_id: str):
"""Search memories for a specific user within a specific session."""
return client.search(
query,
filters={
"AND": [
{"user_id": user_id},
{"app_id": app_id},
{"run_id": run_id}
]
}
)
def search_agent_knowledge(query: str, agent_id: str, app_id: str):
"""Search all memories an agent has across all users."""
return client.search(
query,
filters={
"AND": [
{"agent_id": agent_id},
{"app_id": app_id}
]
}
)
# Usage: Travel concierge app with multiple agents
store_scoped_memory(
[{"role": "user", "content": "I'm vegetarian and prefer window seats"}],
user_id="traveler_cam",
agent_id="travel_planner",
run_id="tokyo-2025",
app_id="concierge_app"
)
# User-scoped query: "What does Cam prefer?"
user_mems = search_user_session("dietary restrictions?", "traveler_cam", "concierge_app", "tokyo-2025")
# Agent-scoped query: "What do all travelers prefer?" (across users)
agent_mems = search_agent_knowledge("common dietary restrictions?", "travel_planner", "concierge_app")
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
async function storeScopedMemory(
messages: Array<{ role: string; content: string }>,
userId: string, agentId: string, runId: string, appId: string
) {
await client.add(messages, {
userId: userId,
agentId: agentId,
runId: runId,
appId: appId,
});
}
async function searchUserSession(query: string, userId: string, appId: string, runId: string) {
return client.search(query, {
filters: { AND: [{ user_id: userId }, { app_id: appId }, { run_id: runId }] },
});
}
async function searchAgentKnowledge(query: string, agentId: string, appId: string) {
return client.search(query, {
filters: { AND: [{ agent_id: agentId }, { app_id: appId }] },
});
}
```
### Key Benefits
- Full isolation between users, agents, sessions, and apps
- Query at any scope level — user, agent, session, or app-wide
- No memory leakage between tenants
**Best for:** Multi-agent workflows, multi-tenant SaaS — proper isolation at every level.
---
## 6. Personalized Search
Blend real-time search results with personal context. Uses `custom_instructions` to infer preferences from queries.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
# One-time setup: configure Mem0 to infer from queries
mem0.project.update(
custom_instructions="""Infer user preferences and facts from their search queries.
Extract dietary preferences, location, interests, and purchase history."""
)
def personalized_search(user_id: str, query: str, search_results: list) -> str:
# Get user context from memory
memories = mem0.search(query, user_id=user_id, top_k=5)
user_context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": f"Personalize search results using user context:\n{user_context}"},
{"role": "user", "content": f"Query: {query}\n\nSearch results:\n{search_results}"},
]
)
reply = response.choices[0].message.content
# Store the query to learn preferences over time
mem0.add(
[{"role": "user", "content": query}],
user_id=user_id
)
return reply
# Usage
personalized_search("user_42", "best restaurants nearby", ["Restaurant A", "Restaurant B"])
# Over time, Mem0 learns: "user prefers vegetarian, lives in Austin"
# Future searches are automatically personalized
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function personalizedSearch(userId: string, query: string, searchResults: string[]): Promise<string> {
const memories = await mem0.search(query, { filters: { user_id: userId }, topK: 5 });
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-5-mini',
messages: [
{ role: 'system', content: `Personalize results using user context:\n${context}` },
{ role: 'user', content: `Query: ${query}\nResults: ${searchResults.join(', ')}` },
],
});
const reply = response.choices[0].message.content!;
await mem0.add([{ role: 'user', content: query }], { userId: userId });
return reply;
}
```
### Key Benefits
- Learns preferences from queries automatically via `custom_instructions`
- Personalizes any search provider (Tavily, Google, Bing)
- Zero manual preference setup — improves over time
**Best for:** Personalized search engines, recommendation systems — search results tailored to individual users.
---
## 7. Email Intelligence
Capture, categorize, and recall inbox threads using persistent memories with rich metadata.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
def store_email(user_id: str, sender: str, subject: str, body: str, date: str):
client.add(
[{"role": "user", "content": f"Email from {sender}: {subject}\n\n{body}"}],
user_id=user_id,
metadata={"email_type": "incoming", "sender": sender, "subject": subject, "date": date}
)
def search_emails(user_id: str, query: str):
return client.search(
query,
filters={"AND": [{"user_id": user_id}, {"categories": {"contains": "email"}}]},
top_k=10
)
def get_emails_from_sender(user_id: str, sender: str):
return client.get_all(
filters={
"AND": [
{"user_id": user_id},
{"metadata": {"contains": sender}}
]
}
)
# Usage
store_email("alice", "bob@acme.com", "Q3 Budget Review", "Attached is the Q3 budget...", "2025-01-15")
store_email("alice", "carol@acme.com", "Sprint Planning", "Here are the priorities...", "2025-01-16")
results = search_emails("alice", "budget discussions")
sender_emails = get_emails_from_sender("alice", "bob@acme.com")
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
async function storeEmail(userId: string, sender: string, subject: string, body: string, date: string) {
await client.add(
[{ role: 'user', content: `Email from ${sender}: ${subject}\n\n${body}` }],
{ userId: userId, metadata: { email_type: 'incoming', sender, subject, date } }
);
}
async function searchEmails(userId: string, query: string) {
return client.search(query, {
filters: { AND: [{ user_id: userId }, { categories: { contains: 'email' } }] },
topK: 10,
});
}
```
### Key Benefits
- Rich metadata enables multi-dimensional queries (sender, date, subject)
- Category filtering separates emails from other memory types
- Semantic search across all email content
**Best for:** Inbox management, email automation — searchable email memories with metadata filtering.
---
## Common Patterns Across Use Cases
### Pattern 1: Retrieve → Generate → Store
Every use case follows the same 3-step loop:
```python
# 1. Retrieve relevant context
memories = mem0.search(user_input, user_id=user_id)
context = "\n".join([m["memory"] for m in memories.get("results", [])])
# 2. Generate with context
response = llm.generate(system_prompt=f"Context:\n{context}", user_input=user_input)
# 3. Store the interaction
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": response}],
user_id=user_id
)
```
### Pattern 2: Scope with Entity Identifiers
Use `user_id`, `agent_id`, `app_id`, and `run_id` to isolate memories:
```python
# User-level: personal preferences
client.add(messages, user_id="alice")
# Session-level: conversation within one session
client.add(messages, user_id="alice", run_id="session_123")
# Agent-level: agent-specific knowledge
client.add(messages, agent_id="support_bot", app_id="helpdesk")
```
### Pattern 3: Rich Metadata for Filtering
Attach structured metadata for multi-dimensional queries:
```python
# Store with metadata
client.add(messages, user_id="alice", metadata={"priority": "high", "source": "phone_call"})
# Filter by category + metadata
client.search("billing issues", filters={
"AND": [{"user_id": "alice"}, {"categories": {"contains": "billing"}}]
})
```
### Pattern 4: Custom Instructions for Domain-Specific Extraction
Control what Mem0 extracts from conversations:
```python
client.project.update(
custom_instructions="Extract medical conditions, medications, and allergies. Exclude billing info."
)
```
---
## More Examples
For 30+ cookbooks with complete working code: [docs.mem0.ai/cookbooks](https://docs.mem0.ai/cookbooks)
@@ -1,181 +0,0 @@
#!/usr/bin/env bun
export {};
const DOCS_BASE = "https://docs.mem0.ai";
const SEARCH_ENDPOINT = `${DOCS_BASE}/api/search`;
const LLMS_INDEX = `${DOCS_BASE}/llms.txt`;
const SECTION_MAP: Record<string, string[]> = {
platform: [
"/platform/overview",
"/platform/quickstart",
"/platform/features",
"/platform/features/graph-memory",
"/platform/features/selective-memory",
"/platform/features/custom-categories",
"/platform/features/v2-memory-filters",
"/platform/features/async-client",
"/platform/features/webhooks",
"/platform/features/multimodal-support",
],
api: [
"/api-reference/memory/add-memories",
"/api-reference/memory/v2-search-memories",
"/api-reference/memory/v2-get-memories",
"/api-reference/memory/get-memory",
"/api-reference/memory/update-memory",
"/api-reference/memory/delete-memory",
],
"open-source": [
"/open-source/overview",
"/open-source/python-quickstart",
"/open-source/node-quickstart",
"/open-source/features",
"/open-source/features/graph-memory",
"/open-source/features/rest-api",
"/open-source/configure-components",
],
openmemory: [
"/openmemory/overview",
"/openmemory/quickstart",
],
sdks: ["/sdks/python", "/sdks/js"],
integrations: ["/integrations"],
};
async function fetchUrl(url: string): Promise<string> {
try {
const res = await fetch(url, {
headers: { "User-Agent": "Mem0DocSearchAgent/1.0" },
signal: AbortSignal.timeout(15000),
});
if (!res.ok) return `HTTP Error ${res.status}: ${res.statusText}`;
return await res.text();
} catch (e: any) {
return `Error: ${e.message}`;
}
}
async function searchDocs(query: string, section?: string) {
const params = new URLSearchParams({ query });
try {
const result = await fetchUrl(`${SEARCH_ENDPOINT}?${params}`);
const data = JSON.parse(result);
if (data?.results) {
let results = data.results;
if (section && SECTION_MAP[section]) {
const paths = SECTION_MAP[section];
results = results.filter((r: any) =>
paths.some((p) => r.url?.startsWith(p)),
);
}
return { source: "mintlify_search", results };
}
} catch {}
const indexContent = await fetchUrl(LLMS_INDEX);
const queryLower = query.toLowerCase();
let matching = indexContent
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#") && l.toLowerCase().includes(queryLower));
if (section && SECTION_MAP[section]) {
const paths = SECTION_MAP[section];
matching = matching.filter((u) => paths.some((p) => u.includes(p)));
}
return {
source: "llms_txt_index",
query,
matching_urls: matching.slice(0, 20),
suggestion: "Fetch specific URLs for detailed content",
};
}
async function fetchPage(pagePath: string) {
const url = pagePath.startsWith("/") ? `${DOCS_BASE}${pagePath}` : pagePath;
const content = await fetchUrl(url);
return { url, content: content.slice(0, 10000), truncated: content.length > 10000 };
}
async function getIndex() {
const content = await fetchUrl(LLMS_INDEX);
const urls = content
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#"));
return { total_pages: urls.length, urls, sections: Object.keys(SECTION_MAP) };
}
function listSection(section: string) {
if (!SECTION_MAP[section]) {
return { error: `Unknown section: ${section}`, available: Object.keys(SECTION_MAP) };
}
return { section, pages: SECTION_MAP[section].map((p) => `${DOCS_BASE}${p}`) };
}
function printResult(result: any) {
if (result.results) {
console.log(`Source: ${result.source ?? "unknown"}`);
for (const r of result.results) {
console.log(` - ${r.title ?? "N/A"}: ${r.url ?? "N/A"}`);
if (r.description) console.log(` ${r.description.slice(0, 200)}`);
}
} else if (result.matching_urls) {
console.log(`Source: ${result.source}`);
console.log(`Query: ${result.query}`);
for (const url of result.matching_urls) console.log(` - ${url}`);
if (result.suggestion) console.log(`\n${result.suggestion}`);
} else if (result.urls) {
console.log(`Total documentation pages: ${result.total_pages}`);
console.log(`Sections: ${result.sections.join(", ")}`);
for (const url of result.urls.slice(0, 30)) console.log(` - ${url}`);
if (result.total_pages > 30) console.log(` ... and ${result.total_pages - 30} more`);
} else if (result.pages) {
console.log(`Section: ${result.section}`);
for (const page of result.pages) console.log(` - ${page}`);
} else if (result.content) {
console.log(`URL: ${result.url}`);
if (result.truncated) console.log("[Content truncated to 10000 chars]");
console.log(result.content);
} else if (result.error) {
console.log(`Error: ${result.error}`);
if (result.available) console.log(`Available sections: ${result.available.join(", ")}`);
} else {
console.log(JSON.stringify(result, null, 2));
}
}
const args = process.argv.slice(2);
const flags: Record<string, string | boolean> = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === "--query" && args[i + 1]) flags.query = args[++i];
else if (args[i] === "--page" && args[i + 1]) flags.page = args[++i];
else if (args[i] === "--section" && args[i + 1]) flags.section = args[++i];
else if (args[i] === "--index") flags.index = true;
else if (args[i] === "--json") flags.json = true;
}
let result: any;
if (flags.index) {
result = await getIndex();
} else if (flags.section && !flags.query) {
result = listSection(flags.section as string);
} else if (flags.page) {
result = await fetchPage(flags.page as string);
} else if (flags.query) {
result = await searchDocs(flags.query as string, flags.section as string | undefined);
} else {
console.log("Usage:");
console.log(" bun scripts/mem0_doc_search.ts --query \"topic\"");
console.log(" bun scripts/mem0_doc_search.ts --page \"/platform/features/graph-memory\"");
console.log(" bun scripts/mem0_doc_search.ts --index");
console.log(" bun scripts/mem0_doc_search.ts --section platform");
process.exit(1);
}
if (flags.json) {
console.log(JSON.stringify(result, null, 2));
} else {
printResult(result);
}
@@ -1,63 +0,0 @@
---
name: memory-reviewer
description: Reviews stored memory quality by detecting duplicates, contradictions, and stale entries with actionable recommendations. Use when search results seem conflicting, before running dream consolidation, or for periodic memory hygiene audits.
---
# Memory Reviewer
Audits memory quality for the active project. Finds duplicates, contradictions, and low-confidence entries.
## When to use
- User asks "check my memories", "memory quality", "any duplicates?"
- User runs `/mem0:memory-reviewer` directly
- After a session with 5+ memory writes (suggest proactively)
- After `/mem0:health --deep` identifies issues
## Steps
1. **Fetch all memories** for active project via `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `page_size=200`. Paginate if needed — cap at 200 memories.
2. **Group by `metadata.type`**. Common types: `decision`, `convention`, `anti_pattern`, `task_learning`, `project_profile`, `user_preference`, `session_state`.
3. **Scan each group for issues:**
| Issue | Detection method |
|---|---|
| **Near-duplicates** | >60% noun overlap within same type. Compare memory text after stripping stop words. |
| **Contradictions** | Opposing facts about same topic (e.g., "use PostgreSQL" vs "use MySQL" for same component) |
| **Low-confidence** | `metadata.confidence < 0.3` |
| **Missing type** | No `metadata.type` set |
| **Stale** | `created_at` older than 180 days with no updates |
4. **Output compact summary:**
```
memory-reviewer: project=<id> total=<N>
duplicates: <N> found
contradictions: <N> found
low_confidence: <N> found
untagged: <N> found
stale: <N> found
```
5. **If issues found**, list them with memory IDs:
```
Issues:
[duplicate] "<memory_a>" ≈ "<memory_b>" [mem0:<id_a>, mem0:<id_b>]
[contradiction] "<memory_x>" vs "<memory_y>" [mem0:<id_x>, mem0:<id_y>]
[low_conf] "<memory_z>" (confidence: 0.1) [mem0:<id_z>]
```
6. **Suggest action**: "Run `/mem0:dream` to consolidate duplicates and resolve contradictions."
## Constraints
- **Read-only** — never modify or delete memories (that's `/mem0:dream`'s job)
- **Max 200 memories** per scan
- Report findings, let user decide on action
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -1,181 +0,0 @@
---
name: onboard
description: Sets up mem0 for a new project including API key configuration, MCP authentication, project file import, and coding categories. Use on first run in a new project, when API key needs updating, or to re-run initial setup after configuration changes.
---
# Mem0 Onboarding Wizard
Run this wizard to set up the mem0 plugin for the current project. Complete in ~60 seconds.
**IMPORTANT: Execute steps strictly in order (0 → 1 → 2 → 3 → 4 → 5 → 6). Each step depends on the previous one. Do NOT run steps in parallel or skip ahead. Complete one step fully before starting the next.**
## Step 0: Skip dependency install
The plugin lists `mem0ai` as a declared dependency — no install step is needed. Proceed directly to Step 1.
## Step 1: Set up API key
Check if the API key is available:
```bash
[ -n "${MEM0_API_KEY:-}" ] && echo "SET" || echo "NOT_SET"
```
IMPORTANT: Never run `echo $MEM0_API_KEY` — that prints the secret in plaintext to the conversation log.
### If API key IS set (output is "SET")
Print: `- API key found.` and proceed to Step 2.
### If API key is NOT set (output is "NOT_SET")
Guide the user through API key setup. Show this message:
```
Step 1: Setting up API key.
- API key not found. Let's set it up.
1. Get your API key from https://app.mem0.ai/dashboard/api-keys
2. Choose ONE method:
Option A — Shell profile:
echo 'export MEM0_API_KEY="m0-your-key-here"' >> ~/.zshrc
source ~/.zshrc
Option B — OpenCode environment config:
Add MEM0_API_KEY to your OpenCode environment settings
so it is available in all sessions.
3. Verify:
[ -n "${MEM0_API_KEY:-}" ] && echo "SET" || echo "NOT_SET"
```
After the user confirms, re-run the verify command. If NOT_SET, repeat. If SET, proceed to Step 2.
## Step 2: MCP server connection
First, check if MCP tools are already available using ToolSearch with query `"mem0 search_memories"`. The exact tool name varies by install method (may be `mcp__mem0__search_memories` or `mcp__plugin_mem0_mem0__search_memories`).
**If MCP tools ARE found:** Print `- MCP already connected.` and proceed to Step 3.
**If MCP tools are NOT found:**
The MCP server authenticates using the `MEM0_API_KEY` set in Step 1. No OAuth or browser login is needed.
1. Verify the API key is set (re-run the Step 1 check)
2. Check the plugin is installed and the MCP server for mem0 is listed in OpenCode's MCP configuration
3. If the server shows an error, ask the user to restart OpenCode and run `/mem0:onboard` again
4. If all checks pass but tools are still missing: "Restart OpenCode and run `/mem0:onboard` again."
**STOP here** — do not proceed without MCP tools.
## Step 3: Verify connectivity and show identity
Call `search_memories` with `query="project setup"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `top_k=1` to verify connectivity.
Print:
```
- Connected
user: <user_id>
project: <project_id>
branch: <branch>
```
If the search fails, troubleshoot the API key and MCP connection.
## Step 4: Import project files
Check for common project context files in the working directory root. Read any that exist and store their key facts as memories using `add_memory`.
### 4a: Detect project files
```bash
for f in CLAUDE.md AGENTS.md .cursorrules .windsurfrules mem0.md .mem0.md; do
[ -f "$f" ] && echo "FOUND: $f ($(wc -c < "$f") bytes)"
done || true
```
If no files found, print `- No project files found. Skipping import.` and proceed to Step 5.
### 4b: Read and import via MCP
For each file found in 4a:
1. Read the file contents with the Read tool.
2. Extract the key facts, conventions, and decisions documented in the file. Do not import the entire file verbatim — summarize meaningful chunks.
3. Call `add_memory` for each meaningful chunk with:
- `data`: the extracted fact or convention
- `user_id`: the active user id
- `app_id`: the active project id
- `metadata`: `{"source": "<filename>", "type": "project_context"}`
If a file is very large (over 4000 bytes), split it into logical sections (one `add_memory` call per section).
### 4c: Report to user
After processing all files, print a user-friendly summary:
```
- Importing project files into mem0... done.
<N> file(s) read, <M> memories stored.
These are available as context for future sessions.
```
If `add_memory` calls fail, print:
```
- Project file import failed. Check API key and MCP connection, then retry with: /mem0:onboard
```
## Step 5: Verify coding categories
Coding categories are now configured automatically in the background when the plugin starts. This step only verifies they are set up.
Check if the categories are already configured by searching for a project_profile memory:
Call `search_memories` with `query="coding categories project profile"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `top_k=1`.
If a project_profile memory is found, print:
```
- Coding categories already configured (17 categories, auto-installed).
```
If no project_profile memory is found, store one as a fallback. Call `add_memory` once with:
- `data`: a plain-text description of the project profile and the list of active coding categories:
```
Project profile for <project_id>.
Active coding categories: architecture_decisions, api_design, data_models,
algorithms, dependencies, environment_setup, testing_strategy, debugging_notes,
performance, security, deployment, code_conventions, error_handling,
refactoring_history, integrations, onboarding, project_meta.
```
- `user_id`: the active user id
- `app_id`: the active project id
- `metadata`: `{"type": "project_profile", "source": "onboard"}`
After the `add_memory` call succeeds, print:
```
- Coding categories installed (17 categories).
```
## Step 6: Summary
Print a summary:
```
- Onboarding complete.
user_id: <user_id>
project_id: <project_id> (app_id)
files: <N> found, <M> memories stored
categories: <N installed | already installed | skipped by user>
Memory is now active for this project. Start working — mem0 will
automatically search relevant context and capture learnings.
Run /mem0:tour to see what mem0 already knows about this project.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -29,12 +29,12 @@ Call `get_memory` with the selected memory ID. Store:
### Step 3: Pin it
The MCP `update_memory` tool only accepts `memory_id`, `text`, and `source` — it
does not accept a `metadata` parameter. To pin, append a pin marker to the text:
The `update_memory` tool updates a memory by `id`. To pin durably, append a pin
marker to the text so it travels with the memory:
```python
pinned_text = "[PINNED] " + original_text if not original_text.startswith("[PINNED]") else original_text
update_memory(memory_id=<selected_id>, text=pinned_text)
update_memory(id=<selected_id>, text=pinned_text)
```
**For new memories** (user wants to pin text that isn't stored yet):
@@ -1,135 +0,0 @@
---
name: stats
description: Displays memory usage statistics for the current session and project including counts by category, age distribution, and API latency. Use when checking how many memories exist, reviewing session activity, or auditing memory distribution across categories.
---
# Mem0 Stats
Show session and lifetime memory statistics.
## Execution
### Step 1: Gather session context
Read the session identity from env vars set by the plugin's shell.env hook:
- `MEM0_USER_ID` (falls back to `$USER` if unset)
- `MEM0_APP_ID` — the active project identifier
- `MEM0_SESSION_ID` — current session identifier
- `MEM0_BRANCH` — current git branch
If `MEM0_USER_ID` is unset, use `$USER`. If `MEM0_APP_ID` is unset, note "No project configured" and stop.
### Step 2: Fetch total memory count
Call `get_memories` MCP tool to get the total count for this project:
`filters={"AND": [{"user_id": "<MEM0_USER_ID>"}, {"app_id": "<MEM0_APP_ID>"}]}`, `page_size=1`
Read the `count` field from the response — this is the total number of memories for the project regardless of page size.
### Step 3: Fetch memories for category breakdown
Call `get_memories` MCP tool to retrieve memories for grouping:
`filters={"AND": [{"user_id": "<MEM0_USER_ID>"}, {"app_id": "<MEM0_APP_ID>"}]}`, `page_size=200`
Group each memory by:
1. `categories[0]` (platform-assigned) — primary grouping
2. `metadata.type` (agent-assigned) — secondary if `categories` is empty or absent
3. `created_at` date — for age analysis
**Category normalization:** Merge `auto_capture` and `uncategorized` into a single `uncategorized` row. Do NOT show `auto_capture` as its own row.
Also run a `search_memories` MCP tool call with `query="project"`, `filters={"AND": [{"user_id": "<MEM0_USER_ID>"}, {"app_id": "<MEM0_APP_ID>"}]}`, `top_k=1` to measure round-trip latency. Note the time before and after the MCP call — do NOT attempt raw HTTP calls to the API.
### Step 4: Display
Print a minimal plain-text dashboard. No markdown formatting — OpenCode TUI renders text verbatim.
Example output shape:
```
mem0 stats
Session (<first 12 chars of MEM0_SESSION_ID>) branch: main
Project: my-project — 55 memories — API: 84ms
Category Count
-------------------- -----
decision 24
convention 15
anti_pattern 6
task_learning 5
user_preference 3
session_state 2
Age — oldest: 2026-02-15 newest: 2026-05-23
< 7 days: 5 | 7-30d: 12 | 30-90d: 10 | > 90d: 8
Identity — user: kartik project: my-project branch: main
```
**Display rules:**
- Use plain text with spaces to align columns — no markdown tables, no | pipes, no ** bold, no ## headers
- Category section: sort by count descending, omit categories with 0 memories
- Age: single line with pipe-separated buckets, computed from `created_at`
- Session line: show MEM0_SESSION_ID (first 12 chars) and MEM0_BRANCH if available; skip the line entirely if both are unset
- If only 1-2 total memories, skip the category table — just show the count
- Keep everything compact — no decorative borders or filler
## Weekly digest mode
When invoked with `--weekly` (e.g., `/mem0:stats --weekly`), append a weekly
activity digest after the standard stats dashboard.
### W1: Fetch recent memories
Call `search_memories` in parallel with time-scoped queries:
1. `query="decisions made this week"`, `filters={"AND": [{"user_id": "<MEM0_USER_ID>"}, {"app_id": "<MEM0_APP_ID>"}, {"created_at": {"gte": "<7 days ago YYYY-MM-DD>"}}]}`, `top_k=20`
2. `query="bugs errors fixes"`, same time filter, `top_k=20`
3. `query="patterns conventions learnings"`, same time filter, `top_k=20`
### W2: Analyze
Merge by ID. Group into "New this week" by `categories[0]` or `metadata.type`.
Calculate: memories added last 7 days, most active categories, most active day.
### W3: Display
Append after the standard stats in plain text (no markdown):
```
This week (May 16 - May 23)
+12 memories — most active: Wednesday (5)
Category New
-------------- ---
decision 5
task_learning 4
bug_fix 3
Highlights
- <2-3 sentence summary of most important decisions/learnings this week>
```
### W4: Write digest file
Write to `~/.mem0/weekly-digest.txt` (overwrite). Append one line to
`~/.mem0/digest-history.log`:
```
<YYYY-MM-DD> | <MEM0_APP_ID> | +<new_count> memories | top: <top_category>
```
### W5: Empty state
If no new memories in 7 days, output:
```
No new memories in the past week. Total: <N> memories in <MEM0_APP_ID>.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -1,107 +0,0 @@
---
name: switch-project
description: Overrides the auto-detected project scope to read and write memories under a different project ID, or enables global search to access all memories across all users and projects. Use when working across multiple projects, accessing memories from another repo, enabling team-wide memory access, or when auto-detection resolves to the wrong project.
---
# Mem0 Switch Project
Override the automatic project_id detection for the current directory, or enable global search mode.
## Usage
- `/mem0:switch-project <project-name>` — switch to a specific project scope
- `/mem0:switch-project --global` — enable global search (all memories, all users, all projects)
- `/mem0:switch-project --no-global` — disable global search and return to per-project scoping
## Execution
### If `--global` flag is provided:
1. Set `global_search: true` in `~/.mem0/settings.json` using the Bash tool:
```bash
python3 -c "
import json, os
settings_file = os.path.expanduser('~/.mem0/settings.json')
settings = {}
if os.path.isfile(settings_file):
with open(settings_file) as f:
settings = json.load(f)
settings['global_search'] = True
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
print('Global search enabled')
"
```
2. Print:
```
Global search enabled.
Searches now return all memories across all users and projects.
Writes still use the current user_id and app_id.
Restart the session for the change to take effect.
```
### If `--no-global` flag is provided:
1. Set `global_search: false` in `~/.mem0/settings.json` using the Bash tool:
```bash
python3 -c "
import json, os
settings_file = os.path.expanduser('~/.mem0/settings.json')
settings = {}
if os.path.isfile(settings_file):
with open(settings_file) as f:
settings = json.load(f)
settings['global_search'] = False
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
print('Global search disabled')
"
```
2. Print:
```
Global search disabled.
Searches now return only memories scoped to the current project.
Restart the session for the change to take effect.
```
### If a project name is provided (no flags):
1. If no project name was given, ask: "What project_id should this directory use?"
2. Write the mapping to `~/.mem0/project_map.json` using the Bash tool:
```bash
python3 -c "
import json, os
map_file = os.path.expanduser('~/.mem0/project_map.json')
mapping = {}
if os.path.isfile(map_file):
with open(map_file) as f:
mapping = json.load(f)
mapping[os.getcwd()] = '<PROJECT_NAME>'
os.makedirs(os.path.dirname(map_file), exist_ok=True)
with open(map_file, 'w') as f:
json.dump(mapping, f, indent=2)
print(f'Mapped {os.getcwd()} -> <PROJECT_NAME>')
"
```
(Replace `<PROJECT_NAME>` with the user's chosen project name.)
3. Verify by searching for existing memories:
- Call `search_memories` with `query="project"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<PROJECT_NAME>"}]}`, `top_k=1`
4. Print:
```
Switched to project <PROJECT_NAME>.
<N> memories found for this project.
Note: This override persists across sessions for this directory.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -148,7 +148,7 @@ Identity - user: <user_id> project: <project_id> branch: <branch>
If zero memories found for this project, print:
```
No memories stored yet for project <project_id>.
Run /mem0-onboard to import project files, or start working - mem0 captures learnings automatically.
Start working - mem0 captures learnings automatically, or use /mem0:remember to save something now.
```
## Output formatting
@@ -1,14 +0,0 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@mem0/opencode-plugin"],
"mcp": {
"mem0": {
"type": "remote",
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token {env:MEM0_API_KEY}"
},
"oauth": false
}
}
}
@@ -1,6 +1,6 @@
{
"name": "@mem0/opencode-plugin",
"version": "0.1.3",
"version": "0.2.0",
"type": "module",
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
"main": "dist/index.js",
@@ -11,9 +11,6 @@
"import": "./dist/index.js"
}
},
"bin": {
"mem0-opencode": "./cli.ts"
},
"publishConfig": {
"access": "public"
},
@@ -23,7 +20,6 @@
"opencode-plugin",
"mem0",
"memory",
"mcp",
"ai-memory",
"persistent-memory"
],
@@ -34,8 +30,6 @@
},
"files": [
"dist",
"cli.ts",
"opencode.json",
"LICENSE",
"opencode-skills"
],
@@ -49,6 +43,7 @@
"opencode": {
"type": "plugin",
"hooks": [
"config",
"chat.message",
"tool.execute.before",
"tool.execute.after",
@@ -47,5 +47,32 @@ describe("opencode telemetry", () => {
process.env.MEM0_TELEMETRY = "false";
expect(() => captureEvent("session_start", {}, KEY)).not.toThrow();
expect(() => captureEvent("session_start", {}, undefined)).not.toThrow();
expect(() => captureEvent("session_start", {}, KEY, "proj")).not.toThrow();
});
test("every event carries os_version (matches telemetry.py schema)", () => {
const props = buildEvent("session_start", {}, KEY)!
.properties as Record<string, unknown>;
expect(typeof props.os_version).toBe("string");
});
test("project_hash is sha256(projectId) when a project id is supplied", async () => {
const { createHash } = await import("node:crypto");
const expected = createHash("sha256").update("acme-repo").digest("hex");
const props = buildEvent("session_start", {}, KEY, "acme-repo")!
.properties as Record<string, unknown>;
expect(props.project_hash).toBe(expected);
});
test("project_hash is omitted when no project id is supplied (no raw ids leak)", () => {
const props = buildEvent("session_start", {}, KEY)!
.properties as Record<string, unknown>;
expect("project_hash" in props).toBe(false);
});
test("expanded event types all use the shared plugin.* namespace", () => {
for (const ev of ["user_prompt", "bash_error", "pre_compact", "session_stop"]) {
expect(buildEvent(ev, {}, KEY)!.event).toBe(`plugin.${ev}`);
}
});
});
@@ -13,11 +13,13 @@
* installs without a key emit nothing). Disable with MEM0_TELEMETRY=false.
*
* Never sends: memory content, API keys, raw user/project IDs. Only sends:
* event type, platform, plugin version, anonymized hash of the API key.
* event type, platform, plugin version, and anonymized hashes of the API key
* and project ID.
*/
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { release } from "node:os";
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
@@ -62,6 +64,7 @@ export function buildEvent(
eventType: string,
properties: Record<string, unknown>,
apiKey: string | undefined,
projectId?: string,
): Record<string, unknown> | null {
if (!isTelemetryEnabled() || !apiKey) return null;
return {
@@ -74,9 +77,14 @@ export function buildEvent(
platform: "opencode",
plugin_version: PLUGIN_VERSION,
os: process.platform,
os_version: release(),
sample_rate: 1.0,
$process_person_profile: false,
$lib: "posthog-node",
// Anonymized project segmentation, matching telemetry.py's project_hash.
...(projectId
? { project_hash: createHash("sha256").update(projectId).digest("hex") }
: {}),
},
};
}
@@ -86,8 +94,9 @@ export function captureEvent(
eventType: string,
properties: Record<string, unknown>,
apiKey: string | undefined,
projectId?: string,
): void {
const payload = buildEvent(eventType, properties, apiKey);
const payload = buildEvent(eventType, properties, apiKey, projectId);
if (!payload) return;
try {
void fetch(POSTHOG_HOST, {
+3 -3
View File
@@ -42,7 +42,7 @@ import {
isSubagentSession,
} from "./isolation.ts";
import {
loadTriagePrompt,
loadCompactTriagePrompt,
loadDreamPrompt,
isSkillsMode,
} from "./skill-loader.ts";
@@ -464,7 +464,7 @@ function registerHooks(
"openclaw-mem0: skills-mode skipping recall for system/bootstrap prompt",
);
// Still inject the protocol, just skip recall search
const systemContext = loadTriagePrompt(cfg.skills ?? {});
const systemContext = loadCompactTriagePrompt(cfg.skills ?? {});
return { prependSystemContext: systemContext };
}
@@ -474,7 +474,7 @@ function registerHooks(
const userId = _effectiveUserId(isSubagent ? undefined : sessionId);
// Static protocol goes in prependSystemContext (cacheable across turns)
let systemContext = loadTriagePrompt(cfg.skills ?? {});
let systemContext = loadCompactTriagePrompt(cfg.skills ?? {});
if (isSubagent) {
systemContext =
"You are a subagent — use these memories for context but do not assume you are this user. Do NOT store new memories.\n\n" +
+2 -1
View File
@@ -69,7 +69,8 @@
"langsmith@<0.6.0": "^0.6.0",
"picomatch@<2.3.2": "^2.3.2",
"@qdrant/js-client-rest": "^1.18.0",
"uuid@<11.1.1": ">=11.1.1"
"uuid@<11.1.1": ">=11.1.1",
"esbuild": ">=0.28.1"
}
}
}
+124 -123
View File
@@ -11,6 +11,7 @@ overrides:
picomatch@<2.3.2: ^2.3.2
'@qdrant/js-client-rest': ^1.18.0
uuid@<11.1.1: '>=11.1.1'
esbuild: '>=0.28.1'
importers:
@@ -40,10 +41,10 @@ importers:
version: 5.9.3
vite:
specifier: ^8.0.5
version: 8.0.16(@types/node@22.19.20)(esbuild@0.27.7)
version: 8.0.16(@types/node@22.19.20)(esbuild@0.28.1)
vitest:
specifier: ^4.1.7
version: 4.1.8(@types/node@22.19.20)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.19.20)(esbuild@0.27.7))
version: 4.1.8(@types/node@22.19.20)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.1))
packages:
@@ -149,158 +150,158 @@ packages:
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@esbuild/aix-ppc64@0.27.7':
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.7':
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.7':
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.7':
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.7':
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.7':
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.7':
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.7':
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.7':
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.7':
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.7':
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.7':
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.7':
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.7':
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.7':
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.7':
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.7':
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.7':
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.7':
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.7':
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.7':
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.7':
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.7':
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.7':
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.7':
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.7':
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -879,7 +880,7 @@ packages:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
esbuild: '>=0.28.1'
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
@@ -1035,8 +1036,8 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.7:
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
@@ -2016,7 +2017,7 @@ packages:
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.18
esbuild: ^0.27.0 || ^0.28.0
esbuild: '>=0.28.1'
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
@@ -2323,82 +2324,82 @@ snapshots:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.27.7':
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.27.7':
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.27.7':
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.27.7':
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.27.7':
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.27.7':
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.27.7':
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.27.7':
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.27.7':
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.27.7':
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.27.7':
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.27.7':
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.27.7':
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.27.7':
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.27.7':
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.27.7':
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.27.7':
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.27.7':
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.27.7':
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.27.7':
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.27.7':
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.27.7':
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.27.7':
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.27.7':
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.27.7':
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.27.7':
'@esbuild/win32-x64@0.28.1':
optional: true
'@google/genai@1.52.0':
@@ -2779,7 +2780,7 @@ snapshots:
obug: 2.1.2
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.8(@types/node@22.19.20)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.19.20)(esbuild@0.27.7))
vitest: 4.1.8(@types/node@22.19.20)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.1))
'@vitest/expect@4.1.8':
dependencies:
@@ -2790,13 +2791,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@22.19.20)(esbuild@0.27.7))':
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.1))':
dependencies:
'@vitest/spy': 4.1.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.16(@types/node@22.19.20)(esbuild@0.27.7)
vite: 8.0.16(@types/node@22.19.20)(esbuild@0.28.1)
'@vitest/pretty-format@4.1.8':
dependencies:
@@ -2914,9 +2915,9 @@ snapshots:
dependencies:
run-applescript: 7.1.0
bundle-require@5.1.0(esbuild@0.27.7):
bundle-require@5.1.0(esbuild@0.28.1):
dependencies:
esbuild: 0.27.7
esbuild: 0.28.1
load-tsconfig: 0.2.5
cac@6.7.14: {}
@@ -3050,34 +3051,34 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.4
esbuild@0.27.7:
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.7
'@esbuild/android-arm': 0.27.7
'@esbuild/android-arm64': 0.27.7
'@esbuild/android-x64': 0.27.7
'@esbuild/darwin-arm64': 0.27.7
'@esbuild/darwin-x64': 0.27.7
'@esbuild/freebsd-arm64': 0.27.7
'@esbuild/freebsd-x64': 0.27.7
'@esbuild/linux-arm': 0.27.7
'@esbuild/linux-arm64': 0.27.7
'@esbuild/linux-ia32': 0.27.7
'@esbuild/linux-loong64': 0.27.7
'@esbuild/linux-mips64el': 0.27.7
'@esbuild/linux-ppc64': 0.27.7
'@esbuild/linux-riscv64': 0.27.7
'@esbuild/linux-s390x': 0.27.7
'@esbuild/linux-x64': 0.27.7
'@esbuild/netbsd-arm64': 0.27.7
'@esbuild/netbsd-x64': 0.27.7
'@esbuild/openbsd-arm64': 0.27.7
'@esbuild/openbsd-x64': 0.27.7
'@esbuild/openharmony-arm64': 0.27.7
'@esbuild/sunos-x64': 0.27.7
'@esbuild/win32-arm64': 0.27.7
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
escape-string-regexp@2.0.0: {}
@@ -4021,12 +4022,12 @@ snapshots:
tsup@8.5.1(postcss@8.5.15)(typescript@5.9.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.7)
bundle-require: 5.1.0(esbuild@0.28.1)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3
esbuild: 0.27.7
esbuild: 0.28.1
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
@@ -4069,7 +4070,7 @@ snapshots:
uuid@14.0.0: {}
vite@8.0.16(@types/node@22.19.20)(esbuild@0.27.7):
vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.1):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -4078,13 +4079,13 @@ snapshots:
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 22.19.20
esbuild: 0.27.7
esbuild: 0.28.1
fsevents: 2.3.3
vitest@4.1.8(@types/node@22.19.20)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.19.20)(esbuild@0.27.7)):
vitest@4.1.8(@types/node@22.19.20)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.1)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.19.20)(esbuild@0.27.7))
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.1))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
@@ -4101,7 +4102,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@22.19.20)(esbuild@0.27.7)
vite: 8.0.16(@types/node@22.19.20)(esbuild@0.28.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.19.20
@@ -19,3 +19,4 @@ overrides:
"picomatch@<2.3.2": "^2.3.2"
"@qdrant/js-client-rest": "^1.18.0"
"uuid@<11.1.1": ">=11.1.1"
"esbuild": ">=0.28.1"
+74 -1
View File
@@ -2,7 +2,12 @@
* Tests for path traversal prevention in skill-loader.
*/
import { describe, it, expect } from "vitest";
import { safePath, loadSkill } from "./skill-loader.ts";
import {
safePath,
loadSkill,
loadTriagePrompt,
loadCompactTriagePrompt,
} from "./skill-loader.ts";
// ---------------------------------------------------------------------------
// safePath — path containment
@@ -68,3 +73,71 @@ describe("loadSkill path traversal", () => {
expect(result?.prompt).toBeTruthy();
});
});
describe("loadCompactTriagePrompt", () => {
it("keeps the core triage instructions without inlining the full skill body", () => {
const prompt = loadCompactTriagePrompt();
expect(prompt).toContain("Use `memory_add` tool for ALL user facts");
expect(prompt).toContain("Batch facts by CATEGORY");
expect(prompt).toContain("ALWAYS rewrite the query");
expect(prompt).not.toContain("## Worked Examples");
expect(prompt).not.toContain("### memory_search");
expect(prompt).not.toContain("Conference requires at least 4 breakout rooms");
expect(prompt.length).toBeLessThan(3500);
});
it("keeps the always-recall guidance aligned with the full triage prompt", () => {
const config = { recall: { strategy: "always" as const } };
const expected =
"Automatic recall runs for both long-term and session memory. Use manual searches only when you need more specific context.";
expect(loadCompactTriagePrompt(config)).toContain(expected);
expect(loadTriagePrompt(config)).toContain(expected);
});
it("keeps the manual-recall guidance in compact mode", () => {
const prompt = loadCompactTriagePrompt({
recall: { strategy: "manual" },
});
expect(prompt).toContain("No automatic recall happens in manual mode.");
});
it("includes short config summaries and truncates oversized custom rules", () => {
const prompt = loadCompactTriagePrompt({
categories: {
travel: { importance: 0.7, ttl: "30d" },
},
customRules: {
include: [
"Always remember workshop venue requirements.",
"Keep track of recurring conference planning constraints.",
"Capture every catering preference, transit note, and presenter dependency in detail.",
],
exclude: [
"Never store one-off demo logs, temporary ETA chatter, or transitory checklist updates.",
"Skip verbose retrospectives unless the user explicitly says the lesson should persist.",
],
},
});
expect(prompt).toContain("travel (importance 0.7, expires: 30d)");
expect(prompt).toContain("include rule(s) and 2 exclude rule(s) are configured");
expect(prompt).toContain("Prompt kept compact, full rule text omitted");
expect(prompt).toContain('Preview: include "Always remember workshop venue requirements."');
});
it("omits default credential patterns in compact mode unless custom patterns are configured", () => {
const defaultPrompt = loadCompactTriagePrompt();
const customPrompt = loadCompactTriagePrompt({
triage: {
credentialPatterns: ["mem0-secret=", "Bearer "],
},
});
expect(defaultPrompt).not.toContain("Credential patterns to scan");
expect(customPrompt).toContain("Credential patterns to scan");
expect(customPrompt).toContain("mem0-secret=");
});
});
+201 -3
View File
@@ -191,8 +191,16 @@ function renderCategoriesBlock(
return lines.join("\n");
}
function renderTriageKnobs(config: SkillsConfig): string {
function renderTriageKnobs(
config: SkillsConfig,
options: {
includeCredentialPatterns?: boolean;
includeDefaultCredentialPatterns?: boolean;
} = {},
): string {
const lines: string[] = [];
const includeCredentialPatterns = options.includeCredentialPatterns ?? true;
const includeDefaultCredentialPatterns = options.includeDefaultCredentialPatterns ?? true;
if (config.triage?.importanceThreshold !== undefined) {
lines.push(
@@ -200,13 +208,82 @@ function renderTriageKnobs(config: SkillsConfig): string {
);
}
const patterns = resolveCredentialPatterns(config);
lines.push(`- Credential patterns to scan: ${patterns.map((p) => `\`${p}\``).join(", ")}`);
const hasCustomCredentialPatterns = config.triage?.credentialPatterns !== undefined;
if (includeCredentialPatterns && (includeDefaultCredentialPatterns || hasCustomCredentialPatterns)) {
const patterns = resolveCredentialPatterns(config);
lines.push(`- Credential patterns to scan: ${patterns.map((p) => `\`${p}\``).join(", ")}`);
}
if (lines.length === 0) return "";
return "\n## Active Configuration Overrides\n\n" + lines.join("\n");
}
const COMPACT_CUSTOM_RULE_CHAR_BUDGET = 280;
const COMPACT_CUSTOM_RULE_PREVIEW_LIMIT = 2;
function formatCategoryConfig(
name: string,
cat: CategoryConfig,
): string {
const ttlLabel = cat.ttl ? `expires: ${cat.ttl}` : "permanent";
const immLabel = cat.immutable ? ", immutable" : "";
return `${name} (importance ${cat.importance}, ${ttlLabel}${immLabel})`;
}
function renderCompactCategories(config: SkillsConfig): string[] {
if (!config.categories || Object.keys(config.categories).length === 0) {
return [];
}
const mergedCategories = resolveCategories(config);
return [
"## Active Category Overrides",
"",
...Object.entries(config.categories).map(([name]) =>
`- ${formatCategoryConfig(name, mergedCategories[name]!)}`,
),
];
}
function renderCompactCustomRules(config: SkillsConfig): string[] {
const includeRules = config.customRules?.include ?? [];
const excludeRules = config.customRules?.exclude ?? [];
if (includeRules.length === 0 && excludeRules.length === 0) {
return [];
}
const formatRuleList = (label: string, rules: string[]) =>
`${label}: ${rules.map((rule) => `"${rule}"`).join("; ")}`;
const lines: string[] = [];
if (includeRules.length > 0) {
lines.push(formatRuleList("Include", includeRules));
}
if (excludeRules.length > 0) {
lines.push(formatRuleList("Exclude", excludeRules));
}
const combined = lines.join(" ");
if (combined.length <= COMPACT_CUSTOM_RULE_CHAR_BUDGET) {
return ["## Active Custom Rules", "", ...lines.map((line) => `- ${line}`)];
}
const preview = [
...includeRules.slice(0, COMPACT_CUSTOM_RULE_PREVIEW_LIMIT).map((rule) => `include "${rule}"`),
...excludeRules.slice(0, COMPACT_CUSTOM_RULE_PREVIEW_LIMIT).map((rule) => `exclude "${rule}"`),
];
return [
"## Active Custom Rules",
"",
`- ${includeRules.length} include rule(s) and ${excludeRules.length} exclude rule(s) are configured.`,
`- Prompt kept compact, full rule text omitted because it exceeded ${COMPACT_CUSTOM_RULE_CHAR_BUDGET} characters.`,
...(preview.length > 0
? [`- Preview: ${preview.join("; ")}`]
: []),
];
}
// ============================================================================
// TTL Helpers
// ============================================================================
@@ -358,6 +435,11 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
"- Before updating a memory, search to find the existing version.",
);
parts.push("");
} else if (strategy === "always") {
parts.push(
"Automatic recall runs for both long-term and session memory. Use manual searches only when you need more specific context.",
);
parts.push("");
}
parts.push(
@@ -453,6 +535,122 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
return parts.join("\n");
}
/**
* Build a compact memory system prompt for skills mode turns.
*
* This keeps the required storage and search protocol, plus short config
* summaries, without inlining the full memory-triage skill body every turn.
* If the skill file cannot be read, delegate to loadTriagePrompt(), which has
* its own minimal inline fallback.
*/
export function loadCompactTriagePrompt(config: SkillsConfig = {}): string {
if (!readSkillFile("memory-triage")) {
return loadTriagePrompt(config);
}
const parts: string[] = [];
parts.push("<memory-system>");
parts.push(
"IMPORTANT: Use `memory_add` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).",
);
parts.push(
"After every response, evaluate whether a new agent would benefit from remembering this days later. Most turns should produce zero memory operations.",
);
parts.push(
"Only store durable, self-contained, third-person facts: identity, preferences with rationale, standing rules, decisions, projects, configurations, technical context, and relationships.",
);
parts.push(
"Never store credentials, tokens, webhook secrets, or raw tool output. If a secret was configured, store only that the credential was configured.",
);
parts.push(
"When a recalled fact materially changes, search for the existing memory and update it in place. Skip cosmetic rewording.",
);
parts.push("");
parts.push("## Tool Usage");
parts.push("");
parts.push(
"Batch facts by CATEGORY. All facts in one memory_add call must share the same category because category determines retention policy (TTL, immutability). If a turn has facts in different categories, make one call per category.",
);
parts.push(
'Format: memory_add(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")',
);
parts.push(
'Mixed categories: memory_add(..., category: "identity") and memory_add(..., category: "decision") in separate calls.',
);
parts.push(
"Categories: identity, configuration, rule, preference, decision, technical, relationship, project.",
);
const categoryLines = renderCompactCategories(config);
if (categoryLines.length > 0) {
parts.push("");
parts.push(...categoryLines);
}
const knobLines = renderTriageKnobs(config, {
includeDefaultCredentialPatterns: false,
})
.split("\n")
.filter(Boolean);
if (knobLines.length > 0) {
parts.push("");
parts.push(...knobLines);
}
const customRuleLines = renderCompactCustomRules(config);
if (customRuleLines.length > 0) {
parts.push("");
parts.push(...customRuleLines);
}
if (config.recall?.enabled !== false) {
const strategy = config.recall?.strategy ?? "smart";
parts.push("");
parts.push("## Searching Memory");
parts.push("");
if (strategy === "manual") {
parts.push(
"No automatic recall happens in manual mode. Use memory_search proactively at conversation start, when context is missing, when topics shift, and before updating a memory.",
);
} else if (strategy === "always") {
parts.push(
"Automatic recall runs for both long-term and session memory. Use manual searches only when you need more specific context.",
);
} else {
parts.push(
"Automatic recall runs for long-term memory. Use manual searches when the injected context is not enough.",
);
}
parts.push(
"When calling memory_search, ALWAYS rewrite the query. NEVER pass the user's raw message.",
);
parts.push(
"Convert the request into 3-6 factual keywords that match stored memory language: user, decided, prefers, rule, configured, based in, plus the concrete nouns and names from the request.",
);
parts.push(
'WRONG: memory_search("Who was that nutritionist my wife recommended?")',
);
parts.push(
'RIGHT: memory_search("nutritionist wife recommended relationship")',
);
parts.push('WRONG: memory_search("What timezone am I in?")');
parts.push('RIGHT: memory_search("user timezone location based")');
// Intentionally omitted from the compact path: ENTITY SCOPING and SEARCH SCOPE.
// loadTriagePrompt() keeps the full explanatory sections for the non-compact path.
parts.push(
'Scope: use "long-term" for durable user context, "session" for this conversation, and "all" only when you truly need both.',
);
parts.push(
"When the request implies a time range or category, add filters or categories instead of broadening the query text.",
);
}
parts.push("</memory-system>");
return parts.join("\n");
}
/**
* Load the dream skill prompt for consolidation sessions.
*/
+2 -1
View File
@@ -71,7 +71,8 @@
},
"pnpm": {
"overrides": {
"uuid@<11.1.1": ">=11.1.1"
"uuid@<11.1.1": ">=11.1.1",
"esbuild": ">=0.28.1"
}
}
}
+189 -124
View File
@@ -6,6 +6,7 @@ settings:
overrides:
uuid@<11.1.1: '>=11.1.1'
esbuild: '>=0.28.1'
importers:
@@ -13,7 +14,7 @@ importers:
dependencies:
mem0ai:
specifier: ^3.0.7
version: 3.0.7(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.21.0)(redis@5.12.1)(ws@8.21.0)
version: 3.0.7(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.11.3)(redis@5.12.1)(ws@8.21.0)
devDependencies:
'@earendil-works/pi-ai':
specifier: ^0.79.0
@@ -35,7 +36,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^4.1.7
version: 4.1.8(@types/node@25.9.2)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))
version: 4.1.8(@types/node@25.9.2)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))
packages:
@@ -253,158 +254,158 @@ packages:
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@esbuild/aix-ppc64@0.27.7':
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.7':
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.7':
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.7':
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.7':
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.7':
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.7':
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.7':
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.7':
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.7':
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.7':
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.7':
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.7':
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.7':
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.7':
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.7':
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.7':
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.7':
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.7':
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.7':
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.7':
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.7':
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.7':
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.7':
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.7':
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.7':
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -469,30 +470,35 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-arm64-musl@0.3.9':
resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@mariozechner/clipboard-linux-riscv64-gnu@0.3.9':
resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-x64-gnu@0.3.9':
resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-x64-musl@0.3.9':
resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@mariozechner/clipboard-win32-arm64-msvc@0.3.9':
resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==}
@@ -639,36 +645,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.3':
resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.3':
resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.3':
resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.3':
resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.3':
resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.3':
resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==}
@@ -730,66 +742,79 @@ packages:
resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.61.1':
resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.61.1':
resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.61.1':
resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.61.1':
resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.61.1':
resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.61.1':
resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.61.1':
resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.61.1':
resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.61.1':
resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.61.1':
resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.61.1':
resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.61.1':
resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.61.1':
resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==}
@@ -1080,6 +1105,10 @@ packages:
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-writer@2.0.0:
resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==}
engines: {node: '>=4'}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -1091,7 +1120,7 @@ packages:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
esbuild: '>=0.28.1'
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
@@ -1259,8 +1288,8 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.7:
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
@@ -1600,24 +1629,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -1901,6 +1934,9 @@ packages:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
packet-reader@1.0.0:
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
partial-json@0.1.7:
resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==}
@@ -1949,6 +1985,15 @@ packages:
resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==}
engines: {node: '>=10'}
pg@8.11.3:
resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==}
engines: {node: '>= 8.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pg@8.21.0:
resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==}
engines: {node: '>= 16.0.0'}
@@ -2330,7 +2375,7 @@ packages:
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.18
esbuild: ^0.27.0 || ^0.28.0
esbuild: '>=0.28.1'
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
@@ -2925,82 +2970,82 @@ snapshots:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.27.7':
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.27.7':
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.27.7':
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.27.7':
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.27.7':
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.27.7':
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.27.7':
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.27.7':
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.27.7':
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.27.7':
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.27.7':
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.27.7':
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.27.7':
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.27.7':
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.27.7':
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.27.7':
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.27.7':
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.27.7':
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.27.7':
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.27.7':
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.27.7':
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.27.7':
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.27.7':
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.27.7':
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.27.7':
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.27.7':
'@esbuild/win32-x64@0.28.1':
optional: true
'@google/genai@1.52.0':
@@ -3478,13 +3523,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))':
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)
vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)
'@vitest/pretty-format@4.1.8':
dependencies:
@@ -3597,6 +3642,8 @@ snapshots:
buffer-equal-constant-time@1.0.1: {}
buffer-writer@2.0.0: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
@@ -3606,9 +3653,9 @@ snapshots:
dependencies:
run-applescript: 7.1.0
bundle-require@5.1.0(esbuild@0.27.7):
bundle-require@5.1.0(esbuild@0.28.1):
dependencies:
esbuild: 0.27.7
esbuild: 0.28.1
load-tsconfig: 0.2.5
cac@6.7.14: {}
@@ -3752,34 +3799,34 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.4
esbuild@0.27.7:
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.7
'@esbuild/android-arm': 0.27.7
'@esbuild/android-arm64': 0.27.7
'@esbuild/android-x64': 0.27.7
'@esbuild/darwin-arm64': 0.27.7
'@esbuild/darwin-x64': 0.27.7
'@esbuild/freebsd-arm64': 0.27.7
'@esbuild/freebsd-x64': 0.27.7
'@esbuild/linux-arm': 0.27.7
'@esbuild/linux-arm64': 0.27.7
'@esbuild/linux-ia32': 0.27.7
'@esbuild/linux-loong64': 0.27.7
'@esbuild/linux-mips64el': 0.27.7
'@esbuild/linux-ppc64': 0.27.7
'@esbuild/linux-riscv64': 0.27.7
'@esbuild/linux-s390x': 0.27.7
'@esbuild/linux-x64': 0.27.7
'@esbuild/netbsd-arm64': 0.27.7
'@esbuild/netbsd-x64': 0.27.7
'@esbuild/openbsd-arm64': 0.27.7
'@esbuild/openbsd-x64': 0.27.7
'@esbuild/openharmony-arm64': 0.27.7
'@esbuild/sunos-x64': 0.27.7
'@esbuild/win32-arm64': 0.27.7
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
escape-string-regexp@2.0.0: {}
@@ -4190,7 +4237,7 @@ snapshots:
crypt: 0.0.2
is-buffer: 1.1.6
mem0ai@3.0.7(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.21.0)(redis@5.12.1)(ws@8.21.0):
mem0ai@3.0.7(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.11.3)(redis@5.12.1)(ws@8.21.0):
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
'@azure/identity': 4.13.1
@@ -4211,7 +4258,7 @@ snapshots:
natural: 8.1.1
ollama: 0.5.18
openai: 4.104.0(ws@8.21.0)(zod@3.25.76)
pg: 8.21.0
pg: 8.11.3
redis: 5.12.1
uuid: 14.0.0
zod: 3.25.76
@@ -4402,6 +4449,8 @@ snapshots:
dependencies:
p-finally: 1.0.0
packet-reader@1.0.0: {}
partial-json@0.1.7: {}
path-expression-matcher@1.5.0: {}
@@ -4424,6 +4473,10 @@ snapshots:
pg-numeric@1.0.2: {}
pg-pool@3.14.0(pg@8.11.3):
dependencies:
pg: 8.11.3
pg-pool@3.14.0(pg@8.21.0):
dependencies:
pg: 8.21.0
@@ -4448,6 +4501,18 @@ snapshots:
postgres-interval: 3.0.0
postgres-range: 1.1.4
pg@8.11.3:
dependencies:
buffer-writer: 2.0.0
packet-reader: 1.0.0
pg-connection-string: 2.13.0
pg-pool: 3.14.0(pg@8.11.3)
pg-protocol: 1.14.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.4.0
pg@8.21.0:
dependencies:
pg-connection-string: 2.13.0
@@ -4783,12 +4848,12 @@ snapshots:
tsup@8.5.1(jiti@2.7.0)(postcss@8.5.15)(typescript@6.0.3)(yaml@2.9.0):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.7)
bundle-require: 5.1.0(esbuild@0.28.1)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3
esbuild: 0.27.7
esbuild: 0.28.1
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
@@ -4837,7 +4902,7 @@ snapshots:
uuid@14.0.0: {}
vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0):
vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -4846,15 +4911,15 @@ snapshots:
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 25.9.2
esbuild: 0.27.7
esbuild: 0.28.1
fsevents: 2.3.3
jiti: 2.7.0
yaml: 2.9.0
vitest@4.1.8(@types/node@25.9.2)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)):
vitest@4.1.8(@types/node@25.9.2)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
@@ -4871,7 +4936,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)
vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.9.2
@@ -3,3 +3,4 @@ packages:
overrides:
"uuid@<11.1.1": ">=11.1.1"
"esbuild": ">=0.28.1"
+2 -1
View File
@@ -84,7 +84,8 @@
"minimatch@>=5.0.0 <5.1.8": "^5.1.8",
"minimatch@>=9.0.0 <9.0.7": "^9.0.7",
"picomatch@<2.3.2": "^2.3.2",
"rollup@>=4.0.0 <4.59.0": "^4.59.0"
"rollup@>=4.0.0 <4.59.0": "^4.59.0",
"esbuild": ">=0.28.1"
}
}
}
+116 -115
View File
@@ -11,6 +11,7 @@ overrides:
minimatch@>=9.0.0 <9.0.7: ^9.0.7
picomatch@<2.3.2: ^2.3.2
rollup@>=4.0.0 <4.59.0: ^4.59.0
esbuild: '>=0.28.1'
importers:
@@ -67,7 +68,7 @@ importers:
version: 3.1.14
ts-jest:
specifier: ^29.4.11
version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.7)(jest-util@29.7.0)(jest@29.7.0(@types/node@18.19.130)(ts-node@10.9.2(@types/node@18.19.130)(typescript@5.9.3)))(typescript@5.9.3)
version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@29.7.0)(jest@29.7.0(@types/node@18.19.130)(ts-node@10.9.2(@types/node@18.19.130)(typescript@5.9.3)))(typescript@5.9.3)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@18.19.130)(typescript@5.9.3)
@@ -303,158 +304,158 @@ packages:
resolution: {integrity: sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==}
engines: {node: '>=16'}
'@esbuild/aix-ppc64@0.27.7':
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.7':
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.7':
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.7':
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.7':
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.7':
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.7':
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.7':
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.7':
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.7':
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.7':
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.7':
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.7':
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.7':
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.7':
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.7':
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.7':
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.7':
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.7':
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.7':
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.7':
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.7':
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.7':
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.7':
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.7':
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.7':
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -866,7 +867,7 @@ packages:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
esbuild: '>=0.28.1'
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
@@ -1011,8 +1012,8 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
esbuild@0.27.7:
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
@@ -2142,82 +2143,82 @@ snapshots:
dependencies:
'@edge-runtime/primitives': 4.1.0
'@esbuild/aix-ppc64@0.27.7':
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.27.7':
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.27.7':
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.27.7':
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.27.7':
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.27.7':
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.27.7':
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.27.7':
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.27.7':
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.27.7':
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.27.7':
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.27.7':
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.27.7':
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.27.7':
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.27.7':
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.27.7':
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.27.7':
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.27.7':
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.27.7':
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.27.7':
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.27.7':
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.27.7':
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.27.7':
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.27.7':
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.27.7':
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.27.7':
'@esbuild/win32-x64@0.28.1':
optional: true
'@istanbuljs/load-nyc-config@1.1.0':
@@ -2702,9 +2703,9 @@ snapshots:
buffer-from@1.1.2: {}
bundle-require@5.1.0(esbuild@0.27.7):
bundle-require@5.1.0(esbuild@0.28.1):
dependencies:
esbuild: 0.27.7
esbuild: 0.28.1
load-tsconfig: 0.2.5
cac@6.7.14: {}
@@ -2823,34 +2824,34 @@ snapshots:
es-errors@1.3.0: {}
esbuild@0.27.7:
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.7
'@esbuild/android-arm': 0.27.7
'@esbuild/android-arm64': 0.27.7
'@esbuild/android-x64': 0.27.7
'@esbuild/darwin-arm64': 0.27.7
'@esbuild/darwin-x64': 0.27.7
'@esbuild/freebsd-arm64': 0.27.7
'@esbuild/freebsd-x64': 0.27.7
'@esbuild/linux-arm': 0.27.7
'@esbuild/linux-arm64': 0.27.7
'@esbuild/linux-ia32': 0.27.7
'@esbuild/linux-loong64': 0.27.7
'@esbuild/linux-mips64el': 0.27.7
'@esbuild/linux-ppc64': 0.27.7
'@esbuild/linux-riscv64': 0.27.7
'@esbuild/linux-s390x': 0.27.7
'@esbuild/linux-x64': 0.27.7
'@esbuild/netbsd-arm64': 0.27.7
'@esbuild/netbsd-x64': 0.27.7
'@esbuild/openbsd-arm64': 0.27.7
'@esbuild/openbsd-x64': 0.27.7
'@esbuild/openharmony-arm64': 0.27.7
'@esbuild/sunos-x64': 0.27.7
'@esbuild/win32-arm64': 0.27.7
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
escalade@3.2.0: {}
@@ -3711,7 +3712,7 @@ snapshots:
ts-interface-checker@0.1.13: {}
ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.7)(jest-util@29.7.0)(jest@29.7.0(@types/node@18.19.130)(ts-node@10.9.2(@types/node@18.19.130)(typescript@5.9.3)))(typescript@5.9.3):
ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@29.7.0)(jest@29.7.0(@types/node@18.19.130)(ts-node@10.9.2(@types/node@18.19.130)(typescript@5.9.3)))(typescript@5.9.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@@ -3729,7 +3730,7 @@ snapshots:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.29.7)
esbuild: 0.27.7
esbuild: 0.28.1
jest-util: 29.7.0
ts-node@10.9.2(@types/node@18.19.130)(typescript@5.9.3):
@@ -3752,12 +3753,12 @@ snapshots:
tsup@8.5.1(typescript@5.9.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.7)
bundle-require: 5.1.0(esbuild@0.28.1)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3(supports-color@5.5.0)
esbuild: 0.27.7
esbuild: 0.28.1
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
@@ -12,3 +12,4 @@ overrides:
"minimatch@>=9.0.0 <9.0.7": "^9.0.7"
"picomatch@<2.3.2": "^2.3.2"
"rollup@>=4.0.0 <4.59.0": "^4.59.0"
"esbuild": ">=0.28.1"
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "3.0.7",
"version": "3.0.8",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -154,7 +154,8 @@
"rollup@>=4.0.0 <4.59.0": "^4.59.0",
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
"glob@>=10.2.0 <10.5.0": "^10.5.0",
"@modelcontextprotocol/sdk": "^1.25.4"
"@modelcontextprotocol/sdk": "^1.25.4",
"esbuild": ">=0.28.1"
}
}
}
+116 -115
View File
@@ -21,6 +21,7 @@ overrides:
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
glob@>=10.2.0 <10.5.0: ^10.5.0
'@modelcontextprotocol/sdk': ^1.25.4
esbuild: '>=0.28.1'
importers:
@@ -125,7 +126,7 @@ importers:
version: 5.0.10
ts-jest:
specifier: ^29.2.6
version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.7)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.21)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.5.4)))(typescript@5.5.4)
version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.21)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.5.4)))(typescript@5.5.4)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.19.21)(typescript@5.5.4)
@@ -375,158 +376,158 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@esbuild/aix-ppc64@0.27.7':
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.7':
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.7':
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.7':
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.7':
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.7':
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.7':
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.7':
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.7':
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.7':
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.7':
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.7':
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.7':
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.7':
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.7':
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.7':
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.7':
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.7':
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.7':
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.7':
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.7':
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.7':
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.7':
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.7':
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.7':
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.7':
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -1206,7 +1207,7 @@ packages:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
esbuild: '>=0.28.1'
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
@@ -1450,8 +1451,8 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.7:
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
@@ -3381,82 +3382,82 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@esbuild/aix-ppc64@0.27.7':
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.27.7':
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.27.7':
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.27.7':
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.27.7':
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.27.7':
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.27.7':
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.27.7':
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.27.7':
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.27.7':
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.27.7':
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.27.7':
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.27.7':
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.27.7':
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.27.7':
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.27.7':
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.27.7':
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.27.7':
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.27.7':
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.27.7':
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.27.7':
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.27.7':
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.27.7':
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.27.7':
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.27.7':
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.27.7':
'@esbuild/win32-x64@0.28.1':
optional: true
'@google/genai@1.52.0':
@@ -4218,9 +4219,9 @@ snapshots:
dependencies:
run-applescript: 7.1.0
bundle-require@5.1.0(esbuild@0.27.7):
bundle-require@5.1.0(esbuild@0.28.1):
dependencies:
esbuild: 0.27.7
esbuild: 0.28.1
load-tsconfig: 0.2.5
cac@6.7.14: {}
@@ -4435,34 +4436,34 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.4
esbuild@0.27.7:
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.7
'@esbuild/android-arm': 0.27.7
'@esbuild/android-arm64': 0.27.7
'@esbuild/android-x64': 0.27.7
'@esbuild/darwin-arm64': 0.27.7
'@esbuild/darwin-x64': 0.27.7
'@esbuild/freebsd-arm64': 0.27.7
'@esbuild/freebsd-x64': 0.27.7
'@esbuild/linux-arm': 0.27.7
'@esbuild/linux-arm64': 0.27.7
'@esbuild/linux-ia32': 0.27.7
'@esbuild/linux-loong64': 0.27.7
'@esbuild/linux-mips64el': 0.27.7
'@esbuild/linux-ppc64': 0.27.7
'@esbuild/linux-riscv64': 0.27.7
'@esbuild/linux-s390x': 0.27.7
'@esbuild/linux-x64': 0.27.7
'@esbuild/netbsd-arm64': 0.27.7
'@esbuild/netbsd-x64': 0.27.7
'@esbuild/openbsd-arm64': 0.27.7
'@esbuild/openbsd-x64': 0.27.7
'@esbuild/openharmony-arm64': 0.27.7
'@esbuild/sunos-x64': 0.27.7
'@esbuild/win32-arm64': 0.27.7
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
escalade@3.2.0: {}
@@ -6016,7 +6017,7 @@ snapshots:
ts-interface-checker@0.1.13: {}
ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.7)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.21)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.5.4)))(typescript@5.5.4):
ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.21)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.5.4)))(typescript@5.5.4):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@@ -6034,7 +6035,7 @@ snapshots:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.29.7)
esbuild: 0.27.7
esbuild: 0.28.1
jest-util: 29.7.0
ts-node@10.9.2(@types/node@22.19.21)(typescript@5.5.4):
@@ -6059,12 +6060,12 @@ snapshots:
tsup@8.5.1(typescript@5.5.4):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.7)
bundle-require: 5.1.0(esbuild@0.28.1)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3(supports-color@5.5.0)
esbuild: 0.27.7
esbuild: 0.28.1
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
+1
View File
@@ -22,3 +22,4 @@ overrides:
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4"
"glob@>=10.2.0 <10.5.0": "^10.5.0"
"@modelcontextprotocol/sdk": "^1.25.4"
"esbuild": ">=0.28.1"
+10
View File
@@ -112,6 +112,13 @@ export class ConfigManager {
| undefined) ??
userConf?.url ??
defaultConf.baseURL;
const llmRaw = userConf as Record<string, unknown> | undefined;
const temperature =
userConf?.temperature ??
(llmRaw?.temperature as number | undefined);
const topP = userConf?.topP ?? (llmRaw?.top_p as number | undefined);
const maxTokens =
userConf?.maxTokens ?? (llmRaw?.max_tokens as number | undefined);
return {
baseURL: llmBaseURL,
@@ -125,6 +132,9 @@ export class ConfigManager {
userConf?.modelProperties !== undefined
? userConf.modelProperties
: defaultConf.modelProperties,
temperature,
topP,
maxTokens,
};
})(),
},
+51 -9
View File
@@ -5,6 +5,9 @@ import { LLMConfig, Message } from "../types";
export class AnthropicLLM implements LLM {
private client: Anthropic;
private model: string;
private maxTokens: number;
private temperature?: number;
private topP?: number;
constructor(config: LLMConfig) {
const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
@@ -12,18 +15,24 @@ export class AnthropicLLM implements LLM {
throw new Error("Anthropic API key is required");
}
this.client = new Anthropic({ apiKey });
this.model = config.model || "claude-3-sonnet-20240229";
this.model = config.model || "claude-sonnet-4-6";
// Defaults mirror the Python provider's AnthropicConfig
// (max_tokens=2000, temperature=0.1, top_p omitted).
this.maxTokens = config.maxTokens ?? 2000;
this.temperature = config.temperature ?? 0.1;
this.topP = config.topP;
}
async generateResponse(
messages: Message[],
responseFormat?: { type: string },
): Promise<string> {
tools?: any[],
): Promise<string | LLMResponse> {
// Extract system message if present
const systemMessage = messages.find((msg) => msg.role === "system");
const otherMessages = messages.filter((msg) => msg.role !== "system");
const response = await this.client.messages.create({
const params: Anthropic.MessageCreateParamsNonStreaming = {
model: this.model,
messages: otherMessages.map((msg) => ({
role: msg.role as "user" | "assistant",
@@ -36,8 +45,41 @@ export class AnthropicLLM implements LLM {
typeof systemMessage?.content === "string"
? systemMessage.content
: undefined,
max_tokens: 4096,
});
max_tokens: this.maxTokens,
};
// Anthropic rejects requests that include both temperature and top_p;
// prefer temperature, matching the Python provider's _get_common_params.
if (this.temperature !== undefined) {
params.temperature = this.temperature;
} else if (this.topP !== undefined) {
params.top_p = this.topP;
}
if (tools) {
params.tools = tools;
params.tool_choice = { type: "auto" };
}
const response = await this.client.messages.create(params);
if (tools) {
let content = "";
const toolCalls: Array<{ name: string; arguments: string }> = [];
for (const block of response.content) {
if (block.type === "text") {
content = block.text;
} else if (block.type === "tool_use") {
toolCalls.push({
name: block.name,
arguments: JSON.stringify(block.input),
});
}
}
return { content, role: "assistant", toolCalls };
}
const firstBlock = response.content[0];
if (firstBlock.type === "text") {
@@ -49,9 +91,9 @@ export class AnthropicLLM implements LLM {
async generateChat(messages: Message[]): Promise<LLMResponse> {
const response = await this.generateResponse(messages);
return {
content: response,
role: "assistant",
};
if (typeof response === "string") {
return { content: response, role: "assistant" };
}
return response;
}
}
+232 -17
View File
@@ -36,10 +36,29 @@ import {
SearchMemoryOptions,
DeleteAllMemoryOptions,
GetAllMemoryOptions,
UpdateProjectOptions,
} from "./memory.types";
import { parse_vision_messages } from "../utils/memory";
import { HistoryManager } from "../storage/base";
import { captureClientEvent } from "../utils/telemetry";
import {
detectScaleThresholdFromAddResult,
detectScaleThresholdFromTopK,
detectPerformanceSlowQuery,
detectTemporalUsageFromMetadata,
detectTemporalUsageFromSearch,
displayDecayUsageNotice,
displayFirstRunNotice,
displayPerformanceSlowQueryNotice,
displayScaleThresholdNotice,
displayTemporalUsageNotice,
getDecayFeatureErrorMessage,
getDecayUsageDeleteCountAfterSuccess,
getTemporalFeatureErrorMessage,
isDecayUsageDeleteEligible,
PerformanceSlowQueryTrigger,
ScaleThresholdTrigger,
} from "../utils/notices";
import { lemmatizeForBm25 } from "../utils/lemmatization";
import {
extractEntities,
@@ -494,6 +513,73 @@ export class Memory {
}
}
private async _displayFirstRunNotice(triggerFunction: string) {
try {
await this._getTelemetryId();
await displayFirstRunNotice(this, triggerFunction);
} catch {}
}
private async _displayDecayUsageNotice(trigger: {
triggerFunction: "delete" | "delete_all";
triggerSource: "delete_count" | "delete_all";
triggerReason: "repeated_deletes" | "bulk_delete";
deleteCount?: number;
deletedCount?: number;
}) {
try {
await this._getTelemetryId();
await displayDecayUsageNotice(this, trigger);
} catch {}
}
private async _displayTemporalUsageNotice(trigger: {
triggerFunction: "add" | "search";
triggerSource: "metadata" | "query" | "filter";
triggerReason:
| "date_like_metadata"
| "relative_phrase"
| "date_like_query"
| "date_range_filter";
}) {
try {
await this._getTelemetryId();
await displayTemporalUsageNotice(this, trigger);
} catch {}
}
private async _displayScaleThresholdNotice(trigger: ScaleThresholdTrigger) {
try {
await this._getTelemetryId();
await displayScaleThresholdNotice(this, trigger);
} catch {}
}
private async _displayPerformanceSlowQueryNotice(
trigger: PerformanceSlowQueryTrigger,
) {
try {
await this._getTelemetryId();
await displayPerformanceSlowQueryNotice(this, trigger);
} catch {}
}
private async _getNoticeTelemetryId() {
try {
if (
!this.telemetryId ||
this.telemetryId === "anonymous" ||
this.telemetryId === "anonymous-supabase"
) {
this.telemetryId = (await getOrCreateMem0UserId()) || "anonymous";
}
return this.telemetryId;
} catch {
this.telemetryId = "anonymous";
return this.telemetryId;
}
}
static fromConfig(configDict: Record<string, any>): Memory {
try {
const config = MemoryConfigSchema.parse(configDict);
@@ -504,10 +590,29 @@ export class Memory {
}
}
async updateProject(options: UpdateProjectOptions = {}): Promise<never> {
if (options?.decay === true) {
await this._getNoticeTelemetryId();
throw new Error(await getDecayFeatureErrorMessage(this));
}
throw new Error("Project updates are not supported by the OSS Memory SDK.");
}
async add(
messages: string | Message[],
config: AddMemoryOptions,
): Promise<SearchResult> {
if (config?.timestamp !== undefined) {
await this._getNoticeTelemetryId();
throw new Error(
await getTemporalFeatureErrorMessage(this, {
triggerFunction: "add",
triggerParameter: "timestamp",
}),
);
}
// Validate messages input
if (messages === undefined || messages === null) {
throw new Error(
@@ -515,6 +620,10 @@ export class Memory {
);
}
const temporalUsageNotice = detectTemporalUsageFromMetadata(
config?.metadata,
);
await this._ensureInitialized();
await this._captureEvent("add", {
message_count: Array.isArray(messages) ? messages.length : 1,
@@ -554,6 +663,27 @@ export class Memory {
infer,
);
if (temporalUsageNotice) {
await this._displayTemporalUsageNotice({
triggerFunction: "add",
triggerSource: temporalUsageNotice.triggerSource,
triggerReason: temporalUsageNotice.triggerReason,
});
} else {
const scaleThresholdNotice = await detectScaleThresholdFromAddResult(
this,
vectorStoreResult,
);
if (scaleThresholdNotice) {
await this._displayScaleThresholdNotice({
triggerFunction: "add",
...scaleThresholdNotice,
});
} else {
await this._displayFirstRunNotice("add");
}
}
return {
results: vectorStoreResult,
};
@@ -996,7 +1126,10 @@ export class Memory {
async get(memoryId: string): Promise<MemoryItem | null> {
await this._ensureInitialized();
const memory = await this.vectorStore.get(memoryId);
if (!memory) return null;
if (!memory) {
await this._displayFirstRunNotice("get");
return null;
}
const filters = {
...(memory.payload.user_id && { user_id: memory.payload.user_id }),
@@ -1031,13 +1164,30 @@ export class Memory {
}
}
return { ...memoryItem, ...filters };
const result = { ...memoryItem, ...filters };
await this._displayFirstRunNotice("get");
return result;
}
async search(
query: string,
config: SearchMemoryOptions,
): Promise<SearchResult> {
if (config?.referenceDate !== undefined) {
await this._getNoticeTelemetryId();
throw new Error(
await getTemporalFeatureErrorMessage(this, {
triggerFunction: "search",
triggerParameter: "referenceDate",
}),
);
}
const temporalUsageNotice = detectTemporalUsageFromSearch(
query,
config?.filters,
);
// Reject top-level entity params - must use filters instead
rejectTopLevelEntityParams(config as Record<string, any>, "search");
@@ -1105,6 +1255,8 @@ export class Memory {
);
}
const searchStartMs = Date.now();
// Step 1: Preprocess query
const queryLemmatized = lemmatizeForBm25(query);
const queryEntities = extractEntities(query);
@@ -1280,9 +1432,41 @@ export class Memory {
};
});
return {
const result = {
results,
};
const searchElapsedMs = Date.now() - searchStartMs;
if (temporalUsageNotice) {
await this._displayTemporalUsageNotice({
triggerFunction: "search",
triggerSource: temporalUsageNotice.triggerSource,
triggerReason: temporalUsageNotice.triggerReason,
});
} else {
const scaleThresholdNotice = detectScaleThresholdFromTopK(topK);
if (scaleThresholdNotice) {
await this._displayScaleThresholdNotice({
triggerFunction: "search",
...scaleThresholdNotice,
});
} else {
const performanceSlowQueryNotice = detectPerformanceSlowQuery(
searchElapsedMs,
topK,
results.length,
);
if (performanceSlowQueryNotice) {
await this._displayPerformanceSlowQueryNotice({
triggerFunction: "search",
triggerReason: "slow_query",
...performanceSlowQueryNotice,
});
} else {
await this._displayFirstRunNotice("search");
}
}
}
return result;
}
async update(memoryId: string, data: string): Promise<{ message: string }> {
@@ -1290,14 +1474,28 @@ export class Memory {
await this._captureEvent("update", { memory_id: memoryId });
const embedding = await this.embedder.embed(data);
await this.updateMemory(memoryId, data, { [data]: embedding });
return { message: "Memory updated successfully!" };
const result = { message: "Memory updated successfully!" };
await this._displayFirstRunNotice("update");
return result;
}
async delete(memoryId: string): Promise<{ message: string }> {
await this._ensureInitialized();
await this._captureEvent("delete", { memory_id: memoryId });
await this.deleteMemory(memoryId);
return { message: "Memory deleted successfully!" };
const result = { message: "Memory deleted successfully!" };
const deleteCount = getDecayUsageDeleteCountAfterSuccess();
if (isDecayUsageDeleteEligible(deleteCount)) {
await this._displayDecayUsageNotice({
triggerFunction: "delete",
triggerSource: "delete_count",
triggerReason: "repeated_deletes",
deleteCount,
});
} else {
await this._displayFirstRunNotice("delete");
}
return result;
}
async deleteAll(
@@ -1328,12 +1526,25 @@ export class Memory {
await this.deleteMemory(memory.id);
}
return { message: "Memories deleted successfully!" };
const result = { message: "Memories deleted successfully!" };
if (memories.length > 0) {
await this._displayDecayUsageNotice({
triggerFunction: "delete_all",
triggerSource: "delete_all",
triggerReason: "bulk_delete",
deletedCount: memories.length,
});
} else {
await this._displayFirstRunNotice("delete_all");
}
return result;
}
async history(memoryId: string): Promise<any[]> {
await this._ensureInitialized();
return this.db.getHistory(memoryId);
const result = await this.db.getHistory(memoryId);
await this._displayFirstRunNotice("history");
return result;
}
async reset(): Promise<void> {
@@ -1385,6 +1596,7 @@ export class Memory {
console.error(this._initError);
});
await this._initPromise;
await this._displayFirstRunNotice("reset");
}
async getAll(config: GetAllMemoryOptions): Promise<SearchResult> {
@@ -1452,7 +1664,17 @@ export class Memory {
...(mem.payload.run_id && { run_id: mem.payload.run_id }),
}));
return { results };
const result = { results };
const scaleThresholdNotice = detectScaleThresholdFromTopK(topK);
if (scaleThresholdNotice) {
await this._displayScaleThresholdNotice({
triggerFunction: "get_all",
...scaleThresholdNotice,
});
} else {
await this._displayFirstRunNotice("get_all");
}
return result;
}
private async createMemory(
@@ -1500,20 +1722,13 @@ export class Memory {
existingEmbeddings[data] || (await this.embedder.embed(data));
const newMetadata = {
...existingMemory.payload,
...metadata,
data,
hash: createHash("md5").update(data).digest("hex"),
textLemmatized: lemmatizeForBm25(data),
createdAt: existingMemory.payload.createdAt,
updatedAt: new Date().toISOString(),
...(existingMemory.payload.user_id && {
user_id: existingMemory.payload.user_id,
}),
...(existingMemory.payload.agent_id && {
agent_id: existingMemory.payload.agent_id,
}),
...(existingMemory.payload.run_id && {
run_id: existingMemory.payload.run_id,
}),
};
await this.vectorStore.update(memoryId, embedding, newMetadata);
@@ -11,6 +11,7 @@ export interface AddMemoryOptions extends Entity {
metadata?: Record<string, any>;
filters?: SearchFilters;
infer?: boolean;
timestamp?: number | string | Date | null;
}
export interface SearchMemoryOptions {
@@ -18,6 +19,7 @@ export interface SearchMemoryOptions {
filters?: SearchFilters;
threshold?: number;
explain?: boolean;
referenceDate?: number | string | Date | null;
}
export interface GetAllMemoryOptions {
@@ -26,3 +28,8 @@ export interface GetAllMemoryOptions {
}
export interface DeleteAllMemoryOptions extends Entity {}
export interface UpdateProjectOptions {
decay?: boolean;
[key: string]: any;
}
+6
View File
@@ -49,6 +49,9 @@ export interface LLMConfig {
model?: string | any;
modelProperties?: Record<string, any>;
timeout?: number;
temperature?: number;
topP?: number;
maxTokens?: number;
}
export interface MemoryConfig {
@@ -131,6 +134,9 @@ export const MemoryConfigSchema = z.object({
baseURL: z.string().optional(),
url: z.string().optional(),
timeout: z.number().optional(),
temperature: z.number().optional(),
topP: z.number().optional(),
maxTokens: z.number().optional(),
}),
}),
historyDbPath: z.string().optional(),
File diff suppressed because it is too large Load Diff
+48 -6
View File
@@ -13,10 +13,12 @@ let version =
// Safely check for process.env in different environments
let MEM0_TELEMETRY = true;
try {
MEM0_TELEMETRY = process?.env?.MEM0_TELEMETRY === "false" ? false : true;
MEM0_TELEMETRY =
process?.env?.MEM0_TELEMETRY?.toLowerCase() === "false" ? false : true;
} catch (error) {}
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
const NOTICE_EVENT_NAME = "mem0.notice_displayed";
// 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.
@@ -35,7 +37,11 @@ const MEM0_TELEMETRY_SAMPLE_RATE: number = ((): number => {
})();
// Events that bypass sampling. Keep in sync with _captureEvent call sites in memory/index.ts.
const LIFECYCLE_EVENTS: ReadonlySet<string> = new Set(["init", "reset"]);
const ALWAYS_SEND_EVENTS: ReadonlySet<string> = new Set([
"init",
"reset",
"notice_displayed",
]);
class UnifiedTelemetry implements TelemetryClient {
private apiKey: string;
@@ -91,6 +97,10 @@ class UnifiedTelemetry implements TelemetryClient {
const telemetry = new UnifiedTelemetry(POSTHOG_API_KEY, POSTHOG_HOST);
function isTelemetryEnabled(): boolean {
return MEM0_TELEMETRY;
}
async function captureClientEvent(
eventName: string,
instance: TelemetryInstance,
@@ -102,8 +112,8 @@ async function captureClientEvent(
}
// >= 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) {
const alwaysSend = ALWAYS_SEND_EVENTS.has(eventName);
if (!alwaysSend && Math.random() >= MEM0_TELEMETRY_SAMPLE_RATE) {
return;
}
@@ -116,7 +126,7 @@ async function captureClientEvent(
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,
sample_rate: alwaysSend ? 1.0 : MEM0_TELEMETRY_SAMPLE_RATE,
};
await telemetry.captureEvent(
@@ -126,4 +136,36 @@ async function captureClientEvent(
);
}
export { telemetry, captureClientEvent };
async function captureNoticeEvent(
instance: TelemetryInstance,
properties: Record<string, any> = {},
) {
if (!instance.telemetryId) return;
const eventData: TelemetryEventData = {
function: `${instance.constructor.name}`,
method: "notice_displayed",
api_host: instance.host,
timestamp: new Date().toISOString(),
client_version: version,
client_source: "nodejs",
...properties,
sample_rate: 1.0,
};
await telemetry.captureEvent(
instance.telemetryId,
NOTICE_EVENT_NAME,
eventData,
);
}
export {
POSTHOG_API_KEY,
POSTHOG_HOST,
NOTICE_EVENT_NAME,
telemetry,
captureClientEvent,
captureNoticeEvent,
isTelemetryEnabled,
};
+263
View File
@@ -0,0 +1,263 @@
/// <reference types="jest" />
/**
* Anthropic LLM — unit tests (mocked @anthropic-ai/sdk).
*/
const mockCreate = jest.fn();
jest.mock("@anthropic-ai/sdk", () => {
return jest.fn().mockImplementation(() => ({
messages: { create: mockCreate },
}));
});
import { AnthropicLLM } from "../src/llms/anthropic";
describe("AnthropicLLM (unit)", () => {
beforeEach(() => mockCreate.mockClear());
it("returns text when no tools are provided and model returns a text block", async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: "text", text: '{"facts": ["fact1"]}' }],
});
const llm = new AnthropicLLM({ apiKey: "test-key" });
const result = await llm.generateResponse([
{ role: "user", content: "Hello" },
]);
expect(mockCreate).toHaveBeenCalledTimes(1);
expect(result).toBe('{"facts": ["fact1"]}');
// No tools → tool_choice must NOT be forwarded
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs.tool_choice).toBeUndefined();
});
// Bug #1 regression: bare string "auto" must NOT be sent; object form required
it("forwards tool_choice as { type: 'auto' } (not bare string) when tools are provided", async () => {
mockCreate.mockResolvedValueOnce({
content: [
{
type: "tool_use",
id: "toolu_1",
name: "add_graph_memory",
input: { source: "Alice", destination: "Bob" },
},
],
});
const tools = [
{
name: "add_graph_memory",
description: "Add a graph memory",
input_schema: { type: "object", properties: {} },
},
];
const llm = new AnthropicLLM({ apiKey: "test-key" });
await llm.generateResponse(
[{ role: "user", content: "Alice knows Bob" }],
undefined,
tools,
);
const callArgs = mockCreate.mock.calls[0][0];
// Must be object form, not a bare string
expect(callArgs.tool_choice).toEqual({ type: "auto" });
expect(callArgs.tool_choice).not.toBe("auto");
});
// Bug #2 regression: must NOT throw on a tool_use block
it("does NOT throw when the model returns a tool_use block", async () => {
mockCreate.mockResolvedValueOnce({
content: [
{
type: "tool_use",
id: "toolu_1",
name: "add_graph_memory",
input: { source: "Alice", destination: "Bob" },
},
],
});
const tools = [
{
name: "add_graph_memory",
description: "Add a graph memory",
input_schema: { type: "object", properties: {} },
},
];
const llm = new AnthropicLLM({ apiKey: "test-key" });
await expect(
llm.generateResponse(
[{ role: "user", content: "Alice knows Bob" }],
undefined,
tools,
),
).resolves.not.toThrow();
});
it("parses tool_use blocks into toolCalls with JSON-stringified arguments", async () => {
const inputObj = { source: "Alice", destination: "Bob" };
mockCreate.mockResolvedValueOnce({
content: [
{
type: "tool_use",
id: "toolu_1",
name: "add_graph_memory",
input: inputObj,
},
],
});
const tools = [
{
name: "add_graph_memory",
description: "Add a graph memory",
input_schema: { type: "object", properties: {} },
},
];
const llm = new AnthropicLLM({ apiKey: "test-key" });
const result = await llm.generateResponse(
[{ role: "user", content: "Alice knows Bob" }],
undefined,
tools,
);
expect(result).toHaveProperty("toolCalls");
const response = result as {
content: string;
role: string;
toolCalls: Array<{ name: string; arguments: string }>;
};
expect(response.toolCalls).toHaveLength(1);
expect(response.toolCalls[0].name).toBe("add_graph_memory");
expect(JSON.parse(response.toolCalls[0].arguments)).toEqual(inputObj);
});
it("handles a mixed text + tool_use response", async () => {
const inputObj = { source: "Alice", destination: "Bob" };
mockCreate.mockResolvedValueOnce({
content: [
{ type: "text", text: "Calling the tool now." },
{
type: "tool_use",
id: "toolu_2",
name: "add_graph_memory",
input: inputObj,
},
],
});
const tools = [
{
name: "add_graph_memory",
description: "Add a graph memory",
input_schema: { type: "object", properties: {} },
},
];
const llm = new AnthropicLLM({ apiKey: "test-key" });
const result = await llm.generateResponse(
[{ role: "user", content: "Alice knows Bob" }],
undefined,
tools,
);
expect(result).toHaveProperty("toolCalls");
const response = result as {
content: string;
role: string;
toolCalls: Array<{ name: string; arguments: string }>;
};
expect(response.content).toBe("Calling the tool now.");
expect(response.role).toBe("assistant");
expect(response.toolCalls).toHaveLength(1);
expect(response.toolCalls[0].name).toBe("add_graph_memory");
expect(JSON.parse(response.toolCalls[0].arguments)).toEqual(inputObj);
});
it("returns a structured response when tools are provided but the model returns only a text block", async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: "text", text: "Just a text response" }],
});
const tools = [
{
name: "noop",
description: "No operation",
input_schema: { type: "object", properties: {} },
},
];
const llm = new AnthropicLLM({ apiKey: "test-key" });
const result = await llm.generateResponse(
[{ role: "user", content: "Hello" }],
undefined,
tools,
);
expect(result).toEqual({
content: "Just a text response",
role: "assistant",
toolCalls: [],
});
});
// Parity with the Python provider's AnthropicConfig defaults:
// model claude-sonnet-4-6, max_tokens 2000, temperature 0.1, top_p omitted.
it("sends Python-parity defaults (model, max_tokens, temperature)", async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: "text", text: "ok" }],
});
const llm = new AnthropicLLM({ apiKey: "test-key" });
await llm.generateResponse([{ role: "user", content: "Hi" }]);
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs.model).toBe("claude-sonnet-4-6");
expect(callArgs.max_tokens).toBe(2000);
expect(callArgs.temperature).toBe(0.1);
expect(callArgs.top_p).toBeUndefined();
});
it("forwards maxTokens, temperature, and model from config", async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: "text", text: "ok" }],
});
const llm = new AnthropicLLM({
apiKey: "test-key",
model: "claude-opus-4-8",
maxTokens: 1024,
temperature: 0.7,
});
await llm.generateResponse([{ role: "user", content: "Hi" }]);
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs.model).toBe("claude-opus-4-8");
expect(callArgs.max_tokens).toBe(1024);
expect(callArgs.temperature).toBe(0.7);
});
// Anthropic rejects requests with both temperature and top_p set.
it("never sends both temperature and top_p (prefers temperature)", async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: "text", text: "ok" }],
});
const llm = new AnthropicLLM({
apiKey: "test-key",
temperature: 0.5,
topP: 0.9,
});
await llm.generateResponse([{ role: "user", content: "Hi" }]);
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs.temperature).toBe(0.5);
expect(callArgs.top_p).toBeUndefined();
});
});
@@ -418,6 +418,7 @@ describe("Memory – LM Studio end-to-end flow", () => {
}));
jest.doMock("../src/utils/telemetry", () => ({
captureClientEvent: jest.fn().mockResolvedValue(undefined),
isTelemetryEnabled: jest.fn(() => false),
}));
MemoryClass = require("../src/memory").Memory;
@@ -290,6 +290,7 @@ describe("Memory – auto-initialization", () => {
jest.doMock("../src/utils/telemetry", () => ({
captureClientEvent: jest.fn().mockResolvedValue(undefined),
isTelemetryEnabled: jest.fn(() => false),
}));
MemoryClass = require("../src/memory").Memory;
+15
View File
@@ -191,6 +191,21 @@ describe("Memory - update()", () => {
const after: MemoryItem | null = await memory.get(id);
expect(after!.hash).not.toBe(before!.hash);
});
test("preserves custom metadata fields after update", async () => {
const addResult: SearchResult = await memory.add("Original text", {
userId,
metadata: { category: "hobbies", priority: "high" },
infer: false,
});
const id = addResult.results[0].id;
await memory.update(id, "Updated text");
const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("Updated text");
expect(after!.metadata).toEqual(
expect.objectContaining({ category: "hobbies", priority: "high" }),
);
});
});
// ─── delete() ────────────────────────────────────────────
@@ -0,0 +1,387 @@
/// <reference types="jest" />
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
jest.setTimeout(15000);
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
const mockEmbedding = new Array(1536).fill(0.1);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
embeddingDims: 1536,
})),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest.fn(),
})),
}));
const DECAY_COPY =
'Memory decay requires Mem0 Platform. Get a free API key at https://app.mem0.ai?utm_source=oss_sdk&utm_medium=in_product&utm_campaign=decay_stub&utm_content=node_error and use `import MemoryClient from "mem0ai"`.';
const PLAIN_DECAY_ERROR =
"The decay parameter is not supported by the OSS Memory SDK.";
const PROJECT_UPDATE_ERROR =
"Project updates are not supported by the OSS Memory SDK.";
function makeTempMem0Dir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-node-decay-feature-"));
}
function decayPayload(overrides: Record<string, any> = {}) {
return {
notices: {
decay_stub: {
enabled: true,
notice_type: "error",
copy: DECAY_COPY,
...overrides,
},
},
};
}
function createFetchMock(options: {
variant?: string;
payload?: unknown;
failFlags?: boolean;
flagEnabled?: boolean;
}) {
const calls: any[] = [];
const fetchMock = jest.fn(async (url: string | URL, init?: RequestInit) => {
const target = String(url);
if (target.includes("/flags")) {
if (options.failFlags) {
throw new Error("flag evaluation failed");
}
return {
ok: true,
json: jest.fn().mockResolvedValue({
flags: {
"mem0-oss-notices": {
key: "mem0-oss-notices",
enabled: options.flagEnabled ?? true,
variant: options.variant ?? "displayed",
metadata: {
payload:
options.payload === undefined
? JSON.stringify(decayPayload())
: options.payload,
},
},
},
}),
};
}
if (target.includes("/i/v0/e/")) {
calls.push(JSON.parse(String(init?.body)));
return {
ok: true,
text: jest.fn().mockResolvedValue(""),
};
}
return {
ok: true,
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue(""),
};
});
return { fetchMock, calls };
}
function noticeEvents(calls: any[]) {
return calls.filter((call) => call.event === "mem0.notice_displayed");
}
async function createMemory() {
const { Memory } = await import("../src/memory");
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-decay-feature-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
describe("Node OSS decay feature error notice", () => {
let originalMem0Dir: string | undefined;
let originalTelemetry: string | undefined;
let originalSampleRate: string | undefined;
let originalFetch: typeof global.fetch;
beforeEach(() => {
originalMem0Dir = process.env.MEM0_DIR;
originalTelemetry = process.env.MEM0_TELEMETRY;
originalSampleRate = process.env.MEM0_TELEMETRY_SAMPLE_RATE;
originalFetch = global.fetch;
process.env.MEM0_DIR = makeTempMem0Dir();
process.env.MEM0_TELEMETRY = "true";
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "1";
jest.resetModules();
});
afterEach(() => {
if (originalMem0Dir === undefined) delete process.env.MEM0_DIR;
else process.env.MEM0_DIR = originalMem0Dir;
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = originalTelemetry;
if (originalSampleRate === undefined) {
delete process.env.MEM0_TELEMETRY_SAMPLE_RATE;
} else {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = originalSampleRate;
}
global.fetch = originalFetch;
jest.restoreAllMocks();
jest.resetModules();
});
it("raises CTA copy for displayed and emits displayed=true", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
DECAY_COPY,
);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_stub",
notice_type: "error",
flag_key: "mem0-oss-notices",
variant: "displayed",
displayed: true,
payload: DECAY_COPY,
notice_config_found: true,
sync_type: "async",
trigger_function: "update_project",
trigger_parameter: "decay",
sample_rate: 1,
}),
);
});
it("raises CTA copy for holdout and emits displayed=true", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "holdout" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
DECAY_COPY,
);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_stub",
variant: "holdout",
displayed: true,
trigger_function: "update_project",
trigger_parameter: "decay",
}),
);
expect(notices[0].properties.bypass_reason).toBeUndefined();
});
it("uses plain error for unknown future variants and emits not_displayed", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "silent" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
PLAIN_DECAY_ERROR,
);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_stub",
variant: "silent",
displayed: false,
bypass_reason: "not_displayed",
payload: DECAY_COPY,
}),
);
});
it("uses plain error for disabled payload and emits payload_disabled", async () => {
const { fetchMock, calls } = createFetchMock({
variant: "holdout",
payload: JSON.stringify(decayPayload({ enabled: false })),
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
PLAIN_DECAY_ERROR,
);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_stub",
displayed: false,
bypass_reason: "payload_disabled",
disabled_reason: "payload_disabled",
notice_config_found: true,
}),
);
});
it.each([
[
"missing config",
JSON.stringify({ notices: {} }),
"missing_notice_config",
],
[
"missing copy",
JSON.stringify(decayPayload({ copy: "" })),
"missing_copy",
],
["malformed payload", "{not-json", "missing_notice_config"],
])("uses plain error for %s", async (_label, payload, bypassReason) => {
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
PLAIN_DECAY_ERROR,
);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_stub",
displayed: false,
bypass_reason: bypassReason,
}),
);
});
it("uses plain error and emits no event when the blunt flag is disabled", async () => {
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
flagEnabled: false,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
PLAIN_DECAY_ERROR,
);
expect(noticeEvents(calls)).toHaveLength(0);
});
it("uses plain error and emits no event when PostHog fails", async () => {
const { fetchMock, calls } = createFetchMock({ failFlags: true });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
PLAIN_DECAY_ERROR,
);
expect(noticeEvents(calls)).toHaveLength(0);
});
it("uses plain error and skips flag evaluation when telemetry is off", async () => {
process.env.MEM0_TELEMETRY = "False";
jest.resetModules();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
PLAIN_DECAY_ERROR,
);
expect(fetchMock).not.toHaveBeenCalled();
expect(noticeEvents(calls)).toHaveLength(0);
});
it.each([
["empty options", {}],
["decay false", { decay: false }],
["non-decay options", { customInstructions: "Updated" }],
])("does not emit notice telemetry for %s", async (_label, options) => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject(options)).rejects.toThrow(
PROJECT_UPDATE_ERROR,
);
expect(
fetchMock.mock.calls.filter(([url]) => String(url).includes("/flags")),
).toHaveLength(0);
expect(noticeEvents(calls)).toHaveLength(0);
});
it("does not trigger first-run notice or update_project telemetry", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.updateProject({ decay: true })).rejects.toThrow(
DECAY_COPY,
);
const eventNames = calls.map((call) => call.event);
expect(eventNames).toContain("mem0.notice_displayed");
expect(eventNames).not.toContain("mem0.update_project");
expect(
noticeEvents(calls).some(
(call) => call.properties.notice_id === "first_run",
),
).toBe(false);
});
});
@@ -0,0 +1,526 @@
/// <reference types="jest" />
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
jest.setTimeout(15000);
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
const mockEmbedding = new Array(1536).fill(0.1);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
embeddingDims: 1536,
})),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest.fn(),
})),
}));
const DECAY_USAGE_COPY =
"Tip: Python fallback copy with memory.project.update(decay=True).";
function makeTempMem0Dir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-node-decay-usage-"));
}
function configPath(): string {
return path.join(process.env.MEM0_DIR as string, "config.json");
}
function readConfig(): Record<string, any> {
const file = configPath();
if (!fs.existsSync(file)) return {};
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function writeConfig(config: Record<string, any>) {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), JSON.stringify(config, null, 4));
}
function consumeFirstRun() {
writeConfig({
user_id: "node-decay-usage-test-user",
notice_state: {
first_run: {
consumed: true,
trigger_function: "test_setup",
variant: "test",
},
},
});
}
function decayUsagePayload(overrides: Record<string, any> = {}) {
return {
notices: {
decay_usage: {
enabled: true,
notice_type: "log_line",
copy: DECAY_USAGE_COPY,
...overrides,
},
},
};
}
function createFetchMock(options: {
variant?: string;
payload?: unknown;
failFlags?: boolean;
flagEnabled?: boolean;
}) {
const calls: any[] = [];
const fetchMock = jest.fn(async (url: string | URL, init?: RequestInit) => {
const target = String(url);
if (target.includes("/flags")) {
if (options.failFlags) {
throw new Error("flag evaluation failed");
}
const payload =
options.payload === undefined
? JSON.stringify(decayUsagePayload())
: options.payload;
return {
ok: true,
json: jest.fn().mockResolvedValue({
flags: {
"mem0-oss-notices": {
key: "mem0-oss-notices",
enabled: options.flagEnabled ?? true,
variant: options.variant ?? "displayed",
metadata: { payload },
},
},
}),
};
}
if (target.includes("/i/v0/e/")) {
calls.push(JSON.parse(String(init?.body)));
return {
ok: true,
text: jest.fn().mockResolvedValue(""),
};
}
return {
ok: true,
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue(""),
};
});
return { fetchMock, calls };
}
function noticeEvents(calls: any[]) {
return calls.filter((call) => call.event === "mem0.notice_displayed");
}
function flagRequestCount(fetchMock: jest.Mock) {
return fetchMock.mock.calls.filter(([url]) => String(url).includes("/flags"))
.length;
}
async function createMemory() {
const { Memory } = await import("../src/memory");
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-decay-usage-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
async function addMemories(memory: any, userId: string, count: number) {
const ids: string[] = [];
for (let index = 0; index < count; index++) {
const result = await memory.add(`Decay usage memory ${index}`, {
userId,
infer: false,
});
ids.push(result.results[0].id);
}
return ids;
}
describe("Node OSS decay usage notice", () => {
let originalMem0Dir: string | undefined;
let originalTelemetry: string | undefined;
let originalSampleRate: string | undefined;
let originalFetch: typeof global.fetch;
let stderrSpy: jest.SpyInstance;
beforeEach(() => {
originalMem0Dir = process.env.MEM0_DIR;
originalTelemetry = process.env.MEM0_TELEMETRY;
originalSampleRate = process.env.MEM0_TELEMETRY_SAMPLE_RATE;
originalFetch = global.fetch;
process.env.MEM0_DIR = makeTempMem0Dir();
process.env.MEM0_TELEMETRY = "true";
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "1";
stderrSpy = jest
.spyOn(process.stderr, "write")
.mockImplementation(() => true);
jest.resetModules();
});
afterEach(() => {
if (originalMem0Dir === undefined) delete process.env.MEM0_DIR;
else process.env.MEM0_DIR = originalMem0Dir;
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = originalTelemetry;
if (originalSampleRate === undefined) {
delete process.env.MEM0_TELEMETRY_SAMPLE_RATE;
} else {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = originalSampleRate;
}
global.fetch = originalFetch;
jest.restoreAllMocks();
jest.resetModules();
});
it("does not evaluate or write decay state before the 5th successful delete", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
const ids = await addMemories(memory, "decay-delete-user", 4);
for (const id of ids) {
await memory.delete(id);
}
expect(flagRequestCount(fetchMock)).toBe(0);
expect(noticeEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.decay_usage).toBeUndefined();
expect(stderrSpy).not.toHaveBeenCalledWith(
expect.stringContaining(DECAY_USAGE_COPY),
);
});
it("evaluates on the 5th successful delete and records delete_count", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
const ids = await addMemories(memory, "decay-delete-user", 5);
for (const id of ids) {
await memory.delete(id);
}
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_usage",
notice_type: "log_line",
flag_key: "mem0-oss-notices",
variant: "displayed",
displayed: true,
payload: DECAY_USAGE_COPY,
notice_config_found: true,
sync_type: "async",
trigger_function: "delete",
trigger_source: "delete_count",
trigger_reason: "repeated_deletes",
delete_count: 5,
sample_rate: 1,
}),
);
expect(stderrSpy.mock.calls.flat().join("")).toContain(DECAY_USAGE_COPY);
expect(readConfig().notice_state.decay_usage.events).toHaveLength(1);
});
it("does not count or emit when delete fails", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
jest
.spyOn(memory as any, "deleteMemory")
.mockRejectedValue(new Error("delete failed"));
await expect(memory.delete("memory-id")).rejects.toThrow("delete failed");
expect(flagRequestCount(fetchMock)).toBe(0);
expect(noticeEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.decay_usage).toBeUndefined();
});
it("evaluates deleteAll after deleting at least one memory", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addMemories(memory, "decay-delete-all-user", 3);
await memory.deleteAll({ userId: "decay-delete-all-user" });
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_usage",
variant: "displayed",
displayed: true,
payload: DECAY_USAGE_COPY,
trigger_function: "delete_all",
trigger_source: "delete_all",
trigger_reason: "bulk_delete",
deleted_count: 3,
}),
);
});
it("does not evaluate deleteAll when no memories are deleted", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.deleteAll({ userId: "empty-delete-all-user" });
expect(flagRequestCount(fetchMock)).toBe(0);
expect(noticeEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.decay_usage).toBeUndefined();
});
it("does not evaluate when deleteAll fails", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.deleteAll({} as any)).rejects.toThrow(
"At least one filter is required",
);
expect(flagRequestCount(fetchMock)).toBe(0);
expect(noticeEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.decay_usage).toBeUndefined();
});
it("is silent for holdout and emits displayed=false", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "holdout" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addMemories(memory, "decay-holdout-user", 3);
await memory.deleteAll({ userId: "decay-holdout-user" });
expect(stderrSpy).not.toHaveBeenCalledWith(
expect.stringContaining(DECAY_USAGE_COPY),
);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_usage",
variant: "holdout",
displayed: false,
bypass_reason: "holdout",
trigger_function: "delete_all",
}),
);
});
it.each([
[
"disabled payload",
JSON.stringify(decayUsagePayload({ enabled: false })),
"payload_disabled",
"payload_disabled",
],
[
"missing config",
JSON.stringify({ notices: {} }),
"missing_notice_config",
undefined,
],
[
"missing copy",
JSON.stringify({
notices: {
decay_usage: { enabled: true, notice_type: "log_line" },
},
}),
"missing_copy",
undefined,
],
["malformed payload", "{not-json", "missing_notice_config", undefined],
])(
"stays silent and emits safe bypass for %s",
async (_label, payload, bypassReason, disabledReason) => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await addMemories(memory, "decay-bypass-user", 3);
await memory.deleteAll({ userId: "decay-bypass-user" });
expect(stderrSpy).not.toHaveBeenCalledWith(
expect.stringContaining(DECAY_USAGE_COPY),
);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "decay_usage",
displayed: false,
bypass_reason: bypassReason,
...(disabledReason && { disabled_reason: disabledReason }),
}),
);
},
);
it("does not emit or consume cap when the blunt flag is disabled", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
flagEnabled: false,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await addMemories(memory, "decay-flag-disabled-user", 3);
await memory.deleteAll({ userId: "decay-flag-disabled-user" });
expect(noticeEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.decay_usage).toBeUndefined();
});
it("does not emit or consume cap when PostHog fails", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ failFlags: true });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addMemories(memory, "decay-posthog-failure-user", 3);
await memory.deleteAll({ userId: "decay-posthog-failure-user" });
expect(noticeEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.decay_usage).toBeUndefined();
});
it("skips flag evaluation, event emission, and state writes when telemetry is off", async () => {
process.env.MEM0_TELEMETRY = "False";
jest.resetModules();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addMemories(memory, "decay-telemetry-off-user", 3);
await memory.deleteAll({ userId: "decay-telemetry-off-user" });
expect(fetchMock).not.toHaveBeenCalled();
expect(noticeEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.decay_usage).toBeUndefined();
});
it("blocks the 11th evaluated opportunity before flag evaluation", async () => {
consumeFirstRun();
const now = new Date();
writeConfig({
...readConfig(),
notice_state: {
...readConfig().notice_state,
first_run: readConfig().notice_state.first_run,
decay_usage: {
events: Array.from({ length: 10 }, (_, index) => ({
evaluated_at: new Date(now.getTime() - index * 1000).toISOString(),
variant: "displayed",
})),
},
},
});
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addMemories(memory, "decay-cap-user", 1);
await memory.deleteAll({ userId: "decay-cap-user" });
expect(flagRequestCount(fetchMock)).toBe(0);
expect(noticeEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state.decay_usage.events).toHaveLength(10);
});
it("does not consume first-run on a qualifying decay usage call", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload: JSON.stringify({
notices: {
first_run: {
enabled: true,
notice_type: "log_line",
copy: "First-run should not appear",
},
decay_usage: {
enabled: true,
notice_type: "log_line",
copy: DECAY_USAGE_COPY,
},
},
}),
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await addMemories(memory, "decay-priority-user", 3);
const config = readConfig();
delete config.notice_state.first_run;
writeConfig(config);
await memory.deleteAll({ userId: "decay-priority-user" });
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties.notice_id).toBe("decay_usage");
expect(readConfig().notice_state.first_run).toBeUndefined();
expect(readConfig().notice_state.decay_usage.events).toHaveLength(1);
});
});
@@ -0,0 +1,409 @@
/// <reference types="jest" />
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
jest.setTimeout(15000);
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest.fn().mockResolvedValue(
JSON.stringify({
memory: [{ id: "0", text: "stored fact", attributed_to: "user" }],
}),
),
})),
}));
const mockEmbedding = new Array(1536).fill(0.1);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
embeddingDims: 1536,
})),
}));
const FIRST_RUN_COPY = "First-run CTA from PostHog";
function makeTempMem0Dir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-node-first-run-"));
}
function firstRunPayload(overrides: Record<string, any> = {}) {
return {
notices: {
first_run: {
enabled: true,
notice_type: "log_line",
copy: FIRST_RUN_COPY,
...overrides,
},
},
};
}
function createFetchMock(options: {
variant?: string;
payload?: Record<string, any>;
failFlags?: boolean;
}) {
const calls: any[] = [];
const fetchMock = jest.fn(async (url: string | URL, init?: RequestInit) => {
const target = String(url);
if (target.includes("/flags")) {
if (options.failFlags) {
throw new Error("flag evaluation failed");
}
return {
ok: true,
json: jest.fn().mockResolvedValue({
flags: {
"mem0-oss-notices": {
key: "mem0-oss-notices",
enabled: true,
variant: options.variant ?? "displayed",
metadata: {
payload: JSON.stringify(options.payload ?? firstRunPayload()),
},
},
},
}),
};
}
if (target.includes("/i/v0/e/")) {
calls.push(JSON.parse(String(init?.body)));
return {
ok: true,
text: jest.fn().mockResolvedValue(""),
};
}
return {
ok: true,
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue(""),
};
});
return { fetchMock, calls };
}
function noticeEvents(calls: any[]) {
return calls.filter((call) => call.event === "mem0.notice_displayed");
}
async function createMemory() {
const { Memory } = await import("../src/memory");
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-first-run-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
describe("Node OSS first-run notice", () => {
let originalMem0Dir: string | undefined;
let originalTelemetry: string | undefined;
let originalSampleRate: string | undefined;
let originalFetch: typeof global.fetch;
let stderrSpy: jest.SpyInstance;
beforeEach(() => {
originalMem0Dir = process.env.MEM0_DIR;
originalTelemetry = process.env.MEM0_TELEMETRY;
originalSampleRate = process.env.MEM0_TELEMETRY_SAMPLE_RATE;
originalFetch = global.fetch;
process.env.MEM0_DIR = makeTempMem0Dir();
process.env.MEM0_TELEMETRY = "true";
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "1";
stderrSpy = jest
.spyOn(process.stderr, "write")
.mockImplementation(() => true);
jest.resetModules();
});
afterEach(() => {
if (originalMem0Dir === undefined) delete process.env.MEM0_DIR;
else process.env.MEM0_DIR = originalMem0Dir;
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = originalTelemetry;
if (originalSampleRate === undefined) {
delete process.env.MEM0_TELEMETRY_SAMPLE_RATE;
} else {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = originalSampleRate;
}
global.fetch = originalFetch;
jest.restoreAllMocks();
jest.resetModules();
});
it("shows the displayed first-run copy once after a successful public call", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Direct storage content", {
userId: "first-run-user",
infer: false,
});
await memory.add("Second direct storage content", {
userId: "first-run-user",
infer: false,
});
const stderrOutput = stderrSpy.mock.calls.flat().join("");
expect(stderrOutput.match(new RegExp(FIRST_RUN_COPY, "g"))).toHaveLength(1);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "first_run",
notice_type: "log_line",
flag_key: "mem0-oss-notices",
variant: "displayed",
displayed: true,
payload: FIRST_RUN_COPY,
notice_config_found: true,
sync_type: "async",
trigger_function: "add",
sample_rate: 1,
}),
);
const { __noticeTestHooks } = await import("../src/utils/notices");
const config = __noticeTestHooks.loadMem0Config();
expect(config.notice_state.first_run).toEqual(
expect.objectContaining({
consumed: true,
trigger_function: "add",
variant: "displayed",
}),
);
});
it("treats missing enabled as enabled, matching Python payload semantics", async () => {
const payload = firstRunPayload();
delete (payload.notices.first_run as Record<string, any>).enabled;
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Missing enabled payload content", {
userId: "first-run-missing-enabled",
infer: false,
});
const stderrOutput = stderrSpy.mock.calls.flat().join("");
expect(stderrOutput).toContain(FIRST_RUN_COPY);
expect(noticeEvents(calls)[0].properties).toEqual(
expect.objectContaining({
notice_id: "first_run",
displayed: true,
payload: FIRST_RUN_COPY,
}),
);
});
it("is silent for holdout but still emits and consumes the opportunity", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "holdout" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Holdout content", {
userId: "first-run-holdout",
infer: false,
});
expect(stderrSpy).not.toHaveBeenCalledWith(
expect.stringContaining(FIRST_RUN_COPY),
);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "first_run",
variant: "holdout",
displayed: false,
bypass_reason: "holdout",
notice_config_found: true,
}),
);
});
it("is silent for disabled payloads and records a safe bypass", async () => {
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload: firstRunPayload({ enabled: false }),
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Disabled payload content", {
userId: "first-run-disabled",
infer: false,
});
const stderrOutput = stderrSpy.mock.calls.flat().join("");
expect(stderrOutput).not.toContain(FIRST_RUN_COPY);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "first_run",
displayed: false,
bypass_reason: "payload_disabled",
disabled_reason: "payload_disabled",
notice_config_found: true,
}),
);
});
it("lets the payload disabled kill switch take precedence over holdout", async () => {
const { fetchMock, calls } = createFetchMock({
variant: "holdout",
payload: firstRunPayload({ enabled: false }),
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Disabled holdout payload content", {
userId: "first-run-disabled-holdout",
infer: false,
});
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "first_run",
variant: "holdout",
displayed: false,
bypass_reason: "payload_disabled",
disabled_reason: "payload_disabled",
notice_config_found: true,
}),
);
});
it("does not evaluate flags, emit, or write state when telemetry is off", async () => {
process.env.MEM0_TELEMETRY = "false";
jest.resetModules();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Telemetry off content", {
userId: "first-run-off",
infer: false,
});
expect(fetchMock).not.toHaveBeenCalled();
expect(noticeEvents(calls)).toHaveLength(0);
const { __noticeTestHooks } = await import("../src/utils/notices");
expect(__noticeTestHooks.loadMem0Config().notice_state).toBeUndefined();
});
it("does not export internal notice test hooks from the public OSS entrypoint", async () => {
const publicOssEntry = await import("../src");
expect(publicOssEntry).not.toHaveProperty("__noticeTestHooks");
});
it("treats uppercase False as telemetry off", async () => {
process.env.MEM0_TELEMETRY = "False";
jest.resetModules();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Uppercase telemetry off content", {
userId: "first-run-off-uppercase",
infer: false,
});
expect(fetchMock).not.toHaveBeenCalled();
expect(noticeEvents(calls)).toHaveLength(0);
const { __noticeTestHooks } = await import("../src/utils/notices");
expect(__noticeTestHooks.loadMem0Config().notice_state).toBeUndefined();
});
it("does not run after a failed public call", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.add("Missing owner", {} as any)).rejects.toThrow(
"One of the filters: userId, agentId or runId is required!",
);
expect(
fetchMock.mock.calls.filter(([url]) => String(url).includes("/flags")),
).toHaveLength(0);
expect(noticeEvents(calls)).toHaveLength(0);
});
it("does not break the successful operation when flag evaluation fails", async () => {
const { fetchMock, calls } = createFetchMock({ failFlags: true });
global.fetch = fetchMock as any;
const memory = await createMemory();
const result = await memory.add("Flag failure content", {
userId: "first-run-failure",
infer: false,
});
expect(result.results).toHaveLength(1);
expect(noticeEvents(calls)).toHaveLength(0);
});
it("can trigger from get() when get is the first successful public call", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(memory.get("missing-memory-id")).resolves.toBeNull();
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties.trigger_function).toBe("get");
});
});
@@ -0,0 +1,227 @@
/// <reference types="jest" />
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import type { TelemetryInstance } from "../src/utils/telemetry.types";
function makeTempMem0Dir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-node-notices-"));
}
function makeInstance(
overrides: Partial<TelemetryInstance> = {},
): TelemetryInstance {
return {
telemetryId: "notice-test-user",
constructor: { name: "Memory" },
host: "https://test.example.com",
...overrides,
};
}
describe("Node OSS notice foundation", () => {
let originalMem0Dir: string | undefined;
let originalTelemetry: string | undefined;
let originalFetch: typeof global.fetch;
beforeEach(() => {
originalMem0Dir = process.env.MEM0_DIR;
originalTelemetry = process.env.MEM0_TELEMETRY;
originalFetch = global.fetch;
process.env.MEM0_DIR = makeTempMem0Dir();
process.env.MEM0_TELEMETRY = "true";
jest.resetModules();
});
afterEach(() => {
if (originalMem0Dir === undefined) delete process.env.MEM0_DIR;
else process.env.MEM0_DIR = originalMem0Dir;
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = originalTelemetry;
global.fetch = originalFetch;
jest.restoreAllMocks();
jest.resetModules();
});
it("writes notice state into isolated MEM0_DIR config and preserves user_id", async () => {
const notices = await import("../src/utils/notices");
const configPath = notices.getMem0ConfigPath();
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, JSON.stringify({ user_id: "existing-user" }));
expect(
notices.recordNoticeOpportunity("foundation_notice", {
variant: "displayed",
}),
).toBe(true);
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
expect(config.user_id).toBe("existing-user");
expect(config.notice_state.foundation_notice.events).toHaveLength(1);
});
it("writes config through a temp-file path and leaves no temp file behind", async () => {
const notices = await import("../src/utils/notices");
expect(notices.writeMem0ConfigAtomic({ user_id: "atomic-user" })).toBe(
true,
);
const configDir = path.dirname(notices.getMem0ConfigPath());
const tempFiles = fs
.readdirSync(configDir)
.filter((name) => name.endsWith(".tmp"));
expect(tempFiles).toHaveLength(0);
expect(notices.loadMem0Config().user_id).toBe("atomic-user");
});
it("allows 10 evaluated opportunities in a rolling window and blocks the 11th", async () => {
const notices = await import("../src/utils/notices");
const now = new Date("2026-06-11T12:00:00.000Z");
let state: Record<string, any> = {};
for (let i = 0; i < 10; i++) {
const nextState = notices.appendNoticeCapEvent(
state,
{ variant: "displayed", index: i },
{ now: new Date(now.getTime() + i) },
);
expect(nextState).not.toBeNull();
state = nextState!;
}
expect(notices.hasNoticeCapRoom(state, { now })).toBe(false);
expect(
notices.appendNoticeCapEvent(state, { variant: "displayed" }, { now }),
).toBeNull();
});
it("drops old cap events outside the rolling window", async () => {
const notices = await import("../src/utils/notices");
const oldEvent = {
evaluated_at: "2026-06-01T00:00:00.000Z",
variant: "displayed",
};
const now = new Date("2026-06-11T12:00:00.000Z");
expect(notices.hasNoticeCapRoom({ events: [oldEvent] }, { now })).toBe(
true,
);
const nextState = notices.appendNoticeCapEvent(
{ events: [oldEvent] },
{ variant: "displayed" },
{ now },
);
expect(nextState?.events).toHaveLength(1);
});
it("does not evaluate flags or write notice state when telemetry is off", async () => {
process.env.MEM0_TELEMETRY = "false";
jest.resetModules();
const fetchMock = jest.fn();
global.fetch = fetchMock as any;
const notices = await import("../src/utils/notices");
await expect(
notices.evaluateNoticeFlag("notice-test-user", { fetchImpl: fetchMock }),
).resolves.toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
expect(
notices.recordNoticeOpportunity("foundation_notice", {
variant: "displayed",
}),
).toBe(false);
expect(fs.existsSync(notices.getMem0ConfigPath())).toBe(false);
});
it("returns null when PostHog flag evaluation fails", async () => {
const notices = await import("../src/utils/notices");
const fetchMock = jest.fn().mockRejectedValue(new Error("network down"));
await expect(
notices.evaluateNoticeFlag("notice-test-user", { fetchImpl: fetchMock }),
).resolves.toBeNull();
});
it("returns null when PostHog flag evaluation times out", async () => {
const notices = await import("../src/utils/notices");
const fetchMock = jest.fn(
(_url: string | URL | Request, init?: RequestInit) =>
new Promise((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => {
reject(new Error("aborted"));
});
}),
);
await expect(
notices.evaluateNoticeFlag("notice-test-user", {
fetchImpl: fetchMock as any,
timeoutMs: 1,
}),
).resolves.toBeNull();
});
it("parses displayed variant and JSON payload from PostHog flags response", async () => {
const notices = await import("../src/utils/notices");
const payload = {
notices: {
foundation_notice: {
enabled: true,
notice_type: "log_line",
copy: "Foundation notice",
},
},
};
const fetchMock = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({
flags: {
"mem0-oss-notices": {
key: "mem0-oss-notices",
enabled: true,
variant: "displayed",
metadata: { payload: JSON.stringify(payload) },
},
},
}),
});
const result = await notices.evaluateNoticeFlag("notice-test-user", {
fetchImpl: fetchMock,
});
expect(result?.variant).toBe("displayed");
const parsed = notices.getNoticeConfigFromPayload(
result?.payload,
"foundation_notice",
);
expect(parsed.found).toBe(true);
expect(parsed.config?.copy).toBe("Foundation notice");
});
it("emits mem0.notice_displayed with sample_rate=1", async () => {
const fetchMock = jest.fn().mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue(""),
});
global.fetch = fetchMock as any;
const notices = await import("../src/utils/notices");
await notices.emitNoticeDisplayed(makeInstance(), {
notice_id: "foundation_notice",
notice_type: "log_line",
flag_key: "mem0-oss-notices",
variant: "displayed",
displayed: true,
});
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.event).toBe("mem0.notice_displayed");
expect(body.properties.sample_rate).toBe(1);
expect(body.properties.notice_id).toBe("foundation_notice");
});
});
@@ -0,0 +1,492 @@
/// <reference types="jest" />
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
jest.setTimeout(15000);
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
const mockEmbedding = new Array(1536).fill(0.1);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
embeddingDims: 1536,
})),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest.fn(),
})),
}));
const PERFORMANCE_COPY =
"Mem0 Platform is optimized for this type of workload. Its retrieval benchmarked at ~0.8-1.09s p50 across LoCoMo, LongMemEval, and BEAM 1M/10M workloads; you can use it for free by getting an API key at: https://app.mem0.ai";
function makeTempMem0Dir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-node-performance-"));
}
function configPath(): string {
return path.join(process.env.MEM0_DIR as string, "config.json");
}
function readConfig(): Record<string, any> {
const file = configPath();
if (!fs.existsSync(file)) return {};
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function writeConfig(config: Record<string, any>) {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), JSON.stringify(config, null, 4));
}
function consumeFirstRun() {
writeConfig({
user_id: "node-performance-test-user",
notice_state: {
first_run: {
consumed: true,
trigger_function: "test_setup",
variant: "test",
},
},
});
}
function performancePayload(overrides: Record<string, any> = {}) {
return {
notices: {
performance_slow_query: {
enabled: true,
notice_type: "log_line",
copy: PERFORMANCE_COPY,
...overrides,
},
},
};
}
function createFetchMock(options: {
variant?: string;
payload?: unknown;
failFlags?: boolean;
flagEnabled?: boolean;
}) {
const calls: any[] = [];
const fetchMock = jest.fn(async (url: string | URL, init?: RequestInit) => {
const target = String(url);
if (target.includes("/flags")) {
if (options.failFlags) {
throw new Error("flag evaluation failed");
}
const payload =
options.payload === undefined
? JSON.stringify(performancePayload())
: options.payload;
return {
ok: true,
json: jest.fn().mockResolvedValue({
flags: {
"mem0-oss-notices": {
key: "mem0-oss-notices",
enabled: options.flagEnabled ?? true,
variant: options.variant ?? "displayed",
metadata: { payload },
},
},
}),
};
}
if (target.includes("/i/v0/e/")) {
calls.push(JSON.parse(String(init?.body)));
return {
ok: true,
text: jest.fn().mockResolvedValue(""),
};
}
return {
ok: true,
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue(""),
};
});
return { fetchMock, calls };
}
function noticeEvents(calls: any[]) {
return calls.filter((call) => call.event === "mem0.notice_displayed");
}
function performanceEvents(calls: any[]) {
return noticeEvents(calls).filter(
(call) => call.properties.notice_id === "performance_slow_query",
);
}
function flagRequestCount(fetchMock: jest.Mock) {
return fetchMock.mock.calls.filter(([url]) => String(url).includes("/flags"))
.length;
}
function mockSearchElapsed(elapsedMs: number) {
let now = 1000;
jest.spyOn(Date, "now").mockImplementation(() => {
const current = now;
now += elapsedMs;
return current;
});
}
async function createMemory() {
const { Memory } = await import("../src/memory");
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-performance-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
async function addSeed(memory: any, userId = "performance-user") {
await memory.add("The user's favorite drink is green tea.", {
userId,
infer: false,
});
}
describe("Node OSS performance slow query notice", () => {
let originalMem0Dir: string | undefined;
let originalTelemetry: string | undefined;
let originalSampleRate: string | undefined;
let originalFetch: typeof global.fetch;
let stderrSpy: jest.SpyInstance;
beforeEach(() => {
originalMem0Dir = process.env.MEM0_DIR;
originalTelemetry = process.env.MEM0_TELEMETRY;
originalSampleRate = process.env.MEM0_TELEMETRY_SAMPLE_RATE;
originalFetch = global.fetch;
process.env.MEM0_DIR = makeTempMem0Dir();
process.env.MEM0_TELEMETRY = "true";
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "1";
stderrSpy = jest
.spyOn(process.stderr, "write")
.mockImplementation(() => true);
jest.resetModules();
});
afterEach(() => {
if (originalMem0Dir === undefined) delete process.env.MEM0_DIR;
else process.env.MEM0_DIR = originalMem0Dir;
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = originalTelemetry;
if (originalSampleRate === undefined) {
delete process.env.MEM0_TELEMETRY_SAMPLE_RATE;
} else {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = originalSampleRate;
}
global.fetch = originalFetch;
jest.restoreAllMocks();
jest.resetModules();
});
it("displays and emits safe fields after a slow successful search", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
mockSearchElapsed(2345);
const result = await memory.search("favorite drink private text", {
filters: { user_id: "performance-user" },
topK: 3,
});
expect(result.results.length).toBeGreaterThan(0);
expect(stderrSpy).toHaveBeenCalledWith(`${PERFORMANCE_COPY}\n`);
const event = performanceEvents(calls)[0];
expect(event.properties).toEqual(
expect.objectContaining({
notice_id: "performance_slow_query",
notice_type: "log_line",
flag_key: "mem0-oss-notices",
variant: "displayed",
displayed: true,
payload: PERFORMANCE_COPY,
notice_config_found: true,
sync_type: "async",
trigger_function: "search",
trigger_reason: "slow_query",
elapsed_ms: 2345,
threshold_ms: 2000,
top_k: 3,
result_count: result.results.length,
sample_rate: 1,
}),
);
const serialized = JSON.stringify(event.properties);
expect(serialized).not.toContain("private text");
expect(serialized).not.toContain("performance-user");
});
it("is silent for holdout and emits displayed=false", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "holdout" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
mockSearchElapsed(2345);
await memory.search("favorite drink", {
filters: { user_id: "performance-user" },
topK: 3,
});
expect(stderrSpy).not.toHaveBeenCalled();
expect(performanceEvents(calls)[0].properties).toEqual(
expect.objectContaining({
notice_id: "performance_slow_query",
variant: "holdout",
displayed: false,
bypass_reason: "holdout",
}),
);
});
it.each([
[
"disabled payload",
JSON.stringify(performancePayload({ enabled: false })),
"payload_disabled",
],
[
"missing config",
JSON.stringify({ notices: {} }),
"missing_notice_config",
],
[
"missing copy",
JSON.stringify(performancePayload({ copy: "" })),
"missing_copy",
],
["malformed payload", "{not-json", "missing_notice_config"],
])("is silent and safe for %s", async (_label, payload, bypassReason) => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
mockSearchElapsed(2345);
await memory.search("favorite drink", {
filters: { user_id: "performance-user" },
topK: 3,
});
expect(stderrSpy).not.toHaveBeenCalled();
expect(performanceEvents(calls)[0].properties).toEqual(
expect.objectContaining({
displayed: false,
bypass_reason: bypassReason,
}),
);
});
it("does not evaluate fast searches", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
mockSearchElapsed(100);
await memory.search("favorite drink", {
filters: { user_id: "performance-user" },
topK: 3,
});
expect(flagRequestCount(fetchMock)).toBe(0);
expect(performanceEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.performance_slow_query).toBeUndefined();
});
it("does not evaluate failed searches", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
mockSearchElapsed(2345);
await expect(
memory.search("private slow query", {
filters: {},
topK: 3,
}),
).rejects.toThrow("filters must contain");
expect(flagRequestCount(fetchMock)).toBe(0);
expect(performanceEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.performance_slow_query).toBeUndefined();
});
it("does not consume cap or emit when the blunt flag is disabled", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
flagEnabled: false,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
mockSearchElapsed(2345);
await memory.search("favorite drink", {
filters: { user_id: "performance-user" },
topK: 3,
});
expect(performanceEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.performance_slow_query).toBeUndefined();
});
it("does not consume cap or emit when PostHog fails", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ failFlags: true });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
mockSearchElapsed(2345);
await memory.search("favorite drink", {
filters: { user_id: "performance-user" },
topK: 3,
});
expect(performanceEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.performance_slow_query).toBeUndefined();
});
it("does nothing when telemetry is off", async () => {
process.env.MEM0_TELEMETRY = "false";
jest.resetModules();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
mockSearchElapsed(2345);
await memory.search("favorite drink", {
filters: { user_id: "performance-user" },
topK: 3,
});
expect(fetchMock).not.toHaveBeenCalled();
expect(performanceEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state).toBeUndefined();
});
it("caps evaluated opportunities at 10 per rolling week", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
mockSearchElapsed(2345);
for (let index = 1; index <= 11; index++) {
await memory.search(`favorite drink ${index}`, {
filters: { user_id: "performance-user" },
topK: 3,
});
}
expect(performanceEvents(calls)).toHaveLength(10);
expect(flagRequestCount(fetchMock)).toBe(10);
expect(stderrSpy).toHaveBeenCalledTimes(10);
expect(
readConfig().notice_state.performance_slow_query.events,
).toHaveLength(10);
});
it("lets temporal usage and scale threshold take priority", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
mockSearchElapsed(2345);
await memory.search("what happened last week?", {
filters: { user_id: "performance-user" },
topK: 3,
});
await memory.search("favorite drink", {
filters: { user_id: "performance-user" },
topK: 50,
});
expect(performanceEvents(calls)).toHaveLength(0);
const noticeIds = noticeEvents(calls).map(
(call) => call.properties.notice_id,
);
expect(noticeIds).toEqual(["temporal_usage", "scale_threshold"]);
});
it("does not consume first-run on the same qualifying performance call", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
mockSearchElapsed(2345);
await memory.search("favorite drink", {
filters: { user_id: "performance-user" },
topK: 3,
});
expect(performanceEvents(calls)).toHaveLength(1);
expect(readConfig().notice_state.first_run).toBeUndefined();
});
});
@@ -0,0 +1,463 @@
/// <reference types="jest" />
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
jest.setTimeout(15000);
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
const mockEmbedding = new Array(1536).fill(0.1);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
embeddingDims: 1536,
})),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest.fn(),
})),
}));
const SCALE_TOP_K_COPY = "Scale top {top_k}";
const SCALE_MEMORY_COUNT_COPY = "Scale count {memory_count}";
function makeTempMem0Dir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-node-scale-"));
}
function configPath(): string {
return path.join(process.env.MEM0_DIR as string, "config.json");
}
function readConfig(): Record<string, any> {
const file = configPath();
if (!fs.existsSync(file)) return {};
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function writeConfig(config: Record<string, any>) {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), JSON.stringify(config, null, 4));
}
function consumeFirstRun(scaleState: Record<string, any> = {}) {
writeConfig({
user_id: "node-scale-test-user",
notice_state: {
first_run: {
consumed: true,
trigger_function: "test_setup",
variant: "test",
},
...(Object.keys(scaleState).length > 0 && {
scale_threshold: scaleState,
}),
},
});
}
function scalePayload(overrides: Record<string, any> = {}) {
return {
notices: {
scale_threshold: {
enabled: true,
notice_type: "log_line",
copies: {
top_k: SCALE_TOP_K_COPY,
memory_count: SCALE_MEMORY_COUNT_COPY,
},
...overrides,
},
},
};
}
function createFetchMock(options: {
variant?: string;
payload?: unknown;
failFlags?: boolean;
flagEnabled?: boolean;
}) {
const calls: any[] = [];
const fetchMock = jest.fn(async (url: string | URL, init?: RequestInit) => {
const target = String(url);
if (target.includes("/flags")) {
if (options.failFlags) {
throw new Error("flag evaluation failed");
}
const payload =
options.payload === undefined
? JSON.stringify(scalePayload())
: options.payload;
return {
ok: true,
json: jest.fn().mockResolvedValue({
flags: {
"mem0-oss-notices": {
key: "mem0-oss-notices",
enabled: options.flagEnabled ?? true,
variant: options.variant ?? "displayed",
metadata: { payload },
},
},
}),
};
}
if (target.includes("/i/v0/e/")) {
calls.push(JSON.parse(String(init?.body)));
return {
ok: true,
text: jest.fn().mockResolvedValue(""),
};
}
return {
ok: true,
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue(""),
};
});
return { fetchMock, calls };
}
function noticeEvents(calls: any[]) {
return calls.filter((call) => call.event === "mem0.notice_displayed");
}
function scaleEvents(calls: any[]) {
return noticeEvents(calls).filter(
(call) => call.properties.notice_id === "scale_threshold",
);
}
function flagRequestCount(fetchMock: jest.Mock) {
return fetchMock.mock.calls.filter(([url]) => String(url).includes("/flags"))
.length;
}
async function createMemory() {
const { Memory } = await import("../src/memory");
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-scale-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
async function addSeed(memory: any, userId = "scale-user") {
await memory.add("The user's favorite drink is green tea.", {
userId,
infer: false,
});
}
describe("Node OSS scale threshold notice", () => {
let originalMem0Dir: string | undefined;
let originalTelemetry: string | undefined;
let originalSampleRate: string | undefined;
let originalFetch: typeof global.fetch;
let stderrSpy: jest.SpyInstance;
beforeEach(() => {
originalMem0Dir = process.env.MEM0_DIR;
originalTelemetry = process.env.MEM0_TELEMETRY;
originalSampleRate = process.env.MEM0_TELEMETRY_SAMPLE_RATE;
originalFetch = global.fetch;
process.env.MEM0_DIR = makeTempMem0Dir();
process.env.MEM0_TELEMETRY = "true";
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "1";
stderrSpy = jest
.spyOn(process.stderr, "write")
.mockImplementation(() => true);
jest.resetModules();
});
afterEach(() => {
if (originalMem0Dir === undefined) delete process.env.MEM0_DIR;
else process.env.MEM0_DIR = originalMem0Dir;
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = originalTelemetry;
if (originalSampleRate === undefined) {
delete process.env.MEM0_TELEMETRY_SAMPLE_RATE;
} else {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = originalSampleRate;
}
global.fetch = originalFetch;
jest.restoreAllMocks();
jest.resetModules();
});
it("detects high topK after successful search", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
const result = await memory.search("favorite drink private text", {
filters: { user_id: "scale-user" },
topK: 50,
});
expect(result.results.length).toBeGreaterThan(0);
expect(stderrSpy).toHaveBeenCalledWith("Scale top 50\n");
const event = scaleEvents(calls)[0];
expect(event.properties).toMatchObject({
notice_id: "scale_threshold",
displayed: true,
trigger_function: "search",
trigger_source: "top_k",
trigger_reason: "high_top_k",
top_k: 50,
threshold: 50,
sample_rate: 1,
});
expect(JSON.stringify(event.properties)).not.toContain("private text");
});
it("detects high topK after successful getAll", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
const result = await memory.getAll({
filters: { user_id: "scale-user" },
topK: 50,
});
expect(result.results.length).toBeGreaterThan(0);
expect(stderrSpy).toHaveBeenCalledWith("Scale top 50\n");
const event = scaleEvents(calls)[0];
expect(event.properties).toMatchObject({
notice_id: "scale_threshold",
displayed: true,
trigger_function: "get_all",
trigger_source: "top_k",
trigger_reason: "high_top_k",
top_k: 50,
threshold: 50,
});
});
it("does not evaluate topK below the threshold", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
await memory.search("favorite drink", {
filters: { user_id: "scale-user" },
topK: 49,
});
expect(flagRequestCount(fetchMock)).toBe(0);
expect(scaleEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.scale_threshold).toBeUndefined();
});
it("marks memory-count threshold evaluated before PostHog display", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ failFlags: true });
global.fetch = fetchMock as any;
const memory = await createMemory();
(memory as any).vectorStore.count = jest
.fn()
.mockResolvedValue({ count: 2000 });
await memory.add("Scale count threshold fixture.", {
userId: "scale-user",
infer: false,
});
expect((memory as any).vectorStore.count).toHaveBeenCalledTimes(1);
expect(
readConfig().notice_state.scale_threshold
.memory_count_threshold_evaluated,
).toBe(true);
expect(scaleEvents(calls)).toHaveLength(0);
});
it("does not count provider memories once threshold was evaluated", async () => {
consumeFirstRun({ memory_count_threshold_evaluated: true });
const { fetchMock } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
(memory as any).vectorStore.count = jest
.fn()
.mockResolvedValue({ count: 2000 });
await memory.add("Scale count already evaluated fixture.", {
userId: "scale-user",
infer: false,
});
expect((memory as any).vectorStore.count).not.toHaveBeenCalled();
expect(flagRequestCount(fetchMock)).toBe(0);
});
it("throttles under-threshold provider counts", async () => {
consumeFirstRun();
const { fetchMock } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
(memory as any).vectorStore.count = jest
.fn()
.mockResolvedValue({ count: 1999 });
await memory.add("Scale count under threshold one.", {
userId: "scale-user",
infer: false,
});
await memory.add("Scale count under threshold two.", {
userId: "scale-user",
infer: false,
});
expect((memory as any).vectorStore.count).toHaveBeenCalledTimes(1);
expect(flagRequestCount(fetchMock)).toBe(0);
});
it("records holdout and disabled variants silently", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({
variant: "holdout",
payload: JSON.stringify(scalePayload({ enabled: false })),
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
await memory.search("favorite drink", {
filters: { user_id: "scale-user" },
topK: 50,
});
expect(stderrSpy).not.toHaveBeenCalled();
const event = scaleEvents(calls)[0];
expect(event.properties).toMatchObject({
displayed: false,
bypass_reason: "payload_disabled",
disabled_reason: "payload_disabled",
variant: "holdout",
});
});
it("treats missing enabled as enabled for scale payloads", async () => {
consumeFirstRun();
const payload = scalePayload();
delete (payload.notices.scale_threshold as Record<string, any>).enabled;
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload: JSON.stringify(payload),
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
await memory.search("favorite drink", {
filters: { user_id: "scale-user" },
topK: 50,
});
expect(stderrSpy.mock.calls.flat().join("")).toContain("Scale top 50");
expect(scaleEvents(calls)[0].properties).toMatchObject({
notice_id: "scale_threshold",
displayed: true,
payload: "Scale top 50",
});
});
it("caps evaluated scale opportunities at 10 per week", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSeed(memory);
stderrSpy.mockClear();
for (let index = 0; index < 11; index++) {
await memory.search(`favorite drink ${index}`, {
filters: { user_id: "scale-user" },
topK: 50,
});
}
expect(scaleEvents(calls)).toHaveLength(10);
expect(flagRequestCount(fetchMock)).toBe(10);
expect(stderrSpy).toHaveBeenCalledTimes(10);
expect(readConfig().notice_state.scale_threshold.events).toHaveLength(10);
});
it("does not consume first-run on the same qualifying scale call", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.getAll({
filters: { user_id: "scale-user" },
topK: 50,
});
expect(scaleEvents(calls)).toHaveLength(1);
expect(readConfig().notice_state?.first_run).toBeUndefined();
});
it("telemetry off skips flag evaluation, event emission, and state writes", async () => {
process.env.MEM0_TELEMETRY = "false";
jest.resetModules();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.getAll({
filters: { user_id: "scale-user" },
topK: 50,
});
expect(flagRequestCount(fetchMock)).toBe(0);
expect(scaleEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state).toBeUndefined();
});
});
@@ -0,0 +1,516 @@
/// <reference types="jest" />
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
jest.setTimeout(15000);
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
const mockEmbedding = new Array(1536).fill(0.1);
const mockEmbed = jest.fn().mockResolvedValue(mockEmbedding);
const mockEmbedBatch = jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: mockEmbed,
embedBatch: mockEmbedBatch,
embeddingDims: 1536,
})),
}));
const mockGenerateResponse = jest.fn();
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: mockGenerateResponse,
})),
}));
const TEMPORAL_COPY =
"Temporal reasoning requires a Mem0 API key. Get one for free at https://app.mem0.ai";
const PLAIN_TIMESTAMP_ERROR =
"The timestamp parameter is not supported by the OSS Memory SDK.";
const PLAIN_REFERENCE_DATE_ERROR =
"The referenceDate parameter is not supported by the OSS Memory SDK.";
function makeTempMem0Dir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-node-temporal-feature-"));
}
function temporalPayload(overrides: Record<string, any> = {}) {
return {
notices: {
temporal_stub: {
enabled: true,
notice_type: "error",
copy: TEMPORAL_COPY,
...overrides,
},
},
};
}
function createFetchMock(options: {
variant?: string;
payload?: unknown;
failFlags?: boolean;
flagEnabled?: boolean;
}) {
const calls: any[] = [];
const fetchMock = jest.fn(async (url: string | URL, init?: RequestInit) => {
const target = String(url);
if (target.includes("/flags")) {
if (options.failFlags) {
throw new Error("flag evaluation failed");
}
return {
ok: true,
json: jest.fn().mockResolvedValue({
flags: {
"mem0-oss-notices": {
key: "mem0-oss-notices",
enabled: options.flagEnabled ?? true,
variant: options.variant ?? "displayed",
metadata: {
payload:
options.payload === undefined
? JSON.stringify(temporalPayload())
: options.payload,
},
},
},
}),
};
}
if (target.includes("/i/v0/e/")) {
calls.push(JSON.parse(String(init?.body)));
return {
ok: true,
text: jest.fn().mockResolvedValue(""),
};
}
return {
ok: true,
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue(""),
};
});
return { fetchMock, calls };
}
function noticeEvents(calls: any[]) {
return calls.filter((call) => call.event === "mem0.notice_displayed");
}
async function createMemory() {
const { Memory } = await import("../src/memory");
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-temporal-feature-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
describe("Node OSS temporal feature error notice", () => {
let originalMem0Dir: string | undefined;
let originalTelemetry: string | undefined;
let originalSampleRate: string | undefined;
let originalFetch: typeof global.fetch;
beforeEach(() => {
originalMem0Dir = process.env.MEM0_DIR;
originalTelemetry = process.env.MEM0_TELEMETRY;
originalSampleRate = process.env.MEM0_TELEMETRY_SAMPLE_RATE;
originalFetch = global.fetch;
process.env.MEM0_DIR = makeTempMem0Dir();
process.env.MEM0_TELEMETRY = "true";
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "1";
mockEmbed.mockClear();
mockEmbedBatch.mockClear();
mockGenerateResponse.mockClear();
jest.resetModules();
});
afterEach(() => {
if (originalMem0Dir === undefined) delete process.env.MEM0_DIR;
else process.env.MEM0_DIR = originalMem0Dir;
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = originalTelemetry;
if (originalSampleRate === undefined) {
delete process.env.MEM0_TELEMETRY_SAMPLE_RATE;
} else {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = originalSampleRate;
}
global.fetch = originalFetch;
jest.restoreAllMocks();
jest.resetModules();
});
it.each([
["add", "timestamp"],
["search", "referenceDate"],
])(
"raises CTA copy for displayed %s(%s) and emits displayed=true",
async (triggerFunction, triggerParameter) => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
if (triggerFunction === "add") {
await expect(
memory.add("Temporal add", {
userId: "temporal-user",
timestamp: 1778112000,
}),
).rejects.toThrow(TEMPORAL_COPY);
} else {
await expect(
memory.search("Temporal search", {
filters: { user_id: "temporal-user" },
referenceDate: "2026-05-06",
}),
).rejects.toThrow(TEMPORAL_COPY);
}
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_stub",
notice_type: "error",
flag_key: "mem0-oss-notices",
variant: "displayed",
displayed: true,
payload: TEMPORAL_COPY,
notice_config_found: true,
sync_type: "async",
trigger_function: triggerFunction,
trigger_parameter: triggerParameter,
sample_rate: 1,
}),
);
},
);
it.each([
["add", "timestamp"],
["search", "referenceDate"],
])(
"raises CTA copy for holdout %s(%s) and emits displayed=true",
async (triggerFunction, triggerParameter) => {
const { fetchMock, calls } = createFetchMock({ variant: "holdout" });
global.fetch = fetchMock as any;
const memory = await createMemory();
if (triggerFunction === "add") {
await expect(
memory.add("Temporal add", {
userId: "temporal-user",
timestamp: null,
}),
).rejects.toThrow(TEMPORAL_COPY);
} else {
await expect(
memory.search("Temporal search", {
filters: { user_id: "temporal-user" },
referenceDate: null,
}),
).rejects.toThrow(TEMPORAL_COPY);
}
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_stub",
variant: "holdout",
displayed: true,
trigger_function: triggerFunction,
trigger_parameter: triggerParameter,
}),
);
expect(notices[0].properties.bypass_reason).toBeUndefined();
},
);
it("uses plain error for unknown future variants and emits not_displayed", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "silent" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.add("Temporal add", {
userId: "temporal-user",
timestamp: 1778112000,
}),
).rejects.toThrow(PLAIN_TIMESTAMP_ERROR);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_stub",
variant: "silent",
displayed: false,
bypass_reason: "not_displayed",
payload: TEMPORAL_COPY,
}),
);
});
it("treats missing enabled as enabled for feature-error payloads", async () => {
const payload = temporalPayload();
delete (payload.notices.temporal_stub as Record<string, any>).enabled;
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload: JSON.stringify(payload),
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.add("Temporal add", {
userId: "temporal-user",
timestamp: 1778112000,
}),
).rejects.toThrow(TEMPORAL_COPY);
expect(noticeEvents(calls)[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_stub",
displayed: true,
payload: TEMPORAL_COPY,
}),
);
});
it("uses timestamp plain error for disabled payload and emits payload_disabled", async () => {
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload: JSON.stringify(temporalPayload({ enabled: false })),
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.add("Temporal add", {
userId: "temporal-user",
timestamp: 1778112000,
}),
).rejects.toThrow(PLAIN_TIMESTAMP_ERROR);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_stub",
displayed: false,
bypass_reason: "payload_disabled",
disabled_reason: "payload_disabled",
notice_config_found: true,
trigger_function: "add",
trigger_parameter: "timestamp",
}),
);
});
it.each([
[
"missing config",
JSON.stringify({ notices: {} }),
"missing_notice_config",
],
[
"missing copy",
JSON.stringify(temporalPayload({ copy: "" })),
"missing_copy",
],
["malformed payload", "{not-json", "missing_notice_config"],
])(
"uses referenceDate plain error for %s",
async (_label, payload, bypassReason) => {
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.search("Temporal search", {
filters: { user_id: "temporal-user" },
referenceDate: "2026-05-06",
}),
).rejects.toThrow(PLAIN_REFERENCE_DATE_ERROR);
const notices = noticeEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_stub",
displayed: false,
bypass_reason: bypassReason,
trigger_function: "search",
trigger_parameter: "referenceDate",
}),
);
},
);
it("uses plain error and emits no event when the blunt flag is disabled", async () => {
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
flagEnabled: false,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.add("Temporal add", {
userId: "temporal-user",
timestamp: 1778112000,
}),
).rejects.toThrow(PLAIN_TIMESTAMP_ERROR);
expect(noticeEvents(calls)).toHaveLength(0);
});
it("uses plain error and emits no event when PostHog fails", async () => {
const { fetchMock, calls } = createFetchMock({ failFlags: true });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.search("Temporal search", {
filters: { user_id: "temporal-user" },
referenceDate: "2026-05-06",
}),
).rejects.toThrow(PLAIN_REFERENCE_DATE_ERROR);
expect(noticeEvents(calls)).toHaveLength(0);
});
it("uses plain error and skips flag evaluation when telemetry is off", async () => {
process.env.MEM0_TELEMETRY = "False";
jest.resetModules();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.add("Temporal add", {
userId: "temporal-user",
timestamp: 1778112000,
}),
).rejects.toThrow(PLAIN_TIMESTAMP_ERROR);
expect(fetchMock).not.toHaveBeenCalled();
expect(noticeEvents(calls)).toHaveLength(0);
});
it("throws before add validation, normal telemetry, embeddings, and first-run", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
const addToVectorStoreSpy = jest.spyOn(memory as any, "addToVectorStore");
await expect(
memory.add(
undefined as any,
{
timestamp: 1778112000,
} as any,
),
).rejects.toThrow(TEMPORAL_COPY);
expect(mockEmbed).not.toHaveBeenCalled();
expect(addToVectorStoreSpy).not.toHaveBeenCalled();
expect(calls.map((call) => call.event)).not.toContain("mem0.add");
expect(
noticeEvents(calls).some(
(call) => call.properties.notice_id === "first_run",
),
).toBe(false);
});
it("throws before search validation, normal telemetry, embeddings, and first-run", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.search("Temporal search", {
userId: "invalid-top-level-user",
referenceDate: "2026-05-06",
} as any),
).rejects.toThrow(TEMPORAL_COPY);
expect(mockEmbed).not.toHaveBeenCalled();
expect(calls.map((call) => call.event)).not.toContain("mem0.search");
expect(
noticeEvents(calls).some(
(call) => call.properties.notice_id === "first_run",
),
).toBe(false);
});
it("leaves normal add/search calls unchanged when temporal options are omitted", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "holdout" });
global.fetch = fetchMock as any;
const memory = await createMemory();
const addResult = await memory.add("Normal add", {
userId: "normal-user",
infer: false,
});
expect(addResult.results).toHaveLength(1);
const searchResult = await memory.search("Normal add", {
filters: { user_id: "normal-user" },
topK: 3,
});
expect(searchResult.results.length).toBeGreaterThanOrEqual(0);
const eventNames = calls.map((call) => call.event);
expect(eventNames).toContain("mem0.add");
expect(eventNames).toContain("mem0.search");
expect(
noticeEvents(calls).some(
(call) => call.properties.notice_id === "temporal_stub",
),
).toBe(false);
});
});
@@ -0,0 +1,594 @@
/// <reference types="jest" />
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
jest.setTimeout(15000);
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
const mockEmbedding = new Array(1536).fill(0.1);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
embeddingDims: 1536,
})),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest.fn(),
})),
}));
const TEMPORAL_USAGE_COPY =
"This looks like a time-aware memory workflow. Mem0 Platform has temporal reasoning built in. Use `timestamp` when adding memories and `reference_date` when searching.";
function makeTempMem0Dir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-node-temporal-usage-"));
}
function configPath(): string {
return path.join(process.env.MEM0_DIR as string, "config.json");
}
function readConfig(): Record<string, any> {
const file = configPath();
if (!fs.existsSync(file)) return {};
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function writeConfig(config: Record<string, any>) {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), JSON.stringify(config, null, 4));
}
function consumeFirstRun() {
writeConfig({
user_id: "node-temporal-usage-test-user",
notice_state: {
first_run: {
consumed: true,
trigger_function: "test_setup",
variant: "test",
},
},
});
}
function temporalUsagePayload(overrides: Record<string, any> = {}) {
return {
notices: {
temporal_usage: {
enabled: true,
notice_type: "log_line",
copy: TEMPORAL_USAGE_COPY,
...overrides,
},
},
};
}
function createFetchMock(options: {
variant?: string;
payload?: unknown;
failFlags?: boolean;
flagEnabled?: boolean;
}) {
const calls: any[] = [];
const fetchMock = jest.fn(async (url: string | URL, init?: RequestInit) => {
const target = String(url);
if (target.includes("/flags")) {
if (options.failFlags) {
throw new Error("flag evaluation failed");
}
const payload =
options.payload === undefined
? JSON.stringify(temporalUsagePayload())
: options.payload;
return {
ok: true,
json: jest.fn().mockResolvedValue({
flags: {
"mem0-oss-notices": {
key: "mem0-oss-notices",
enabled: options.flagEnabled ?? true,
variant: options.variant ?? "displayed",
metadata: { payload },
},
},
}),
};
}
if (target.includes("/i/v0/e/")) {
calls.push(JSON.parse(String(init?.body)));
return {
ok: true,
text: jest.fn().mockResolvedValue(""),
};
}
return {
ok: true,
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue(""),
};
});
return { fetchMock, calls };
}
function noticeEvents(calls: any[]) {
return calls.filter((call) => call.event === "mem0.notice_displayed");
}
function temporalUsageEvents(calls: any[]) {
return noticeEvents(calls).filter(
(call) => call.properties.notice_id === "temporal_usage",
);
}
function flagRequestCount(fetchMock: jest.Mock) {
return fetchMock.mock.calls.filter(([url]) => String(url).includes("/flags"))
.length;
}
async function createMemory() {
const { Memory } = await import("../src/memory");
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-temporal-usage-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
async function addSearchSeed(memory: any, userId = "temporal-user") {
await memory.add("The user's favorite drink is green tea.", {
userId,
infer: false,
});
}
describe("Node OSS temporal usage notice", () => {
let originalMem0Dir: string | undefined;
let originalTelemetry: string | undefined;
let originalSampleRate: string | undefined;
let originalFetch: typeof global.fetch;
let stderrSpy: jest.SpyInstance;
beforeEach(() => {
originalMem0Dir = process.env.MEM0_DIR;
originalTelemetry = process.env.MEM0_TELEMETRY;
originalSampleRate = process.env.MEM0_TELEMETRY_SAMPLE_RATE;
originalFetch = global.fetch;
process.env.MEM0_DIR = makeTempMem0Dir();
process.env.MEM0_TELEMETRY = "true";
process.env.MEM0_TELEMETRY_SAMPLE_RATE = "1";
stderrSpy = jest
.spyOn(process.stderr, "write")
.mockImplementation(() => true);
jest.resetModules();
});
afterEach(() => {
if (originalMem0Dir === undefined) delete process.env.MEM0_DIR;
else process.env.MEM0_DIR = originalMem0Dir;
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = originalTelemetry;
if (originalSampleRate === undefined) {
delete process.env.MEM0_TELEMETRY_SAMPLE_RATE;
} else {
process.env.MEM0_TELEMETRY_SAMPLE_RATE = originalSampleRate;
}
global.fetch = originalFetch;
jest.restoreAllMocks();
jest.resetModules();
});
it("detects timestamp-like metadata after successful add", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
const result = await memory.add("Temporal metadata memory", {
userId: "temporal-user",
infer: false,
metadata: { event_date: "2025-04-09" },
});
expect(result.results).toHaveLength(1);
expect(stderrSpy).toHaveBeenCalledWith(`${TEMPORAL_USAGE_COPY}\n`);
const notices = temporalUsageEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_usage",
notice_type: "log_line",
flag_key: "mem0-oss-notices",
variant: "displayed",
displayed: true,
payload: TEMPORAL_USAGE_COPY,
notice_config_found: true,
sync_type: "async",
trigger_function: "add",
trigger_source: "metadata",
trigger_reason: "date_like_metadata",
sample_rate: 1,
}),
);
expect(readConfig().notice_state.temporal_usage.events).toHaveLength(1);
});
it("emits query trigger fields after successful search", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSearchSeed(memory);
await memory.search("what happened last week?", {
filters: { user_id: "temporal-user" },
topK: 3,
});
const notices = temporalUsageEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_usage",
displayed: true,
trigger_function: "search",
trigger_source: "query",
trigger_reason: "relative_phrase",
}),
);
});
it("detects temporal range filters after successful search", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSearchSeed(memory);
await memory.search("favorite drink", {
filters: {
user_id: "temporal-user",
AND: [{ created_at: { gte: "2025-04-01" } }],
},
topK: 3,
});
const notices = temporalUsageEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_usage",
displayed: true,
trigger_function: "search",
trigger_source: "filter",
trigger_reason: "date_range_filter",
}),
);
});
it("does not evaluate for normal non-temporal add/search calls", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Normal memory", {
userId: "normal-user",
infer: false,
metadata: { category: "planning" },
});
await memory.search("favorite drink", {
filters: { user_id: "normal-user" },
topK: 3,
});
expect(flagRequestCount(fetchMock)).toBe(0);
expect(temporalUsageEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.temporal_usage).toBeUndefined();
});
it("does not evaluate when add/search fail", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await expect(
memory.add("Invalid temporal metadata add", {
infer: false,
metadata: { event_date: "2025-04-09" },
} as any),
).rejects.toThrow("One of the filters");
await expect(
memory.search("what happened last week?", {
filters: {},
}),
).rejects.toThrow("filters must contain");
expect(temporalUsageEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.temporal_usage).toBeUndefined();
});
it("is silent for holdout and emits displayed=false", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "holdout" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Temporal metadata memory", {
userId: "temporal-user",
infer: false,
metadata: { event_date: "2025-04-09" },
});
expect(stderrSpy).not.toHaveBeenCalled();
const notices = temporalUsageEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_usage",
variant: "holdout",
displayed: false,
bypass_reason: "holdout",
trigger_source: "metadata",
trigger_reason: "date_like_metadata",
}),
);
});
it.each([
[
"disabled payload",
JSON.stringify(temporalUsagePayload({ enabled: false })),
"payload_disabled",
],
[
"missing config",
JSON.stringify({ notices: {} }),
"missing_notice_config",
],
[
"missing copy",
JSON.stringify(temporalUsagePayload({ copy: "" })),
"missing_copy",
],
["malformed payload", "{not-json", "missing_notice_config"],
])("is silent and safe for %s", async (_label, payload, bypassReason) => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
payload,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Temporal metadata memory", {
userId: "temporal-user",
infer: false,
metadata: { event_date: "2025-04-09" },
});
expect(stderrSpy).not.toHaveBeenCalled();
const notices = temporalUsageEvents(calls);
expect(notices).toHaveLength(1);
expect(notices[0].properties).toEqual(
expect.objectContaining({
notice_id: "temporal_usage",
displayed: false,
bypass_reason: bypassReason,
}),
);
});
it("does not consume cap or emit when the blunt flag is disabled", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({
variant: "displayed",
flagEnabled: false,
});
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Temporal metadata memory", {
userId: "temporal-user",
infer: false,
metadata: { event_date: "2025-04-09" },
});
expect(temporalUsageEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.temporal_usage).toBeUndefined();
});
it("does not consume cap or emit when PostHog fails", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ failFlags: true });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Temporal metadata memory", {
userId: "temporal-user",
infer: false,
metadata: { event_date: "2025-04-09" },
});
expect(temporalUsageEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state?.temporal_usage).toBeUndefined();
});
it("does nothing when telemetry is off", async () => {
process.env.MEM0_TELEMETRY = "False";
jest.resetModules();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Temporal metadata memory", {
userId: "temporal-user",
infer: false,
metadata: { event_date: "2025-04-09" },
});
expect(fetchMock).not.toHaveBeenCalled();
expect(temporalUsageEvents(calls)).toHaveLength(0);
expect(readConfig().notice_state).toBeUndefined();
});
it("caps evaluated opportunities at 10 per rolling week", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSearchSeed(memory);
for (let index = 1; index <= 11; index++) {
await memory.search(`what happened last week ${index}?`, {
filters: { user_id: "temporal-user" },
topK: 3,
});
}
expect(temporalUsageEvents(calls)).toHaveLength(10);
expect(flagRequestCount(fetchMock)).toBe(10);
expect(readConfig().notice_state.temporal_usage.events).toHaveLength(10);
expect(stderrSpy).toHaveBeenCalledTimes(10);
});
it("does not consume first-run on the same qualifying temporal usage call", async () => {
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await memory.add("Temporal metadata memory", {
userId: "temporal-user",
infer: false,
metadata: { event_date: "2025-04-09" },
});
expect(readConfig().notice_state.first_run).toBeUndefined();
await memory.add("Normal follow-up memory", {
userId: "temporal-user",
infer: false,
});
const notices = noticeEvents(calls);
expect(notices.map((call) => call.properties.notice_id)).toEqual([
"temporal_usage",
"first_run",
]);
expect(readConfig().notice_state.first_run.consumed).toBe(true);
});
it("does not include raw query, metadata, or filter values in telemetry props", async () => {
consumeFirstRun();
const { fetchMock, calls } = createFetchMock({ variant: "displayed" });
global.fetch = fetchMock as any;
const memory = await createMemory();
await addSearchSeed(memory);
await memory.search("private trip since 2025-04-09", {
filters: {
user_id: "temporal-user",
created_at: { gte: "2025-04-09" },
},
topK: 3,
});
const props = temporalUsageEvents(calls)[0].properties;
const serialized = JSON.stringify(props);
expect(serialized).not.toContain("private trip");
expect(serialized).not.toContain("2025-04-09");
expect(serialized).not.toContain("temporal-user");
});
it("detects expected query, metadata, and filter cases conservatively", async () => {
const { __noticeTestHooks } = await import("../src/utils/notices");
expect(
__noticeTestHooks.detectTemporalUsageFromSearch("notes from today", null),
).toEqual({ triggerSource: "query", triggerReason: "relative_phrase" });
expect(
__noticeTestHooks.detectTemporalUsageFromSearch(
"notes from 2025-04-09",
null,
),
).toEqual({ triggerSource: "query", triggerReason: "date_like_query" });
expect(
__noticeTestHooks.detectTemporalUsageFromMetadata({
event_date: "2025-04-09",
}),
).toEqual({
triggerSource: "metadata",
triggerReason: "date_like_metadata",
});
expect(
__noticeTestHooks.detectTemporalUsageFromSearch("favorite drink", {
AND: [{ created_at: { gte: "2025-04-01" } }],
}),
).toEqual({ triggerSource: "filter", triggerReason: "date_range_filter" });
expect(
__noticeTestHooks.detectTemporalUsageFromSearch("favorite drink", null),
).toBeNull();
expect(
__noticeTestHooks.detectTemporalUsageFromMetadata({
category: "planning",
}),
).toBeNull();
});
it("detectors never raise for cyclic metadata or filters", async () => {
const { __noticeTestHooks } = await import("../src/utils/notices");
const metadata: Record<string, any> = {};
metadata.self = metadata;
const filters: Record<string, any> = {};
filters.AND = [filters];
expect(
__noticeTestHooks.detectTemporalUsageFromMetadata(metadata),
).toBeNull();
expect(
__noticeTestHooks.detectTemporalUsageFromSearch(
"favorite drink",
filters,
),
).toBeNull();
});
});
@@ -61,6 +61,13 @@ describe("telemetry sampling", () => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("notice_displayed event fires even at the highest random value", async () => {
randomSpy.mockReturnValue(0.999);
const { captureClientEvent } = await import("../src/utils/telemetry");
await captureClientEvent("notice_displayed", 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");
@@ -923,6 +923,7 @@ describe("Memory class – backward compat with all providers", () => {
jest.doMock("../src/utils/telemetry", () => ({
captureClientEvent: jest.fn().mockResolvedValue(undefined),
isTelemetryEnabled: jest.fn(() => false),
}));
MemoryClass = require("../src/memory").Memory;
+15 -2
View File
@@ -35,7 +35,7 @@ class AnthropicLLM(LLMBase):
super().__init__(config)
if not self.config.model:
self.config.model = "claude-3-5-sonnet-20240620"
self.config.model = "claude-sonnet-4-6"
api_key = self.config.api_key or os.getenv("ANTHROPIC_API_KEY")
self.client = anthropic.Anthropic(api_key=api_key)
@@ -106,7 +106,20 @@ class AnthropicLLM(LLMBase):
if tools: # TODO: Remove tools if no issues found with new memory addition logic
params["tools"] = tools
params["tool_choice"] = tool_choice
params["tool_choice"] = {"type": tool_choice}
response = self.client.messages.create(**params)
return self._parse_response(response, tools)
def _parse_response(self, response, tools):
if tools:
result = {"content": None, "tool_calls": []}
for block in response.content:
if block.type == "text":
result["content"] = block.text
elif block.type == "tool_use":
result["tool_calls"].append(
{"name": block.name, "arguments": block.input}
)
return result
return response.content[0].text
+1 -1
View File
@@ -409,7 +409,7 @@ class AWSBedrockLLM(LLMBase):
elif self.provider == "cohere":
return response_json.get("generations", [{"text": ""}])[0].get("text", "")
elif self.provider == "ai21":
return response_json.get("completions", [{"data", {"text": ""}}])[0].get("data", {}).get("text", "")
return response_json.get("completions", [{"data": {"text": ""}}])[0].get("data", {}).get("text", "")
else:
# Generic parsing - try common response fields
for field in ["content", "text", "completion", "generation"]:
+16 -3
View File
@@ -72,13 +72,26 @@ class AzureOpenAIStructuredLLM(LLMBase):
messages[-1]["content"] = user_prompt
is_reasoning = self._is_reasoning_model(self.config.model)
params = {
"model": self.config.model,
"messages": messages,
"temperature": self.config.temperature,
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
# Reasoning models (o1/o3/GPT-5 series) reject temperature/top_p; only
# forward the sampling params for non-reasoning models. Mirrors the
# reasoning-aware handling of OpenAIStructuredLLM (#5458).
if not is_reasoning:
params["temperature"] = self.config.temperature
params["top_p"] = self.config.top_p
# Reasoning models require max_completion_tokens rather than max_tokens.
if is_reasoning or self._uses_max_completion_tokens(self.config.model):
params["max_completion_tokens"] = self.config.max_tokens
else:
params["max_tokens"] = self.config.max_tokens
if is_reasoning:
reasoning_effort = getattr(self.config, "reasoning_effort", None)
if reasoning_effort:
params["reasoning_effort"] = reasoning_effort
if response_format:
params["response_format"] = response_format
if tools:

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