Compare commits

...
Author SHA1 Message Date
Soumil Rathi ebc82245a5 fix(oss): cap v3 add ingestion fanout 2026-06-16 17:05:20 -07:00
d772f9a961 feat: support Gemini via Vertex AI as LLM provider (#4030)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-16 16:59:06 +05:30
7c841a2bce fix(redis): do not crash on empty or None filters in search and list (#5446)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-16 16:10:57 +05:30
Hrushikesh YadavandGitHub 6a6dfb4935 fix(huggingface): use self.config instead of raw config parameter (#5538) 2026-06-16 15:51:27 +05:30
Hrushikesh YadavandGitHub 8b370def80 fix: AsyncMemory.reset() does not reset entity store (#5535) 2026-06-16 15:50:30 +05:30
Hrushikesh YadavandGitHub d46464282c fix(pinecone): hybrid search crashes when filters is None (#5533) 2026-06-16 15:49:26 +05:30
Hrushikesh YadavandGitHub bb4a239cb1 fix(mongodb): reset() passes wrong argument to create_col() (#5532) 2026-06-16 15:48:45 +05:30
Hrushikesh YadavandGitHub e30f0d91fe fix(weaviate): reset() crashes with missing vector_size argument (#5531) 2026-06-16 15:45:47 +05:30
Hrushikesh YadavandGitHub 9f34e858c7 fix(ollama): json format mutates caller's messages list in-place (#5539) 2026-06-16 15:37:22 +05:30
BartokandGitHub 94bbc13de0 fix(memory): skip messages without a content key in message parsers (#5575) 2026-06-16 15:29:53 +05:30
Hrushikesh YadavandGitHub a2f01a8fcc fix: async delete_all aborts on first error, leaving partial deletion (#5529) 2026-06-16 11:59:33 +05:30
Hrushikesh YadavandGitHub 30d172e826 fix: omit None config values from Gemini GenerateContentConfig (#5528) 2026-06-16 11:54:42 +05:30
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
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
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 YadavandGitHub 4d949022f2 fix: preserve custom metadata fields during memory update (#5480) 2026-06-15 16:29:44 +05:30
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 SinghandGitHub b90e3c0b76 fix(reranker): respect config.top_k in Cohere and ZeroEntropy fallback paths (#5560) 2026-06-15 16:10:00 +05:30
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 YadavandGitHub 09a9e34382 fix(litellm): function-calling check blocks all calls on non-tool models (#5536) 2026-06-15 15:59:46 +05:30
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
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 SinghandGitHub 66901d7393 fix(llms): accept and forward **kwargs in Together/LangChain/Sarvam providers (#5556) 2026-06-15 12:23:04 +05:30
Hrushikesh YadavandGitHub a1eefc31bc fix(bedrock): use dict literal instead of set in AI21 response parse default (#5527) 2026-06-15 12:09:34 +05:30
Davide LeopardiandGitHub de471799d1 fix(llms): send max_completion_tokens for the GPT-5 family across providers (#5547) 2026-06-15 12:04:19 +05:30
Rod BoevandGitHub 3951ad4705 fix(openclaw): reduce skills-mode triage prompt footprint (#5502) 2026-06-15 11:18:39 +05:30
KartikandGitHub 9315e3036f chore: retire in-repo evaluation/ in favor of mem0ai/memory-benchmarks (#5520) 2026-06-14 00:43:02 +05:30
KartikandGitHub 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
youneshimaandGitHub 3553fc79dd feat(memory): add OSS-to-Platform notices (#5494) 2026-06-13 18:34:20 +05:30
KartikandGitHub f322cf82b9 chore: consolidate cookbooks/ into an indexed examples/ directory (#5517) 2026-06-13 18:25:50 +05:30
KartikandGitHub 73c975ba68 chore: bump version to 0.1.3, update mem0ai to ^3.0.7, and adjust CHANGELOG (#5521) 2026-06-13 18:10:50 +05:30
KartikandGitHub 931d579ba5 chore(openclaw): release v1.0.13 and backfill v1.0.12 changelog (#5519) 2026-06-13 17:59:04 +05:30
124 changed files with 12552 additions and 3216 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"
+33
View File
@@ -4,6 +4,39 @@ description: "Release notes for the OpenClaw plugin and agent harness."
mode: "wide"
---
<Update label="2026-06-12" description="v1.0.13">
**Fixes:**
- **Custom categories payload:** `customCategories` (a `Record<string, string>` map) is now converted via the new `customCategoryMapToList()` helper into the `Array<Record<string, string>>` shape the Mem0 SDK expects on `add` calls — previously the raw object was passed as `custom_categories` and silently ignored ([#5345](https://github.com/mem0ai/mem0/pull/5345))
- **Skip runtime setup during metadata registration:** `register()` now detects `registrationMode === "cli-metadata"`, registers only the CLI commands, and returns early — avoiding backend initialization, service/tool registration, and hook installation during OpenClaw's metadata-only registration pass ([#5383](https://github.com/mem0ai/mem0/pull/5383))
**Security:**
- Bumped `mem0ai` from `3.0.3` to `3.0.7` (latest Node SDK) — includes the transitive axios CVE remediation shipped in `3.0.6` ([#5460](https://github.com/mem0ai/mem0/pull/5460))
- Added pnpm override `uuid@<11.1.1` → `>=11.1.1` to resolve an open MEDIUM Dependabot alert ([#5489](https://github.com/mem0ai/mem0/pull/5489))
**Improvements:**
- **Repo consolidation:** Plugin moved from repo-root `openclaw/` to `integrations/openclaw/`; `package.json` `repository.directory` updated to match so npm provenance links to the correct subdirectory ([#5491](https://github.com/mem0ai/mem0/pull/5491))
**Tests:**
- Added `customCategoryMapToList` unit tests and a `PlatformProvider` test asserting `custom_categories` is passed to the Mem0 SDK as a list ([#5345](https://github.com/mem0ai/mem0/pull/5345))
- Added a regression test asserting `cli-metadata` registration registers only CLI commands and triggers no runtime side effects ([#5383](https://github.com/mem0ai/mem0/pull/5383))
</Update>
<Update label="2026-06-02" description="v1.0.12">
**Docs:**
- **Agent Mode onboarding:** README now documents an autonomous setup path for AI agents — `mem0 init --agent --json` mints an evaluation Mem0 API key with no email, OTP, or browser and exports it as `MEM0_API_KEY` for `openclaw mem0 init`; a human owner can later run `mem0 init --email <email>` to claim ownership without disrupting the agent ([#5123](https://github.com/mem0ai/mem0/pull/5123))
**Security:**
- Added pnpm overrides to remediate advisories in transitive dependencies: `langsmith@<0.6.0` → `^0.6.0`, `picomatch@<2.3.2` → `^2.3.2`, `vite` → `^8.0.5`, and `@qdrant/js-client-rest` → `^1.18.0` ([#5294](https://github.com/mem0ai/mem0/pull/5294))
**Dependencies:**
- Bumped `mem0ai` from `3.0.2` to `3.0.3` ([#5212](https://github.com/mem0ai/mem0/pull/5212))
- Bumped dev dependencies `@vitest/coverage-v8` and `vitest` from `^4.0.18` to `^4.1.7`; added `vite@^8.0.5` and `@qdrant/js-client-rest@^1.18.0` ([#5294](https://github.com/mem0ai/mem0/pull/5294))
</Update>
<Update label="2026-04-29" description="v1.0.11">
**New Features:**
+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:**
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,19 +2,14 @@
All notable changes to the `@mem0/opencode-plugin` will be documented in this file.
## 0.1.4 — Anonymous usage telemetry
### Added
- **PostHog telemetry (`telemetry.ts`):** Anonymous, fire-and-forget usage events. Opt out with `MEM0_TELEMETRY=false`. Only fires when an API key is present; never sends memory content, prompts, or the API key — only an anonymized `sha256(apiKey)[:32]` identity plus event type, platform, and plugin version. Emits the same schema as the Mem0 editor plugin (`plugin.*` events, `source: "plugin"`, `platform: "opencode"`) so OpenCode appears as a `platform` in the shared plugin dashboard. Events: `plugin.session_start` (with memory count) and `plugin.tool_use` (`add` / `search` / `update` / `delete`).
## 0.1.3 — File-context injection, session summaries & activity timeline
## 0.1.3 — File-context injection, session summaries & activity timeline, anonymous telemetry
### Added
- **File-context injection (`tool.execute.before` / Read):** Before the agent reads a file, the plugin searches mem0 for memories referencing that file path and injects prior work as system context. Gates on file size (>= 1,500 bytes). Gives the agent "I've worked on this file before" awareness automatically.
- **Stop hook session summary (`experimental.session.compacting`):** Enhanced session compaction to store a structured `session_summary` memory with `infer=True`, letting the mem0 backend AI extract key facts (request, decisions, learnings, next steps). Previously only stored a raw stats string.
- **SessionStart activity timeline:** The initial memory loading now formats recent memories with type icons (⚖️ decision, 🔴 bug_fix, 🔵 task_learning, etc.) and relative age indicators (2h ago, 1d ago) instead of bare text. Provides a visual "Recent Activity" timeline on first message.
- **PostHog telemetry (`telemetry.ts`):** Anonymous, fire-and-forget usage events. Opt out with `MEM0_TELEMETRY=false`. Only fires when an API key is present; never sends memory content, prompts, or the API key — only an anonymized `sha256(apiKey)[:32]` identity plus event type, platform, and plugin version. Emits the same schema as the Mem0 editor plugin (`plugin.*` events, `source: "plugin"`, `platform: "opencode"`) so OpenCode appears as a `platform` in the shared plugin dashboard. Events: `plugin.session_start` (with memory count) and `plugin.tool_use` (`add` / `search` / `update` / `delete`).
### Changed
@@ -6,7 +6,7 @@
"name": "@mem0/opencode-plugin",
"dependencies": {
"@opencode-ai/plugin": "^1.0.162",
"mem0ai": "^3.0.5",
"mem0ai": "^3.0.7",
},
"devDependencies": {
"bun-types": ">=1.3.14",
@@ -462,7 +462,7 @@
"md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="],
"mem0ai": ["mem0ai@3.0.5", "", { "dependencies": { "axios": "^1.15.2", "openai": "^4.93.0", "uuid": "9.0.1", "zod": "^3.24.1" }, "peerDependencies": { "@anthropic-ai/sdk": "^0.40.1", "@azure/identity": "^4.0.0", "@azure/search-documents": "^12.0.0", "@cloudflare/workers-types": "^4.20250504.0", "@google/genai": "^1.2.0", "@langchain/core": "^1.1.47", "@mistralai/mistralai": "^1.5.2", "@qdrant/js-client-rest": "1.13.0", "@supabase/supabase-js": "^2.49.1", "@types/jest": "29.5.14", "@types/pg": "8.11.0", "better-sqlite3": "^12.6.2", "cloudflare": "^4.2.0", "compromise": "^14.0.0", "groq-sdk": "0.3.0", "natural": "^8.0.1", "ollama": "^0.5.14", "pg": "8.11.3", "redis": "^4.6.13" } }, "sha512-W/R59d5fMpUGHhPEnyoo36GSz5NFJbAs+vS4BxoIvE+t19mIJfoz/2FJSKIw80mT8AkeNYpDfzc/DYRu2IzYIw=="],
"mem0ai": ["mem0ai@3.0.7", "", { "dependencies": { "axios": "^1.16.0", "openai": "^4.93.0", "uuid": "9.0.1", "zod": "^3.24.1" }, "peerDependencies": { "@anthropic-ai/sdk": "^0.40.1", "@azure/identity": "^4.0.0", "@azure/search-documents": "^12.0.0", "@cloudflare/workers-types": "^4.20250504.0", "@google/genai": "^1.40.0", "@langchain/core": "^1.1.47", "@mistralai/mistralai": "^1.5.2", "@qdrant/js-client-rest": "^1.18.0", "@supabase/supabase-js": "^2.49.1", "@types/jest": "29.5.14", "@types/pg": "8.11.0", "better-sqlite3": "^12.6.2", "cloudflare": "^4.2.0", "compromise": "^14.0.0", "groq-sdk": "0.3.0", "natural": "^8.0.1", "ollama": "^0.5.14", "pg": "8.11.3", "redis": "^4.6.13" } }, "sha512-CUHzX7DyeKTHcI3aDsSqY9LXTD7GcFxf988796TuOa4yJgGuF2Xd2NROcBhVFRo3r9y8fVmbo3c5TF9jv1KlKw=="],
"memjs": ["memjs@1.3.2", "", {}, "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg=="],
@@ -1,6 +1,6 @@
{
"name": "@mem0/opencode-plugin",
"version": "0.1.4",
"version": "0.1.3",
"type": "module",
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
"main": "dist/index.js",
@@ -59,7 +59,7 @@
},
"dependencies": {
"@opencode-ai/plugin": "^1.0.162",
"mem0ai": "^3.0.5"
"mem0ai": "^3.0.7"
},
"devDependencies": {
"bun-types": ">=1.3.14",
+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" +
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "1.0.12",
"version": "1.0.13",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
@@ -35,7 +35,7 @@
},
"dependencies": {
"@sinclair/typebox": "0.34.47",
"mem0ai": "3.0.6"
"mem0ai": "3.0.7"
},
"openclaw": {
"extensions": [
@@ -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"
}
}
}
+131 -267
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:
@@ -20,8 +21,8 @@ importers:
specifier: 0.34.47
version: 0.34.47
mem0ai:
specifier: 3.0.6
version: 3.0.6(@anthropic-ai/sdk@0.40.1)(@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@4.104.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.18.0(typescript@5.9.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@4.7.1)(ws@8.21.0)
specifier: 3.0.7
version: 3.0.7(@anthropic-ai/sdk@0.40.1)(@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@4.104.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.18.0(typescript@5.9.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)
devDependencies:
'@qdrant/js-client-rest':
specifier: ^1.18.0
@@ -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]
@@ -398,21 +399,12 @@ packages:
resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==}
engines: {node: '>=18.0.0', pnpm: '>=8'}
'@redis/bloom@1.2.0':
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/bloom@5.12.1':
resolution: {integrity: sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==}
engines: {node: '>= 18.19.0'}
peerDependencies:
'@redis/client': ^5.12.1
'@redis/client@1.6.1':
resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==}
engines: {node: '>=14'}
'@redis/client@5.12.1':
resolution: {integrity: sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==}
engines: {node: '>= 18.19.0'}
@@ -425,38 +417,18 @@ packages:
'@opentelemetry/api':
optional: true
'@redis/graph@1.1.1':
resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/json@1.0.7':
resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/json@5.12.1':
resolution: {integrity: sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==}
engines: {node: '>= 18.19.0'}
peerDependencies:
'@redis/client': ^5.12.1
'@redis/search@1.2.0':
resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/search@5.12.1':
resolution: {integrity: sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==}
engines: {node: '>= 18.19.0'}
peerDependencies:
'@redis/client': ^5.12.1
'@redis/time-series@1.1.0':
resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/time-series@5.12.1':
resolution: {integrity: sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==}
engines: {node: '>= 18.19.0'}
@@ -498,42 +470,36 @@ 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==}
@@ -595,79 +561,66 @@ 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==}
@@ -916,10 +869,6 @@ 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==}
@@ -931,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==}
@@ -1087,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
@@ -1191,10 +1140,6 @@ packages:
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
engines: {node: '>=18'}
generic-pool@3.9.0:
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
engines: {node: '>= 4'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -1413,28 +1358,24 @@ 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==}
@@ -1504,8 +1445,8 @@ packages:
md5@2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
mem0ai@3.0.6:
resolution: {integrity: sha512-X1KbCVE5351qpJ6X2KXFhQJiW+dqoHj1otA66DSdXtIEO1uXuIdyeTKeURdAH+wjXtIcFa08ylL2meA1pBk9Iw==}
mem0ai@3.0.7:
resolution: {integrity: sha512-CUHzX7DyeKTHcI3aDsSqY9LXTD7GcFxf988796TuOa4yJgGuF2Xd2NROcBhVFRo3r9y8fVmbo3c5TF9jv1KlKw==}
engines: {node: '>=18'}
peerDependencies:
'@anthropic-ai/sdk': ^0.40.1
@@ -1696,9 +1637,6 @@ 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==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -1732,15 +1670,6 @@ 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'}
@@ -1868,9 +1797,6 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
redis@4.7.1:
resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==}
redis@5.12.1:
resolution: {integrity: sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==}
engines: {node: '>= 18.19.0'}
@@ -2091,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
@@ -2225,9 +2151,6 @@ packages:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
zod-to-json-schema@3.25.2:
resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==}
peerDependencies:
@@ -2401,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':
@@ -2589,48 +2512,22 @@ snapshots:
'@qdrant/openapi-typescript-fetch@1.2.6': {}
'@redis/bloom@1.2.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/bloom@5.12.1(@redis/client@5.12.1)':
dependencies:
'@redis/client': 5.12.1
'@redis/client@1.6.1':
dependencies:
cluster-key-slot: 1.1.2
generic-pool: 3.9.0
yallist: 4.0.0
'@redis/client@5.12.1':
dependencies:
cluster-key-slot: 1.1.2
'@redis/graph@1.1.1(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/json@1.0.7(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/json@5.12.1(@redis/client@5.12.1)':
dependencies:
'@redis/client': 5.12.1
'@redis/search@1.2.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/search@5.12.1(@redis/client@5.12.1)':
dependencies:
'@redis/client': 5.12.1
'@redis/time-series@1.1.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/time-series@5.12.1(@redis/client@5.12.1)':
dependencies:
'@redis/client': 5.12.1
@@ -2883,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:
@@ -2894,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:
@@ -3009,8 +2906,6 @@ snapshots:
buffer-equal-constant-time@1.0.1: {}
buffer-writer@2.0.0: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
@@ -3020,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: {}
@@ -3156,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: {}
@@ -3276,8 +3171,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
generic-pool@3.9.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -3582,7 +3475,7 @@ snapshots:
crypt: 0.0.2
is-buffer: 1.1.6
mem0ai@3.0.6(@anthropic-ai/sdk@0.40.1)(@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@4.104.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.18.0(typescript@5.9.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@4.7.1)(ws@8.21.0):
mem0ai@3.0.7(@anthropic-ai/sdk@0.40.1)(@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@4.104.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.18.0(typescript@5.9.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):
dependencies:
'@anthropic-ai/sdk': 0.40.1
'@azure/identity': 4.13.1
@@ -3603,8 +3496,8 @@ snapshots:
natural: 8.1.1
ollama: 0.5.18
openai: 4.104.0(ws@8.21.0)(zod@3.25.76)
pg: 8.11.3
redis: 4.7.1
pg: 8.21.0
redis: 5.12.1
uuid: 14.0.0
zod: 3.25.76
transitivePeerDependencies:
@@ -3783,8 +3676,6 @@ snapshots:
dependencies:
p-finally: 1.0.0
packet-reader@1.0.0: {}
pathe@2.0.3: {}
pg-cloudflare@1.4.0:
@@ -3796,10 +3687,6 @@ 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
@@ -3824,18 +3711,6 @@ 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
@@ -3960,15 +3835,6 @@ snapshots:
readdirp@4.1.2: {}
redis@4.7.1:
dependencies:
'@redis/bloom': 1.2.0(@redis/client@1.6.1)
'@redis/client': 1.6.1
'@redis/graph': 1.1.1(@redis/client@1.6.1)
'@redis/json': 1.0.7(@redis/client@1.6.1)
'@redis/search': 1.2.0(@redis/client@1.6.1)
'@redis/time-series': 1.1.0(@redis/client@1.6.1)
redis@5.12.1:
dependencies:
'@redis/bloom': 5.12.1(@redis/client@5.12.1)
@@ -4156,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
@@ -4204,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
@@ -4213,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
@@ -4236,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
@@ -4281,8 +4147,6 @@ snapshots:
xtend@4.0.2: {}
yallist@4.0.0: {}
zod-to-json-schema@3.25.2(zod@4.4.3):
dependencies:
zod: 4.4.3
@@ -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;
}
}
+307 -26
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,
@@ -65,6 +84,58 @@ const ENTITY_PARAMS = [
"runId",
];
const ADD_CONTEXT_SEARCH_MAX_CHARS = 15000;
const ADD_MAX_EXTRACTED_MEMORIES = 20;
const ADD_MEMORY_EMBEDDING_MAX_CHARS = 15000;
const ADD_MAX_ENTITY_LINKS_PER_EVENT = 100;
const SEARCH_QUERY_MAX_CHARS = 15000;
function tailCapText(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
return text.slice(-maxChars);
}
function capExtractedMemories<T>(memories: T[]): T[] {
if (memories.length <= ADD_MAX_EXTRACTED_MEMORIES) return memories;
console.warn(
`Extracted memory cap applied: extracted=${memories.length} processing=${ADD_MAX_EXTRACTED_MEMORIES}`,
);
return memories.slice(-ADD_MAX_EXTRACTED_MEMORIES);
}
function capMemoryEmbeddingTexts(texts: string[]): string[] {
const capped = texts.map((text) =>
tailCapText(text, ADD_MEMORY_EMBEDDING_MAX_CHARS),
);
const cappedCount = texts.filter(
(text, index) => text !== capped[index],
).length;
if (cappedCount > 0) {
console.warn(
`Memory embedding input cap applied: capped_count=${cappedCount} max_chars=${ADD_MEMORY_EMBEDDING_MAX_CHARS}`,
);
}
return capped;
}
function capSearchQuery(query: string): string {
const capped = tailCapText(query, SEARCH_QUERY_MAX_CHARS);
if (capped.length !== query.length) {
console.warn(
`Search query cap applied: original_chars=${query.length} searched_chars=${capped.length}`,
);
}
return capped;
}
function capEntityKeys(keys: string[]): string[] {
if (keys.length <= ADD_MAX_ENTITY_LINKS_PER_EVENT) return keys;
console.warn(
`ADD entity-linking cap applied: extracted=${keys.length} processing=${ADD_MAX_ENTITY_LINKS_PER_EVENT}`,
);
return keys.slice(0, ADD_MAX_ENTITY_LINKS_PER_EVENT);
}
/**
* Validates that no top-level entity parameters are passed in config.
* @throws Error if entity params are found at top level
@@ -494,6 +565,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 +642,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 +672,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 +715,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,
};
@@ -604,7 +786,16 @@ export class Memory {
const parsedMessages = messages.map((m) => m.content).join("\n");
// Phase 1: Existing memory retrieval
const queryEmbedding = await this.embedder.embed(parsedMessages);
const searchMessages = tailCapText(
parsedMessages,
ADD_CONTEXT_SEARCH_MAX_CHARS,
);
if (searchMessages.length !== parsedMessages.length) {
console.warn(
`ADD context-search embedding capped: original_chars=${parsedMessages.length} embedded_chars=${searchMessages.length}`,
);
}
const queryEmbedding = await this.embedder.embed(searchMessages);
const existingResults = await this.vectorStore.search(
queryEmbedding,
10,
@@ -675,6 +866,7 @@ export class Memory {
console.error("Error parsing extraction response:", e);
extractedMemories = [];
}
extractedMemories = capExtractedMemories(extractedMemories);
if (extractedMemories.length === 0) {
// Save messages even if nothing extracted
@@ -696,17 +888,21 @@ export class Memory {
const memTexts = extractedMemories
.map((m) => m.text ?? "")
.filter((t) => t.length > 0);
const memEmbeddingTexts = capMemoryEmbeddingTexts(memTexts);
let embedMap: Record<string, number[]> = {};
try {
const memEmbeddingsList = await this.embedder.embedBatch(memTexts);
const memEmbeddingsList =
await this.embedder.embedBatch(memEmbeddingTexts);
for (let i = 0; i < memTexts.length; i++) {
embedMap[memTexts[i]] = memEmbeddingsList[i];
}
} catch {
// Fallback: embed individually
for (const text of memTexts) {
for (let i = 0; i < memTexts.length; i++) {
const text = memTexts[i];
const embeddingText = memEmbeddingTexts[i];
try {
embedMap[text] = await this.embedder.embed(text);
embedMap[text] = await this.embedder.embed(embeddingText);
} catch (e) {
console.warn(`Failed to embed memory text: ${e}`);
}
@@ -877,9 +1073,8 @@ export class Memory {
const orderedKeys = Object.keys(globalEntities);
if (orderedKeys.length > 0) {
const entityTexts = orderedKeys.map(
(k) => globalEntities[k].entityText,
);
const entityKeys = capEntityKeys(orderedKeys);
const entityTexts = entityKeys.map((k) => globalEntities[k].entityText);
// 7b: Single batch embed for all unique entities
let entityEmbeddings: (number[] | null)[];
@@ -899,9 +1094,9 @@ export class Memory {
// Filter out entities with failed embeddings
const valid: Array<{ index: number; key: string }> = [];
for (let i = 0; i < orderedKeys.length; i++) {
for (let i = 0; i < entityKeys.length; i++) {
if (entityEmbeddings[i] !== null) {
valid.push({ index: i, key: orderedKeys[i] });
valid.push({ index: i, key: entityKeys[i] });
}
}
@@ -996,7 +1191,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 +1229,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 +1320,9 @@ export class Memory {
);
}
const searchStartMs = Date.now();
query = capSearchQuery(query);
// Step 1: Preprocess query
const queryLemmatized = lemmatizeForBm25(query);
const queryEntities = extractEntities(query);
@@ -1280,9 +1498,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 +1540,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 +1592,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 +1662,7 @@ export class Memory {
console.error(this._initError);
});
await this._initPromise;
await this._displayFirstRunNotice("reset");
}
async getAll(config: GetAllMemoryOptions): Promise<SearchResult> {
@@ -1452,7 +1730,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 +1788,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;
+76
View File
@@ -176,3 +176,79 @@ describe("Memory - add()", () => {
);
});
});
describe("Memory - ingestion caps", () => {
test("caps add context-search embedding to the recent tail", async () => {
const memory = createMemory({
vectorStore: {
provider: "memory",
config: {
collectionName: `test-add-context-cap-${Date.now()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
});
const embedSpy = jest.spyOn((memory as any).embedder, "embed");
const longText = `old-${"x".repeat(15000)}`;
await memory.add(longText, { userId: `cap_user_${Date.now()}` });
expect(embedSpy.mock.calls[0][0]).toBe(longText.slice(-15000));
await memory.reset();
});
test("caps extracted memories to the latest 20 before embedding", async () => {
const memory = createMemory({
vectorStore: {
provider: "memory",
config: {
collectionName: `test-add-memory-cap-${Date.now()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
});
(memory as any).llm.generateResponse = jest.fn().mockResolvedValue(
JSON.stringify({
memory: Array.from({ length: 25 }, (_, i) => ({
id: String(i),
text: `memory-${i}`,
})),
}),
);
const embedBatchSpy = jest.spyOn((memory as any).embedder, "embedBatch");
const result: SearchResult = await memory.add("remember these", {
userId: `cap_user_${Date.now()}`,
});
expect(result.results).toHaveLength(20);
expect(embedBatchSpy.mock.calls[0][0]).toEqual(
Array.from({ length: 20 }, (_, i) => `memory-${i + 5}`),
);
await memory.reset();
});
test("caps search query before embedding", async () => {
const memory = createMemory({
vectorStore: {
provider: "memory",
config: {
collectionName: `test-search-cap-${Date.now()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
});
const embedSpy = jest.spyOn((memory as any).embedder, "embed");
const longQuery = `old-${"q".repeat(15000)}`;
await memory.search(longQuery, {
filters: { user_id: `cap_user_${Date.now()}` },
});
expect(embedSpy.mock.calls[0][0]).toBe(longQuery.slice(-15000));
await memory.reset();
});
});
+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;
+64
View File
@@ -0,0 +1,64 @@
import os
from typing import Optional
from mem0.configs.llms.base import BaseLlmConfig
class GeminiConfig(BaseLlmConfig):
"""
Configuration class for Google Gemini LLM.
Supports both the Gemini Developer API (via API key) and Vertex AI (via GCP credentials).
"""
def __init__(
self,
# Base parameters
model: Optional[str] = None,
temperature: float = 0.1,
api_key: Optional[str] = None,
max_tokens: int = 2000,
top_p: float = 0.1,
top_k: int = 1,
enable_vision: bool = False,
vision_details: Optional[str] = "auto",
http_client_proxies: Optional[dict] = None,
# Gemini-specific parameters
vertexai: Optional[bool] = None,
project: Optional[str] = None,
location: Optional[str] = None,
):
"""
Initialize Gemini configuration.
Args:
model: Gemini model to use (e.g., "gemini-2.0-flash"), defaults to None
temperature: Controls randomness, defaults to 0.1
api_key: Google API key for the Gemini Developer API, defaults to None
max_tokens: Maximum tokens to generate, defaults to 2000
top_p: Nucleus sampling parameter, defaults to 0.1
top_k: Top-k sampling parameter, defaults to 1
enable_vision: Enable vision capabilities, defaults to False
vision_details: Vision detail level, defaults to "auto"
http_client_proxies: HTTP client proxy settings, defaults to None
vertexai: Whether to use Vertex AI backend. If None, checks GOOGLE_GENAI_USE_VERTEXAI env var.
project: GCP project ID for Vertex AI. If None, checks GOOGLE_CLOUD_PROJECT env var.
location: GCP location for Vertex AI. If None, checks GOOGLE_CLOUD_LOCATION env var.
"""
super().__init__(
model=model,
temperature=temperature,
api_key=api_key,
max_tokens=max_tokens,
top_p=top_p,
top_k=top_k,
enable_vision=enable_vision,
vision_details=vision_details,
http_client_proxies=http_client_proxies,
)
if vertexai is None:
vertexai = os.getenv("GOOGLE_GENAI_USE_VERTEXAI", "").lower() in ("true", "1", "yes")
self.vertexai = vertexai
self.project = project or os.getenv("GOOGLE_CLOUD_PROJECT")
self.location = location or os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1")
+2 -2
View File
@@ -16,8 +16,8 @@ class HuggingFaceEmbedding(EmbeddingBase):
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
super().__init__(config)
if config.huggingface_base_url:
self.client = OpenAI(base_url=config.huggingface_base_url)
if self.config.huggingface_base_url:
self.client = OpenAI(base_url=self.config.huggingface_base_url)
self.config.model = self.config.model or "tei"
else:
self.config.model = self.config.model or "multi-qa-MiniLM-L6-cos-v1"
+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:
+25 -1
View File
@@ -79,6 +79,25 @@ class LLMBase(ABC):
return False
def _uses_max_completion_tokens(self, model: str) -> bool:
"""
Check if the model expects ``max_completion_tokens`` instead of ``max_tokens``.
The whole GPT-5 family (gpt-5.4-mini, gpt-5.4-nano, gpt-5.5, ...) rejects the
legacy ``max_tokens`` parameter on the Chat Completions API and requires
``max_completion_tokens``. Older models (gpt-4.x, gpt-3.5, etc.) still accept
``max_tokens``.
Args:
model: The model name to check
Returns:
bool: True if the model requires ``max_completion_tokens``
"""
# Strip provider prefixes (e.g. "openai/gpt-5.4-mini" -> "gpt-5.4-mini")
base_model = (model or "").lower().rsplit("/", 1)[-1]
return base_model.startswith("gpt-5")
def _get_supported_params(self, **kwargs) -> Dict:
"""
Get parameters that are supported by the current model.
@@ -141,10 +160,15 @@ class LLMBase(ABC):
"""
params = {
"temperature": self.config.temperature,
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
model = getattr(self.config, "model", "")
if self._uses_max_completion_tokens(model):
params["max_completion_tokens"] = self.config.max_tokens
else:
params["max_tokens"] = self.config.max_tokens
# Add provider-specific parameters from kwargs
params.update(kwargs)
+34 -10
View File
@@ -1,5 +1,5 @@
import os
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Union
try:
from google import genai
@@ -8,18 +8,39 @@ except ImportError:
raise ImportError("The 'google-genai' library is required. Please install it using 'pip install google-genai'.")
from mem0.configs.llms.base import BaseLlmConfig
from mem0.configs.llms.gemini import GeminiConfig
from mem0.llms.base import LLMBase
class GeminiLLM(LLMBase):
def __init__(self, config: Optional[BaseLlmConfig] = None):
def __init__(self, config: Optional[Union[BaseLlmConfig, GeminiConfig, Dict]] = None):
# Convert to GeminiConfig if needed
if config is None:
config = GeminiConfig()
elif isinstance(config, dict):
config = GeminiConfig(**config)
elif isinstance(config, BaseLlmConfig) and not isinstance(config, GeminiConfig):
config = GeminiConfig(
model=config.model,
temperature=config.temperature,
api_key=config.api_key,
max_tokens=config.max_tokens,
top_p=config.top_p,
top_k=config.top_k,
enable_vision=config.enable_vision,
vision_details=config.vision_details,
)
super().__init__(config)
if not self.config.model:
self.config.model = "gemini-2.0-flash"
api_key = self.config.api_key or os.getenv("GOOGLE_API_KEY")
self.client = genai.Client(api_key=api_key)
if self.config.vertexai:
self.client = genai.Client(vertexai=True, project=self.config.project, location=self.config.location)
else:
api_key = self.config.api_key or os.getenv("GOOGLE_API_KEY")
self.client = genai.Client(api_key=api_key)
def _parse_response(self, response, tools):
"""
@@ -157,12 +178,15 @@ class GeminiLLM(LLMBase):
# Extract system instruction and reformat messages
system_instruction, contents = self._reformat_messages(messages)
# Prepare generation config
config_params = {
"temperature": self.config.temperature,
"max_output_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
# Prepare generation config — only include non-None values so the
# Gemini SDK uses its own defaults instead of rejecting None.
config_params = {}
if self.config.temperature is not None:
config_params["temperature"] = self.config.temperature
if self.config.max_tokens is not None:
config_params["max_output_tokens"] = self.config.max_tokens
if self.config.top_p is not None:
config_params["top_p"] = self.config.top_p
# Add system instruction to config if present
if system_instruction:
+4 -1
View File
@@ -57,6 +57,7 @@ class LangchainLLM(LLMBase):
response_format=None,
tools: Optional[List[Dict]] = None,
tool_choice: str = "auto",
**kwargs,
):
"""
Generate a response based on the given messages using langchain_community.
@@ -66,6 +67,8 @@ class LangchainLLM(LLMBase):
response_format (str or object, optional): Format of the response. Not used in Langchain.
tools (list, optional): List of tools that the model can call.
tool_choice (str, optional): Tool choice method.
**kwargs: Additional model parameters forwarded to the underlying LangChain
model's ``invoke`` (matches the ``LLMBase.generate_response`` contract).
Returns:
str: The generated response.
@@ -90,5 +93,5 @@ class LangchainLLM(LLMBase):
if tools:
langchain_model = langchain_model.bind_tools(tools=tools, tool_choice=tool_choice)
response: AIMessage = langchain_model.invoke(langchain_messages)
response: AIMessage = langchain_model.invoke(langchain_messages, **kwargs)
return self._parse_response(response, tools)
+5 -2
View File
@@ -67,16 +67,19 @@ class LiteLLM(LLMBase):
Returns:
str: The generated response.
"""
if not litellm.supports_function_calling(self.config.model):
if tools and not litellm.supports_function_calling(self.config.model):
raise ValueError(f"Model '{self.config.model}' in litellm does not support function calling.")
params = {
"model": self.config.model,
"messages": messages,
"temperature": self.config.temperature,
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
if 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 response_format:
params["response_format"] = response_format
if tools: # TODO: Remove tools if no issues found with new memory addition logic
+2 -1
View File
@@ -119,11 +119,12 @@ class OllamaLLM(LLMBase):
# Handle JSON response format by using Ollama's native format parameter
if response_format and response_format.get("type") == "json_object":
params["format"] = "json"
# Also add JSON format instruction to the last message as a fallback
messages = [dict(m) for m in messages]
if messages and messages[-1]["role"] == "user":
messages[-1]["content"] += "\n\nPlease respond with valid JSON only."
else:
messages.append({"role": "user", "content": "Please respond with valid JSON only."})
params["messages"] = messages
# Add options for Ollama (temperature, num_predict, top_p)
options = {
+2 -5
View File
@@ -36,11 +36,8 @@ class OpenAIStructuredLLM(LLMBase):
Returns:
str: The generated response.
"""
params = {
"model": self.config.model,
"messages": messages,
"temperature": self.config.temperature,
}
params = self._get_supported_params(messages=messages)
params["model"] = self.config.model
if response_format:
params["response_format"] = response_format
+6 -1
View File
@@ -28,7 +28,7 @@ class SarvamLLM(LLMBase):
getattr(self.config, "sarvam_base_url", None) or os.getenv("SARVAM_API_BASE") or "https://api.sarvam.ai/v1"
)
def generate_response(self, messages: List[Dict[str, str]], response_format=None) -> str:
def generate_response(self, messages: List[Dict[str, str]], response_format=None, **kwargs) -> str:
"""
Generate a response based on the given messages using Sarvam-M.
@@ -36,6 +36,8 @@ class SarvamLLM(LLMBase):
messages (list): List of message dicts containing 'role' and 'content'.
response_format (str or object, optional): Format of the response.
Currently not used by Sarvam API.
**kwargs: Additional provider-specific parameters forwarded to the Sarvam
request payload (matches the ``LLMBase.generate_response`` contract).
Returns:
str: The generated response.
@@ -72,6 +74,9 @@ class SarvamLLM(LLMBase):
if param in self.config.model:
params[param] = self.config.model[param]
# Forward any per-call provider-specific parameters (LLMBase contract).
params.update(kwargs)
try:
response = requests.post(url, headers=headers, json=params, timeout=30)
response.raise_for_status()
+4
View File
@@ -58,6 +58,7 @@ class TogetherLLM(LLMBase):
response_format=None,
tools: Optional[List[Dict]] = None,
tool_choice: str = "auto",
**kwargs,
):
"""
Generate a response based on the given messages using TogetherAI.
@@ -67,6 +68,8 @@ class TogetherLLM(LLMBase):
response_format (str or object, optional): Format of the response. Defaults to "text".
tools (list, optional): List of tools that the model can call. Defaults to None.
tool_choice (str, optional): Tool choice method. Defaults to "auto".
**kwargs: Additional provider-specific parameters forwarded to the Together
client (matches the ``LLMBase.generate_response`` contract).
Returns:
str: The generated response.
@@ -78,6 +81,7 @@ class TogetherLLM(LLMBase):
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
params.update(kwargs)
if response_format:
params["response_format"] = response_format
if tools: # TODO: Remove tools if no issues found with new memory addition logic
+307 -37
View File
@@ -5,6 +5,7 @@ import hashlib
import json
import logging
import os
import time
import uuid
import warnings
from copy import deepcopy
@@ -26,6 +27,29 @@ from mem0.memory.base import MemoryBase
from mem0.memory.setup import mem0_dir, setup_config
from mem0.memory.storage import SQLiteManager
from mem0.memory.telemetry import MEM0_TELEMETRY, capture_event
from mem0.memory.notices import (
PERFORMANCE_SLOW_QUERY_THRESHOLD_SECONDS,
detect_scale_threshold_from_add_result,
detect_scale_threshold_from_top_k,
detect_decay_usage_from_delete,
detect_decay_usage_from_delete_all,
detect_temporal_usage_from_metadata,
detect_temporal_usage_from_search,
display_decay_usage_notice,
display_decay_usage_notice_async,
display_first_run_notice,
display_first_run_notice_async,
display_performance_slow_query_notice,
display_performance_slow_query_notice_async,
display_scale_threshold_notice,
display_scale_threshold_notice_async,
display_temporal_usage_notice,
display_temporal_usage_notice_async,
get_decay_feature_error_message,
get_decay_feature_error_message_async,
get_temporal_feature_error_message,
get_temporal_feature_error_message_async,
)
from mem0.memory.utils import (
extract_json,
parse_messages,
@@ -56,6 +80,63 @@ warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*swigva
# Initialize logger early for util functions
logger = logging.getLogger(__name__)
ADD_CONTEXT_SEARCH_MAX_CHARS = 15000
ADD_MAX_EXTRACTED_MEMORIES = 20
ADD_MEMORY_EMBEDDING_MAX_CHARS = 15000
ADD_MAX_ENTITY_LINKS_PER_EVENT = 100
SEARCH_QUERY_MAX_CHARS = 15000
def _tail_cap_text(text: str, max_chars: int) -> str:
if len(text) <= max_chars:
return text
return text[-max_chars:]
def _cap_extracted_memories(extracted_memories: list) -> list:
if len(extracted_memories) <= ADD_MAX_EXTRACTED_MEMORIES:
return extracted_memories
logger.warning(
"Extracted memory cap applied: extracted=%d processing=%d",
len(extracted_memories),
ADD_MAX_EXTRACTED_MEMORIES,
)
return extracted_memories[-ADD_MAX_EXTRACTED_MEMORIES:]
def _cap_memory_embedding_texts(mem_texts: list[str]) -> list[str]:
capped = [_tail_cap_text(t, ADD_MEMORY_EMBEDDING_MAX_CHARS) for t in mem_texts]
capped_count = sum(1 for original, capped_text in zip(mem_texts, capped) if len(original) != len(capped_text))
if capped_count:
logger.warning(
"Memory embedding input cap applied: capped_count=%d max_chars=%d",
capped_count,
ADD_MEMORY_EMBEDDING_MAX_CHARS,
)
return capped
def _cap_search_query(query: str) -> str:
capped = _tail_cap_text(query, SEARCH_QUERY_MAX_CHARS)
if len(capped) != len(query):
logger.warning(
"Search query cap applied: original_chars=%d searched_chars=%d",
len(query),
len(capped),
)
return capped
def _cap_entity_keys(entity_keys: list[str]) -> list[str]:
if len(entity_keys) <= ADD_MAX_ENTITY_LINKS_PER_EVENT:
return entity_keys
logger.warning(
"ADD entity-linking cap applied: extracted=%d processing=%d",
len(entity_keys),
ADD_MAX_ENTITY_LINKS_PER_EVENT,
)
return entity_keys[:ADD_MAX_ENTITY_LINKS_PER_EVENT]
# Fields that hold runtime auth/connection objects and must be preserved.
# These are non-serializable objects (e.g. AWSV4SignerAuth, RequestsHttpConnection)
@@ -349,6 +430,36 @@ def _entity_collection_name(provider: str, collection_name: str) -> str:
setup_config()
logger = logging.getLogger(__name__)
_PROJECT_UPDATE_UNSUPPORTED_ERROR = "Project updates are not supported by the OSS Memory SDK."
class _OSSProject:
def update(
self,
custom_instructions: Optional[str] = None,
custom_categories: Optional[list] = None,
retrieval_criteria: Optional[list] = None,
multilingual: Optional[bool] = None,
decay: Optional[bool] = None,
):
if decay is True:
raise ValueError(get_decay_feature_error_message("sync", "project.update", "decay"))
raise ValueError(_PROJECT_UPDATE_UNSUPPORTED_ERROR)
class _AsyncOSSProject:
async def update(
self,
custom_instructions: Optional[str] = None,
custom_categories: Optional[list] = None,
retrieval_criteria: Optional[list] = None,
multilingual: Optional[bool] = None,
decay: Optional[bool] = None,
):
if decay is True:
raise ValueError(await get_decay_feature_error_message_async("async", "project.update", "decay"))
raise ValueError(_PROJECT_UPDATE_UNSUPPORTED_ERROR)
class Memory(MemoryBase):
def __init__(self, config: MemoryConfig = MemoryConfig()):
@@ -417,6 +528,10 @@ class Memory(MemoryBase):
capture_event("mem0.init", self, {"sync_type": "sync"})
@property
def project(self):
return _OSSProject()
@property
def entity_store(self):
"""Lazily initialize entity store on first use."""
@@ -600,6 +715,7 @@ class Memory(MemoryBase):
agent_id: Optional[str] = None,
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
timestamp: Optional[Any] = None,
infer: bool = True,
memory_type: Optional[str] = None,
prompt: Optional[str] = None,
@@ -617,6 +733,7 @@ class Memory(MemoryBase):
agent_id (str, optional): ID of the agent creating the memory. Defaults to None.
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
infer (bool, optional): If True (default), an LLM is used to extract key facts from
'messages' and decide whether to add, update, or delete related memories.
If False, 'messages' are added as raw memories directly.
@@ -639,7 +756,10 @@ class Memory(MemoryBase):
LLMError: If LLM operations fail.
DatabaseError: If database operations fail.
"""
if timestamp is not None:
raise ValueError(get_temporal_feature_error_message("sync", "add", "timestamp"))
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
processed_metadata, effective_filters = _build_filters_and_metadata(
user_id=user_id,
agent_id=agent_id,
@@ -671,6 +791,13 @@ class Memory(MemoryBase):
if agent_id is not None and memory_type == MemoryType.PROCEDURAL.value:
results = self._create_procedural_memory(messages, metadata=processed_metadata, prompt=prompt)
scale_threshold_notice = detect_scale_threshold_from_add_result(self, results)
if temporal_usage_notice:
display_temporal_usage_notice(self, "sync", "add", *temporal_usage_notice)
elif scale_threshold_notice:
display_scale_threshold_notice(self, "sync", "add", *scale_threshold_notice)
else:
display_first_run_notice(self, "sync", "add")
return results
if self.config.llm.config.get("enable_vision"):
@@ -679,6 +806,13 @@ class Memory(MemoryBase):
messages = parse_vision_messages(messages)
vector_store_result = self._add_to_vector_store(messages, processed_metadata, effective_filters, infer, prompt=prompt)
scale_threshold_notice = detect_scale_threshold_from_add_result(self, vector_store_result)
if temporal_usage_notice:
display_temporal_usage_notice(self, "sync", "add", *temporal_usage_notice)
elif scale_threshold_notice:
display_scale_threshold_notice(self, "sync", "add", *scale_threshold_notice)
else:
display_first_run_notice(self, "sync", "add")
return {"results": vector_store_result}
def _add_to_vector_store(self, messages, metadata, filters, infer, prompt=None):
@@ -727,9 +861,16 @@ class Memory(MemoryBase):
# Phase 1: Existing memory retrieval
search_filters = {k: v for k, v in filters.items() if k in ("user_id", "agent_id", "run_id") and v}
query_embedding = self.embedding_model.embed(parsed_messages, "search")
search_messages = _tail_cap_text(parsed_messages, ADD_CONTEXT_SEARCH_MAX_CHARS)
if len(search_messages) != len(parsed_messages):
logger.warning(
"ADD context-search embedding capped: original_chars=%d embedded_chars=%d",
len(parsed_messages),
len(search_messages),
)
query_embedding = self.embedding_model.embed(search_messages, "search")
existing_results = self.vector_store.search(
query=parsed_messages,
query=search_messages,
vectors=query_embedding,
top_k=10,
filters=search_filters,
@@ -783,6 +924,7 @@ class Memory(MemoryBase):
except Exception as e:
logger.error(f"Error parsing extraction response: {e}")
extracted_memories = []
extracted_memories = _cap_extracted_memories(extracted_memories)
if not extracted_memories:
# Save messages even if nothing extracted
@@ -791,15 +933,16 @@ class Memory(MemoryBase):
# Phase 3: Batch embed all extracted memory texts
mem_texts = [m.get("text", "") for m in extracted_memories if m.get("text")]
mem_embedding_texts = _cap_memory_embedding_texts(mem_texts)
try:
mem_embeddings_list = self.embedding_model.embed_batch(mem_texts, "add")
mem_embeddings_list = self.embedding_model.embed_batch(mem_embedding_texts, "add")
embed_map = dict(zip(mem_texts, mem_embeddings_list))
except Exception:
# Fallback: embed individually
embed_map = {}
for text in mem_texts:
for text, embedding_text in zip(mem_texts, mem_embedding_texts):
try:
embed_map[text] = self.embedding_model.embed(text, "add")
embed_map[text] = self.embedding_model.embed(embedding_text, "add")
except Exception as e:
logger.warning(f"Failed to embed memory text: {e}")
@@ -901,7 +1044,7 @@ class Memory(MemoryBase):
global_entities[key] = [entity_type, entity_text, {memory_id}]
if global_entities:
ordered_keys = list(global_entities.keys())
ordered_keys = _cap_entity_keys(list(global_entities.keys()))
entity_texts = [global_entities[k][1] for k in ordered_keys]
# 7b: Single batch embed for all unique entities
@@ -1005,6 +1148,7 @@ class Memory(MemoryBase):
capture_event("mem0.get", self, {"memory_id": memory_id, "sync_type": "sync"})
memory = self.vector_store.get(vector_id=memory_id)
if not memory:
display_first_run_notice(self, "sync", "get")
return None
promoted_payload_keys = [
@@ -1033,6 +1177,7 @@ class Memory(MemoryBase):
if additional_metadata:
result_item["metadata"] = additional_metadata
display_first_run_notice(self, "sync", "get")
return result_item
def get_all(
@@ -1088,6 +1233,7 @@ class Memory(MemoryBase):
)
limit = top_k
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
keys, encoded_ids = process_telemetry_filters(effective_filters)
capture_event(
@@ -1096,6 +1242,10 @@ class Memory(MemoryBase):
all_memories_result = self._get_all_from_vector_store(effective_filters, limit)
if scale_threshold_notice:
display_scale_threshold_notice(self, "sync", "get_all", *scale_threshold_notice)
else:
display_first_run_notice(self, "sync", "get_all")
return {"results": all_memories_result}
def _get_all_from_vector_store(self, filters, limit):
@@ -1154,6 +1304,7 @@ class Memory(MemoryBase):
threshold: float = 0.1,
rerank: bool = False,
explain: bool = False,
reference_date: Optional[Any] = None,
**kwargs,
):
"""
@@ -1185,6 +1336,7 @@ class Memory(MemoryBase):
threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1.
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -1194,12 +1346,16 @@ class Memory(MemoryBase):
ValueError: If filters doesn't contain at least one of user_id, agent_id, run_id,
or if threshold/top_k values are invalid.
"""
if reference_date is not None:
raise ValueError(get_temporal_feature_error_message("sync", "search", "reference_date"))
# Reject top-level entity params - must use filters instead
_reject_top_level_entity_params(kwargs, "search")
# Validate search parameters (before applying defaults)
_validate_search_params(threshold=threshold, top_k=top_k)
query = _validate_and_trim_search_query(query)
temporal_usage_notice = detect_temporal_usage_from_search(query, filters)
# Validate and trim entity IDs in filters
effective_filters = filters.copy() if filters else {}
@@ -1222,6 +1378,7 @@ class Memory(MemoryBase):
)
limit = top_k
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
# Apply enhanced metadata filtering if advanced operators are detected
if self._has_advanced_operators(effective_filters):
@@ -1250,7 +1407,9 @@ class Memory(MemoryBase):
},
)
search_start = time.perf_counter()
original_memories = self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
search_elapsed_seconds = time.perf_counter() - search_start
# Apply reranking if enabled and reranker is available
if rerank and self.reranker and original_memories:
@@ -1260,6 +1419,21 @@ class Memory(MemoryBase):
except Exception as e:
logger.warning(f"Reranking failed, using original results: {e}")
if temporal_usage_notice:
display_temporal_usage_notice(self, "sync", "search", *temporal_usage_notice)
elif scale_threshold_notice:
display_scale_threshold_notice(self, "sync", "search", *scale_threshold_notice)
elif search_elapsed_seconds > PERFORMANCE_SLOW_QUERY_THRESHOLD_SECONDS:
display_performance_slow_query_notice(
self,
"sync",
"search",
search_elapsed_seconds,
top_k,
len(original_memories),
)
else:
display_first_run_notice(self, "sync", "search")
return {"results": original_memories}
def _process_metadata_filters(self, metadata_filters: Dict[str, Any]) -> Dict[str, Any]:
@@ -1370,6 +1544,7 @@ class Memory(MemoryBase):
# Guard against None threshold (backward compat)
if threshold is None:
threshold = 0.1
query = _cap_search_query(query)
# Step 1: Preprocess query
query_lemmatized = lemmatize_for_bm25(query)
@@ -1569,6 +1744,7 @@ class Memory(MemoryBase):
existing_embeddings = {data: self.embedding_model.embed(data, "update")}
self._update_memory(memory_id, data, existing_embeddings, metadata)
display_first_run_notice(self, "sync", "update")
return {"message": "Memory updated successfully!"}
def delete(self, memory_id):
@@ -1585,6 +1761,11 @@ class Memory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found")
self._delete_memory(memory_id, existing_memory)
decay_usage_notice = detect_decay_usage_from_delete()
if decay_usage_notice:
display_decay_usage_notice(self, "sync", "delete", *decay_usage_notice)
else:
display_first_run_notice(self, "sync", "delete")
return {"message": "Memory deleted successfully!"}
def delete_all(self, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None):
@@ -1618,6 +1799,11 @@ class Memory(MemoryBase):
logger.info(f"Deleted {len(memories)} memories")
decay_usage_notice = detect_decay_usage_from_delete_all(len(memories))
if decay_usage_notice:
display_decay_usage_notice(self, "sync", "delete_all", *decay_usage_notice)
else:
display_first_run_notice(self, "sync", "delete_all")
return {"message": "Memories deleted successfully!"}
def history(self, memory_id):
@@ -1631,7 +1817,9 @@ class Memory(MemoryBase):
list: List of changes for the memory.
"""
capture_event("mem0.history", self, {"memory_id": memory_id, "sync_type": "sync"})
return self.db.get_history(memory_id)
history = self.db.get_history(memory_id)
display_first_run_notice(self, "sync", "history")
return history
def _create_memory(self, data, existing_embeddings, metadata=None):
logger.debug(f"Creating memory with {data=}")
@@ -1718,7 +1906,9 @@ class Memory(MemoryBase):
prev_value = existing_memory.payload.get("data")
new_metadata = deepcopy(metadata) if metadata is not None else {}
new_metadata = deepcopy(existing_memory.payload)
if metadata is not None:
new_metadata.update(metadata)
new_metadata["data"] = data
new_metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
@@ -1726,17 +1916,9 @@ class Memory(MemoryBase):
new_metadata["created_at"] = existing_memory.payload.get("created_at")
new_metadata["updated_at"] = datetime.now(timezone.utc).isoformat()
# Preserve session identifiers from existing memory only if not provided in new metadata
if "user_id" not in new_metadata and "user_id" in existing_memory.payload:
new_metadata["user_id"] = existing_memory.payload["user_id"]
if "agent_id" not in new_metadata and "agent_id" in existing_memory.payload:
new_metadata["agent_id"] = existing_memory.payload["agent_id"]
if "run_id" not in new_metadata and "run_id" in existing_memory.payload:
new_metadata["run_id"] = existing_memory.payload["run_id"]
# actor_id is immutable after creation (issue #4490)
if "actor_id" in existing_memory.payload:
new_metadata["actor_id"] = existing_memory.payload["actor_id"]
if "role" not in new_metadata and "role" in existing_memory.payload:
new_metadata["role"] = existing_memory.payload["role"]
if data in existing_embeddings:
embeddings = existing_embeddings[data]
@@ -1831,6 +2013,7 @@ class Memory(MemoryBase):
self._entity_store = None
capture_event("mem0.reset", self, {"sync_type": "sync"})
display_first_run_notice(self, "sync", "reset")
def close(self):
"""Release resources held by this Memory instance (SQLite connections, etc.)."""
@@ -1889,6 +2072,10 @@ class AsyncMemory(MemoryBase):
capture_event("mem0.init", self, {"sync_type": "async"})
@property
def project(self):
return _AsyncOSSProject()
@property
def entity_store(self):
"""Lazily initialize entity store on first use."""
@@ -2058,6 +2245,7 @@ class AsyncMemory(MemoryBase):
agent_id: Optional[str] = None,
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
timestamp: Optional[Any] = None,
infer: bool = True,
memory_type: Optional[str] = None,
prompt: Optional[str] = None,
@@ -2072,6 +2260,7 @@ class AsyncMemory(MemoryBase):
agent_id (str, optional): ID of the agent creating the memory. Defaults to None.
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
infer (bool, optional): Whether to infer the memories. Defaults to True.
memory_type (str, optional): Type of memory to create. Defaults to None.
Pass "procedural_memory" to create procedural memories.
@@ -2080,6 +2269,10 @@ class AsyncMemory(MemoryBase):
Returns:
dict: A dictionary containing the result of the memory addition operation.
"""
if timestamp is not None:
raise ValueError(await get_temporal_feature_error_message_async("async", "add", "timestamp"))
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
processed_metadata, effective_filters = _build_filters_and_metadata(
user_id=user_id, agent_id=agent_id, run_id=run_id, input_metadata=metadata
)
@@ -2107,6 +2300,13 @@ class AsyncMemory(MemoryBase):
results = await self._create_procedural_memory(
messages, metadata=processed_metadata, prompt=prompt, llm=llm
)
scale_threshold_notice = await asyncio.to_thread(detect_scale_threshold_from_add_result, self, results)
if temporal_usage_notice:
await display_temporal_usage_notice_async(self, "async", "add", *temporal_usage_notice)
elif scale_threshold_notice:
await display_scale_threshold_notice_async(self, "async", "add", *scale_threshold_notice)
else:
await display_first_run_notice_async(self, "async", "add")
return results
if self.config.llm.config.get("enable_vision"):
@@ -2115,6 +2315,13 @@ class AsyncMemory(MemoryBase):
messages = parse_vision_messages(messages)
vector_store_result = await self._add_to_vector_store(messages, processed_metadata, effective_filters, infer, prompt=prompt)
scale_threshold_notice = await asyncio.to_thread(detect_scale_threshold_from_add_result, self, vector_store_result)
if temporal_usage_notice:
await display_temporal_usage_notice_async(self, "async", "add", *temporal_usage_notice)
elif scale_threshold_notice:
await display_scale_threshold_notice_async(self, "async", "add", *scale_threshold_notice)
else:
await display_first_run_notice_async(self, "async", "add")
return {"results": vector_store_result}
async def _add_to_vector_store(
@@ -2170,10 +2377,17 @@ class AsyncMemory(MemoryBase):
# Phase 1: Existing memory retrieval
search_filters = {k: v for k, v in effective_filters.items() if k in ("user_id", "agent_id", "run_id") and v}
query_embedding = await asyncio.to_thread(self.embedding_model.embed, parsed_messages, "search")
search_messages = _tail_cap_text(parsed_messages, ADD_CONTEXT_SEARCH_MAX_CHARS)
if len(search_messages) != len(parsed_messages):
logger.warning(
"ADD context-search embedding capped: original_chars=%d embedded_chars=%d",
len(parsed_messages),
len(search_messages),
)
query_embedding = await asyncio.to_thread(self.embedding_model.embed, search_messages, "search")
existing_results = await asyncio.to_thread(
self.vector_store.search,
query=parsed_messages,
query=search_messages,
vectors=query_embedding,
top_k=10,
filters=search_filters,
@@ -2228,6 +2442,7 @@ class AsyncMemory(MemoryBase):
except Exception as e:
logger.error(f"Error parsing extraction response (async): {e}")
extracted_memories = []
extracted_memories = _cap_extracted_memories(extracted_memories)
if not extracted_memories:
await asyncio.to_thread(self.db.save_messages, messages, session_scope)
@@ -2235,14 +2450,15 @@ class AsyncMemory(MemoryBase):
# Phase 3: Batch embed all extracted memory texts
mem_texts = [m.get("text", "") for m in extracted_memories if m.get("text")]
mem_embedding_texts = _cap_memory_embedding_texts(mem_texts)
try:
mem_embeddings_list = await asyncio.to_thread(self.embedding_model.embed_batch, mem_texts, "add")
mem_embeddings_list = await asyncio.to_thread(self.embedding_model.embed_batch, mem_embedding_texts, "add")
embed_map = dict(zip(mem_texts, mem_embeddings_list))
except Exception:
embed_map = {}
for text in mem_texts:
for text, embedding_text in zip(mem_texts, mem_embedding_texts):
try:
embed_map[text] = await asyncio.to_thread(self.embedding_model.embed, text, "add")
embed_map[text] = await asyncio.to_thread(self.embedding_model.embed, embedding_text, "add")
except Exception as e:
logger.warning(f"Failed to embed memory text (async): {e}")
@@ -2345,7 +2561,7 @@ class AsyncMemory(MemoryBase):
global_entities[key] = [entity_type, entity_text, {memory_id}]
if global_entities:
ordered_keys = list(global_entities.keys())
ordered_keys = _cap_entity_keys(list(global_entities.keys()))
entity_texts = [global_entities[k][1] for k in ordered_keys]
# 7b: Batch embed entities
@@ -2448,6 +2664,7 @@ class AsyncMemory(MemoryBase):
capture_event("mem0.get", self, {"memory_id": memory_id, "sync_type": "async"})
memory = await asyncio.to_thread(self.vector_store.get, vector_id=memory_id)
if not memory:
await display_first_run_notice_async(self, "async", "get")
return None
promoted_payload_keys = [
@@ -2476,6 +2693,7 @@ class AsyncMemory(MemoryBase):
if additional_metadata:
result_item["metadata"] = additional_metadata
await display_first_run_notice_async(self, "async", "get")
return result_item
async def get_all(
@@ -2531,6 +2749,7 @@ class AsyncMemory(MemoryBase):
)
limit = top_k
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
keys, encoded_ids = process_telemetry_filters(effective_filters)
capture_event(
@@ -2539,6 +2758,10 @@ class AsyncMemory(MemoryBase):
all_memories_result = await self._get_all_from_vector_store(effective_filters, limit)
if scale_threshold_notice:
await display_scale_threshold_notice_async(self, "async", "get_all", *scale_threshold_notice)
else:
await display_first_run_notice_async(self, "async", "get_all")
return {"results": all_memories_result}
async def _get_all_from_vector_store(self, filters, limit):
@@ -2597,6 +2820,7 @@ class AsyncMemory(MemoryBase):
threshold: float = 0.1,
rerank: bool = False,
explain: bool = False,
reference_date: Optional[Any] = None,
**kwargs,
):
"""
@@ -2628,6 +2852,7 @@ class AsyncMemory(MemoryBase):
threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1.
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -2637,12 +2862,18 @@ class AsyncMemory(MemoryBase):
ValueError: If filters doesn't contain at least one of user_id, agent_id, run_id,
or if threshold/top_k values are invalid.
"""
if reference_date is not None:
raise ValueError(
await get_temporal_feature_error_message_async("async", "search", "reference_date")
)
# Reject top-level entity params - must use filters instead
_reject_top_level_entity_params(kwargs, "search")
# Validate search parameters (before applying defaults)
_validate_search_params(threshold=threshold, top_k=top_k)
query = _validate_and_trim_search_query(query)
temporal_usage_notice = detect_temporal_usage_from_search(query, filters)
# Validate and trim entity IDs in filters
effective_filters = filters.copy() if filters else {}
@@ -2667,6 +2898,7 @@ class AsyncMemory(MemoryBase):
)
limit = top_k
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
# Apply enhanced metadata filtering if advanced operators are detected
if self._has_advanced_operators(effective_filters):
@@ -2695,7 +2927,9 @@ class AsyncMemory(MemoryBase):
},
)
search_start = time.perf_counter()
original_memories = await self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
search_elapsed_seconds = time.perf_counter() - search_start
# Apply reranking if enabled and reranker is available
if rerank and self.reranker and original_memories:
@@ -2708,6 +2942,21 @@ class AsyncMemory(MemoryBase):
except Exception as e:
logger.warning(f"Reranking failed, using original results: {e}")
if temporal_usage_notice:
await display_temporal_usage_notice_async(self, "async", "search", *temporal_usage_notice)
elif scale_threshold_notice:
await display_scale_threshold_notice_async(self, "async", "search", *scale_threshold_notice)
elif search_elapsed_seconds > PERFORMANCE_SLOW_QUERY_THRESHOLD_SECONDS:
await display_performance_slow_query_notice_async(
self,
"async",
"search",
search_elapsed_seconds,
top_k,
len(original_memories),
)
else:
await display_first_run_notice_async(self, "async", "search")
return {"results": original_memories}
def _process_metadata_filters(self, metadata_filters: Dict[str, Any]) -> Dict[str, Any]:
@@ -2817,6 +3066,7 @@ class AsyncMemory(MemoryBase):
async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
if threshold is None:
threshold = 0.1
query = _cap_search_query(query)
# Step 1: Preprocess query (CPU-bound)
query_lemmatized = await asyncio.to_thread(lemmatize_for_bm25, query)
@@ -3008,6 +3258,7 @@ class AsyncMemory(MemoryBase):
existing_embeddings = {data: embeddings}
await self._update_memory(memory_id, data, existing_embeddings, metadata)
await display_first_run_notice_async(self, "async", "update")
return {"message": "Memory updated successfully!"}
async def delete(self, memory_id):
@@ -3024,6 +3275,11 @@ class AsyncMemory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found")
await self._delete_memory(memory_id, existing_memory)
decay_usage_notice = detect_decay_usage_from_delete()
if decay_usage_notice:
await display_decay_usage_notice_async(self, "async", "delete", *decay_usage_notice)
else:
await display_first_run_notice_async(self, "async", "delete")
return {"message": "Memory deleted successfully!"}
async def delete_all(self, user_id=None, agent_id=None, run_id=None):
@@ -3056,10 +3312,21 @@ class AsyncMemory(MemoryBase):
for memory in memories[0]:
delete_tasks.append(self._delete_memory(memory.id))
await asyncio.gather(*delete_tasks)
results = await asyncio.gather(*delete_tasks, return_exceptions=True)
logger.info(f"Deleted {len(memories[0])} memories")
errors = [r for r in results if isinstance(r, BaseException)]
if errors:
logger.warning("Failed to delete %d out of %d memories", len(errors), len(results))
for err in errors:
logger.warning("Delete error: %s", err)
logger.info(f"Deleted {len(results) - len(errors)} memories")
decay_usage_notice = detect_decay_usage_from_delete_all(len(memories[0]))
if decay_usage_notice:
await display_decay_usage_notice_async(self, "async", "delete_all", *decay_usage_notice)
else:
await display_first_run_notice_async(self, "async", "delete_all")
return {"message": "Memories deleted successfully!"}
async def history(self, memory_id):
@@ -3073,7 +3340,9 @@ class AsyncMemory(MemoryBase):
list: List of changes for the memory.
"""
capture_event("mem0.history", self, {"memory_id": memory_id, "sync_type": "async"})
return await asyncio.to_thread(self.db.get_history, memory_id)
history = await asyncio.to_thread(self.db.get_history, memory_id)
await display_first_run_notice_async(self, "async", "history")
return history
async def _create_memory(self, data, existing_embeddings, metadata=None):
logger.debug(f"Creating memory with {data=}")
@@ -3179,7 +3448,9 @@ class AsyncMemory(MemoryBase):
prev_value = existing_memory.payload.get("data")
new_metadata = deepcopy(metadata) if metadata is not None else {}
new_metadata = deepcopy(existing_memory.payload)
if metadata is not None:
new_metadata.update(metadata)
new_metadata["data"] = data
new_metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
@@ -3187,18 +3458,9 @@ class AsyncMemory(MemoryBase):
new_metadata["created_at"] = existing_memory.payload.get("created_at")
new_metadata["updated_at"] = datetime.now(timezone.utc).isoformat()
# Preserve session identifiers from existing memory only if not provided in new metadata
if "user_id" not in new_metadata and "user_id" in existing_memory.payload:
new_metadata["user_id"] = existing_memory.payload["user_id"]
if "agent_id" not in new_metadata and "agent_id" in existing_memory.payload:
new_metadata["agent_id"] = existing_memory.payload["agent_id"]
if "run_id" not in new_metadata and "run_id" in existing_memory.payload:
new_metadata["run_id"] = existing_memory.payload["run_id"]
# actor_id is immutable after creation (issue #4490)
if "actor_id" in existing_memory.payload:
new_metadata["actor_id"] = existing_memory.payload["actor_id"]
if "role" not in new_metadata and "role" in existing_memory.payload:
new_metadata["role"] = existing_memory.payload["role"]
if data in existing_embeddings:
embeddings = existing_embeddings[data]
@@ -3290,7 +3552,15 @@ class AsyncMemory(MemoryBase):
self.config.vector_store.provider, self.config.vector_store.config
)
if self._entity_store is not None:
try:
await asyncio.to_thread(self._entity_store.reset)
except Exception as e:
logger.warning(f"Failed to reset entity store: {e}")
self._entity_store = None
capture_event("mem0.reset", self, {"sync_type": "async"})
await display_first_run_notice_async(self, "async", "reset")
def close(self):
"""Release resources held by this AsyncMemory instance."""
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -1,6 +1,7 @@
import json
import logging
import os
import tempfile
import uuid
from hashlib import sha256
@@ -34,10 +35,21 @@ def _load_config():
def _write_config(config):
"""Best-effort write of ~/.mem0/config.json. Never raises."""
path = _config_path()
temp_path = None
try:
with open(path, "w") as f:
os.makedirs(os.path.dirname(path), exist_ok=True)
with tempfile.NamedTemporaryFile("w", dir=os.path.dirname(path), delete=False) as f:
temp_path = f.name
json.dump(config, f, indent=4)
f.flush()
os.fsync(f.fileno())
os.replace(temp_path, path)
except Exception as e:
if temp_path:
try:
os.unlink(temp_path)
except OSError:
pass
_logger.debug("Failed to write mem0 config %s: %s", path, e)
+15 -9
View File
@@ -14,6 +14,7 @@ from mem0.memory.setup import get_or_create_user_id
MEM0_TELEMETRY = os.environ.get("MEM0_TELEMETRY", "True")
PROJECT_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
HOST = "https://us.i.posthog.com"
FEATURE_FLAGS_REQUEST_TIMEOUT_SECONDS = 0.5
if isinstance(MEM0_TELEMETRY, str):
MEM0_TELEMETRY = MEM0_TELEMETRY.lower() in ("true", "1", "yes")
@@ -49,7 +50,9 @@ MEM0_TELEMETRY_SAMPLE_RATE = _parse_sample_rate(os.environ.get("MEM0_TELEMETRY_S
# Events that bypass sampling and always fire. Keep this set in sync with the
# event names passed to capture_event() in mem0/memory/main.py.
# $identify is included so PostHog person-merging is never lost to sampling.
_LIFECYCLE_EVENTS = frozenset({"mem0.init", "mem0.reset", "mem0._create_procedural_memory", "$identify"})
_LIFECYCLE_EVENTS = frozenset(
{"mem0.init", "mem0.reset", "mem0._create_procedural_memory", "mem0.notice_displayed", "$identify"}
)
def _sampling_before_send(msg):
@@ -77,15 +80,15 @@ class AnonymousTelemetry:
self.user_id = None
return
try:
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST, before_send=before_send)
except TypeError:
# posthog <4.5.0 does not accept before_send; fall back without sampling.
_logger.debug("posthog.Posthog does not accept before_send; upgrade to >=4.5.0 for sampling")
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST)
self.posthog = Posthog(
project_api_key=PROJECT_API_KEY,
host=HOST,
before_send=before_send,
feature_flags_request_timeout_seconds=FEATURE_FLAGS_REQUEST_TIMEOUT_SECONDS,
)
self.user_id = get_or_create_user_id(vector_store)
def capture_event(self, event_name, properties=None, user_email=None):
def capture_event(self, event_name, properties=None, user_email=None, flags=None):
if self.posthog is None:
return
@@ -109,7 +112,10 @@ class AnonymousTelemetry:
**properties,
}
try:
self.posthog.capture(distinct_id=distinct_id, event=event_name, properties=properties)
capture_kwargs = {"distinct_id": distinct_id, "properties": properties}
if flags is not None:
capture_kwargs["flags"] = flags
self.posthog.capture(event_name, **capture_kwargs)
except Exception as e:
_logger.debug("Failed to capture telemetry event %r: %s", event_name, e)
+26 -13
View File
@@ -61,12 +61,18 @@ def ensure_json_instruction(system_prompt, user_prompt):
def parse_messages(messages):
response = ""
for msg in messages:
if msg["role"] == "system":
response += f"system: {msg['content']}\n"
if msg["role"] == "user":
response += f"user: {msg['content']}\n"
if msg["role"] == "assistant":
response += f"assistant: {msg['content']}\n"
role = msg.get("role")
content = msg.get("content")
# Skip messages without textual content (e.g. assistant tool-call
# messages that carry `tool_calls` but no `content` key).
if content is None:
continue
if role == "system":
response += f"system: {content}\n"
elif role == "user":
response += f"user: {content}\n"
elif role == "assistant":
response += f"assistant: {content}\n"
return response
@@ -173,12 +179,19 @@ def parse_vision_messages(messages, llm=None, vision_details="auto"):
"""
returned_messages = []
for msg in messages:
if msg["role"] == "system":
role = msg.get("role")
content = msg.get("content")
if role == "system":
returned_messages.append(msg)
continue
# Skip messages without content (e.g. assistant tool-call messages
# that carry `tool_calls` but no `content` key).
if content is None:
continue
# Handle message content
if isinstance(msg["content"], list):
if isinstance(content, list):
if llm is None:
text_parts = [
part["text"] for part in msg["content"]
@@ -186,17 +199,17 @@ def parse_vision_messages(messages, llm=None, vision_details="auto"):
]
if not text_parts:
continue
returned_messages.append({"role": msg["role"], "content": " ".join(text_parts)})
returned_messages.append({"role": role, "content": " ".join(text_parts)})
else:
description = get_image_description(msg, llm, vision_details)
returned_messages.append({"role": msg["role"], "content": description})
elif isinstance(msg["content"], dict) and msg["content"].get("type") == "image_url":
returned_messages.append({"role": role, "content": description})
elif isinstance(content, dict) and content.get("type") == "image_url":
if llm is None:
continue
image_url = msg["content"]["image_url"]["url"]
image_url = content["image_url"]["url"]
try:
description = get_image_description(image_url, llm, vision_details)
returned_messages.append({"role": msg["role"], "content": description})
returned_messages.append({"role": role, "content": description})
except Exception:
raise Exception(f"Error while downloading {image_url}.")
else:
+2 -1
View File
@@ -82,4 +82,5 @@ class CohereReranker(BaseReranker):
# Fallback to original order if reranking fails
for doc in documents:
doc['rerank_score'] = 0.0
return documents[:top_k] if top_k else documents
final_top_k = top_k or self.config.top_k
return documents[:final_top_k] if final_top_k else documents
+2 -1
View File
@@ -93,4 +93,5 @@ class ZeroEntropyReranker(BaseReranker):
# Fallback to original order if reranking fails
for doc in documents:
doc['rerank_score'] = 0.0
return documents[:top_k] if top_k else documents
final_top_k = top_k or self.config.top_k
return documents[:final_top_k] if final_top_k else documents
+2 -1
View File
@@ -7,6 +7,7 @@ from mem0.configs.llms.aws_bedrock import AWSBedrockConfig
from mem0.configs.llms.azure import AzureOpenAIConfig
from mem0.configs.llms.base import BaseLlmConfig
from mem0.configs.llms.deepseek import DeepSeekConfig
from mem0.configs.llms.gemini import GeminiConfig
from mem0.configs.llms.lmstudio import LMStudioConfig
from mem0.configs.llms.minimax import MinimaxConfig
from mem0.configs.llms.ollama import OllamaConfig
@@ -46,7 +47,7 @@ class LlmFactory:
"openai_structured": ("mem0.llms.openai_structured.OpenAIStructuredLLM", OpenAIConfig),
"anthropic": ("mem0.llms.anthropic.AnthropicLLM", AnthropicConfig),
"azure_openai_structured": ("mem0.llms.azure_openai_structured.AzureOpenAIStructuredLLM", AzureOpenAIConfig),
"gemini": ("mem0.llms.gemini.GeminiLLM", BaseLlmConfig),
"gemini": ("mem0.llms.gemini.GeminiLLM", GeminiConfig),
"deepseek": ("mem0.llms.deepseek.DeepSeekLLM", DeepSeekConfig),
"minimax": ("mem0.llms.minimax.MiniMaxLLM", MinimaxConfig),
"xai": ("mem0.llms.xai.XAILLM", XAIConfig),
+4 -3
View File
@@ -187,7 +187,7 @@ class ChromaDB(VectorStoreBase):
"""
self.collection.update(ids=vector_id, embeddings=vector, metadatas=payload)
def get(self, vector_id: str) -> OutputData:
def get(self, vector_id: str) -> Optional[OutputData]:
"""
Retrieve a vector by ID.
@@ -195,10 +195,11 @@ class ChromaDB(VectorStoreBase):
vector_id (str): ID of the vector to retrieve.
Returns:
OutputData: Retrieved vector.
Optional[OutputData]: Retrieved vector, or None if the ID is not found.
"""
result = self.collection.get(ids=[vector_id])
return self._parse_output(result)[0]
parsed = self._parse_output(result)
return parsed[0] if parsed else None
def list_cols(self) -> List[chromadb.Collection]:
"""
+4 -2
View File
@@ -290,7 +290,7 @@ class MilvusDB(VectorStoreBase):
schema = {"id": vector_id, "vectors": vector, "metadata": payload, "text": text}
self.client.upsert(collection_name=self.collection_name, data=schema)
def get(self, vector_id):
def get(self, vector_id) -> Optional[OutputData]:
"""
Retrieve a vector by ID.
@@ -298,9 +298,11 @@ class MilvusDB(VectorStoreBase):
vector_id (str): ID of the vector to retrieve.
Returns:
OutputData: Retrieved vector.
Optional[OutputData]: Retrieved vector, or None if the ID is not found.
"""
result = self.client.get(collection_name=self.collection_name, ids=vector_id)
if not result:
return None
output = OutputData(
id=result[0].get("id", None),
score=None,
+1 -1
View File
@@ -391,7 +391,7 @@ class MongoDB(VectorStoreBase):
"""Reset the index by deleting and recreating it."""
logger.warning(f"Resetting index {self.collection_name}...")
self.delete_col()
self.collection = self.create_col(self.collection_name)
self.collection = self.create_col()
def __del__(self) -> None:
"""Close the database connection when the object is deleted."""
+1 -1
View File
@@ -230,7 +230,7 @@ class PineconeDB(VectorStoreBase):
if filter_dict:
query_params["filter"] = filter_dict
if self.hybrid_search and self.sparse_encoder and "text" in filters:
if self.hybrid_search and self.sparse_encoder and filters and "text" in filters:
query_text = filters.get("text")
if query_text:
sparse_vector = self.sparse_encoder.encode_queries(query_text)
+12 -6
View File
@@ -142,8 +142,11 @@ class RedisDB(VectorStoreBase):
self.index.load(data, id_field="memory_id")
def search(self, query: str, vectors: list, top_k: int = 5, filters: dict = None):
conditions = [Tag(key) == value for key, value in filters.items() if value is not None]
filter = reduce(lambda x, y: x & y, conditions)
filter = None
if filters:
conditions = [Tag(key) == value for key, value in filters.items() if value is not None]
if conditions:
filter = reduce(lambda x, y: x & y, conditions)
v = VectorQuery(
vector=np.array(vectors, dtype=np.float32).tobytes(),
@@ -312,11 +315,14 @@ class RedisDB(VectorStoreBase):
"""
List all recent created memories from the vector store.
"""
conditions = [Tag(key) == value for key, value in filters.items() if value is not None]
filter = reduce(lambda x, y: x & y, conditions)
query = Query(str(filter)).sort_by("created_at", asc=False)
filter = None
if filters:
conditions = [Tag(key) == value for key, value in filters.items() if value is not None]
if conditions:
filter = reduce(lambda x, y: x & y, conditions)
query = Query(str(filter) if filter is not None else "*").sort_by("created_at", asc=False)
if top_k is not None:
query = Query(str(filter)).sort_by("created_at", asc=False).paging(0, top_k)
query = query.paging(0, top_k)
results = self.index.search(query)
return [
+1 -1
View File
@@ -176,7 +176,7 @@ class Supabase(VectorStoreBase):
"""
result = self.collection.fetch([(vector_id,)])
if not result:
return []
return None
record = result[0]
return OutputData(id=str(record.id), score=None, payload=record.metadata)
+5 -6
View File
@@ -305,7 +305,7 @@ class Weaviate(VectorStoreBase):
existing_payload: Mapping[str, str] = existing_data
collection.data.update(uuid=vector_id, properties=existing_payload, vector=vector)
def get(self, vector_id):
def get(self, vector_id) -> Optional[OutputData]:
"""
Retrieve a vector by ID.
@@ -313,7 +313,7 @@ class Weaviate(VectorStoreBase):
vector_id: ID of the vector to retrieve.
Returns:
dict: Retrieved vector and metadata.
Optional[OutputData]: Retrieved vector, or None if the ID is not found.
"""
vector_id = get_valid_uuid(vector_id)
collection = self.client.collections.get(str(self.collection_name))
@@ -322,9 +322,8 @@ class Weaviate(VectorStoreBase):
uuid=vector_id,
return_properties=["hash", "created_at", "updated_at", "user_id", "agent_id", "run_id", "data", "category"],
)
# results = {}
# print("reponse",response)
# for obj in response.objects:
if response is None:
return None
payload = response.properties.copy()
payload["id"] = str(response.uuid).split("'")[0]
results = OutputData(
@@ -389,4 +388,4 @@ class Weaviate(VectorStoreBase):
"""Reset the index by deleting and recreating it."""
logger.warning(f"Resetting index {self.collection_name}...")
self.delete_col()
self.create_col()
self.create_col(self.embedding_model_dims)
+6 -6
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0ai"
version = "2.0.5"
version = "2.0.6"
description = "Long-term memory for AI Agents"
authors = [
{ name = "Mem0", email = "support@mem0.ai" }
@@ -17,7 +17,7 @@ dependencies = [
"qdrant-client>=1.12.0",
"pydantic>=2.7.3",
"openai>=1.90.0",
"posthog>=4.5.0",
"posthog>=7.14.0",
"pytz>=2024.1",
"sqlalchemy>=2.0.31",
"protobuf>=5.29.6,<7.0.0",
@@ -27,7 +27,7 @@ dependencies = [
nlp = [
"spacy>=3.7.0",
]
vector_stores = [
vector-stores = [
"vecs>=0.4.0",
"chromadb>=0.4.24",
"cassandra-driver>=3.29.0",
@@ -103,7 +103,7 @@ only-include = ["mem0"]
python = "3.10"
features = [
"test",
"vector_stores",
"vector-stores",
"llms",
"extras",
]
@@ -112,7 +112,7 @@ features = [
python = "3.11"
features = [
"test",
"vector_stores",
"vector-stores",
"llms",
"extras",
]
@@ -121,7 +121,7 @@ features = [
python = "3.12"
features = [
"test",
"vector_stores",
"vector-stores",
"llms",
"extras",
]
+28
View File
@@ -424,3 +424,31 @@ class TestMiniMaxProvider:
assert msg["role"] != "system"
# user message must be present
assert kwargs["messages"][0]["role"] == "user"
# ---------------------------------------------------------------------------
# _parse_response — legacy InvokeModel provider-specific parsing
# ---------------------------------------------------------------------------
class TestParseResponseLegacy:
def test_ai21_missing_completions_returns_empty(self, mock_boto3):
"""When AI21 response lacks 'completions', the fallback default must
be a valid dict (not a set literal), returning empty string."""
llm = _make_llm("ai21.j2-mid-v1", mock_boto3)
import io
import json
body = io.BytesIO(json.dumps({"not_completions": True}).encode())
response = {"body": body}
result = llm._parse_response(response, tools=None)
assert result == ""
def test_ai21_normal_response(self, mock_boto3):
llm = _make_llm("ai21.j2-mid-v1", mock_boto3)
import io
import json
body = io.BytesIO(json.dumps({
"completions": [{"data": {"text": "hello from ai21"}}]
}).encode())
response = {"body": body}
result = llm._parse_response(response, tools=None)
assert result == "hello from ai21"
@@ -29,6 +29,7 @@ class DummyConfig:
max_tokens=256,
top_p=1.0,
http_client=None,
reasoning_effort=None,
):
self.model = model
self.azure_kwargs = azure_kwargs or DummyAzureKwargs()
@@ -36,6 +37,7 @@ class DummyConfig:
self.max_tokens = max_tokens
self.top_p = top_p
self.http_client = http_client
self.reasoning_effort = reasoning_effort
@mock.patch("mem0.llms.azure_openai_structured.AzureOpenAI")
@@ -119,6 +121,44 @@ def test_generate_response_without_tools(mock_azure_openai):
assert response == "Hello there!"
@mock.patch("mem0.llms.azure_openai_structured.AzureOpenAI")
def test_generate_response_gpt5_uses_max_completion_tokens(mock_azure_openai):
mock_client = Mock()
mock_azure_openai.return_value = mock_client
config = DummyConfig(model="gpt-5.4-mini", azure_kwargs=DummyAzureKwargs(api_key="real-key"))
llm = AzureOpenAIStructuredLLM(config)
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Hi"))]
mock_client.chat.completions.create.return_value = mock_response
llm.generate_response([{"role": "user", "content": "Hi"}])
_, kwargs = mock_client.chat.completions.create.call_args
assert kwargs["max_completion_tokens"] == 256
assert "max_tokens" not in kwargs
@mock.patch("mem0.llms.azure_openai_structured.AzureOpenAI")
def test_generate_response_legacy_model_uses_max_tokens(mock_azure_openai):
mock_client = Mock()
mock_azure_openai.return_value = mock_client
config = DummyConfig(model="gpt-4.1", azure_kwargs=DummyAzureKwargs(api_key="real-key"))
llm = AzureOpenAIStructuredLLM(config)
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Hi"))]
mock_client.chat.completions.create.return_value = mock_response
llm.generate_response([{"role": "user", "content": "Hi"}])
_, kwargs = mock_client.chat.completions.create.call_args
assert kwargs["max_tokens"] == 256
assert "max_completion_tokens" not in kwargs
@mock.patch("mem0.llms.azure_openai_structured.AzureOpenAI")
def test_generate_response_with_tools(mock_azure_openai):
mock_client = Mock()
@@ -171,3 +211,56 @@ def test_generate_response_with_tools_no_tool_calls(mock_azure_openai):
assert response["content"] == "No tools needed."
assert response["tool_calls"] == []
@mock.patch("mem0.llms.azure_openai_structured.AzureOpenAI")
def test_reasoning_model_drops_sampling_params(mock_azure_openai):
"""Reasoning models (o1/o3/GPT-5) reject temperature/max_tokens/top_p."""
mock_client = Mock()
mock_azure_openai.return_value = mock_client
config = DummyConfig(
model="o3-mini",
azure_kwargs=DummyAzureKwargs(api_key="real-key"),
reasoning_effort="low",
)
llm = AzureOpenAIStructuredLLM(config)
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="ok"))]
mock_client.chat.completions.create.return_value = mock_response
llm.generate_response([{"role": "user", "content": "Hi"}])
call_kwargs = mock_client.chat.completions.create.call_args[1]
assert "temperature" not in call_kwargs # reasoning models reject these
assert "max_tokens" not in call_kwargs
assert "top_p" not in call_kwargs
assert call_kwargs["reasoning_effort"] == "low"
assert call_kwargs["model"] == "o3-mini"
@mock.patch("mem0.llms.azure_openai_structured.AzureOpenAI")
def test_regular_model_sends_sampling_params(mock_azure_openai):
"""Regular models still receive the standard sampling params."""
mock_client = Mock()
mock_azure_openai.return_value = mock_client
config = DummyConfig(
model="gpt-4o",
azure_kwargs=DummyAzureKwargs(api_key="real-key"),
temperature=0.3,
)
llm = AzureOpenAIStructuredLLM(config)
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="ok"))]
mock_client.chat.completions.create.return_value = mock_response
llm.generate_response([{"role": "user", "content": "Hi"}])
call_kwargs = mock_client.chat.completions.create.call_args[1]
assert call_kwargs["temperature"] == 0.3
assert "max_tokens" in call_kwargs # standard sampling params still forwarded
assert "top_p" in call_kwargs
assert call_kwargs["model"] == "gpt-4o"

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