Compare commits

...

19 Commits

Author SHA1 Message Date
Indian-boult dc82354e14 docs(skills): fix dead links in skills READMEs (#7092) 2026-08-24 18:25:33 +05:30
Kartik 4ddee9c51d chore(release): bump SDK, CLI, and plugin versions; add Strands, DeepSeek Harness, and Kimi changelogs (#7097) 2026-08-24 18:10:44 +05:30
Himanshu 7e09615571 feat(integrations): dsh-mem0 — Mem0 as a native DeepSeek Harness plugin (#7027)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-08-24 14:03:50 +05:30
Kartik d18e751dec docs: redirect five dead api-reference paths to their real pages (#7094) 2026-08-24 13:53:04 +05:30
Himanshu 8d5b7865bd feat(integrations): strands-mem0 | Mem0 as a native Strands MemoryStore (#7021) 2026-08-22 19:04:24 +05:30
Abhinav Singh 9b565da8e3 docs(embedders): document api_key on the Hugging Face Python config table (#7045) 2026-08-22 13:51:50 +05:30
Yiheng Zhao 48d0d0cd9c fix(docs): balance code fences in cookbook_template.mdx (#7054) 2026-08-22 13:50:49 +05:30
Kartik feb12852c0 fix(docs): redirect the eight 404 paths and repair dead wildcard rules (#7053) 2026-08-21 19:31:37 +05:30
Harsh Vardhan Gupta 5af797834c fix(security): resolve 17 Vanta/Dependabot HIGH+CRITICAL vulnerabilities across 5 pnpm workspaces (#7032) 2026-08-21 17:41:41 +05:30
Kartik 4fa4839077 fix(ci): make the vouch check speak, unblock list updates, widen the docs exemption (#6974) 2026-08-20 23:13:42 +05:30
Kartik 3599aa75ed docs: document the real search filter grammar (#6906) 2026-08-20 21:33:21 +05:30
Himanshu 1de6499b8a fix(integrations/zapier): address Zapier publishing review (#6985) 2026-08-20 18:53:06 +05:30
Kartik ed38ddf873 fix(python): huggingface TEI auth, procedural-memory content handling, and proxy pip auto-install (#6947) 2026-08-20 15:56:55 +05:30
Kartik 530d802b55 fix(plugins): bug-bash fixes for Cursor, Codex, Antigravity, and a Claude.ai docs page (#6948) 2026-08-20 15:56:17 +05:30
Kartik d3334fa5f1 docs: ground the platform/OSS comparison and memory-type status in reality (#6908) 2026-08-20 15:26:22 +05:30
Kartik 52b02c7cc1 docs: fix Claude Desktop MCP setup, CrewAI guide, and missing contributor docs (#6945) 2026-08-20 15:24:05 +05:30
mintlify[bot] 001c235229 Fix broken links: remove duplicate reranking redirect (#6975)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-08-14 12:47:17 +00:00
Kartik bf2d591b27 docs: remove Controlling Memory Ingestion cookbook, redirect to Custom Instructions (#6955) 2026-08-14 18:16:36 +05:30
Kartik b4c50550bf docs: correct client call shape and stale v1 response examples (#6901) 2026-08-14 18:13:37 +05:30
122 changed files with 7085 additions and 1563 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0",
"source": "./integrations/mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
"version": "0.2.14"
"version": "0.2.15"
}
]
}
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0",
"source": "./integrations/mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
"version": "0.2.14"
"version": "0.2.15"
}
]
}
+58 -5
View File
@@ -21,12 +21,17 @@ Package workflows keep their own push-to-main and manual triggers. Their `pull_r
| Mem0 Plugin | `mem0-plugin-checks.yml` | Push to main (`integrations/mem0-plugin/`, excluding `.opencode-plugin/`), manual | pytest + hook exec bits + JSON manifest validation on Python 3.10, 3.11, 3.12 |
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (`.opencode-plugin/`), manual | Bun: tsc + build + dist artifact check |
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (`integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup on Node 20, 22 |
| DeepSeek Harness Plugin | `dsh-mem0-checks.yml` | Push to main (`integrations/dsh-mem0/`), manual | tsc + vitest + tsup on Node 20, 22 |
| n8n Node | `n8n-nodes-mem0-checks.yml` | Push to main (`integrations/n8n-nodes-mem0/`), manual | ESLint + tsc build on Node 20 |
| Zapier App | `zapier-mem0-checks.yml` | Push to main (`integrations/zapier-mem0/`), manual | tsc + `zapier validate` + offline unit tests on Node 22 |
| strands-mem0 | `strands-mem0-checks.yml` | Push to main (`integrations/strands-mem0/`), manual | Ruff + mypy + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage |
| GitHub Scripts | inline in `ci-gate.yml` | none | `node` over every `.github/scripts/*.test.js` |
Adding a package CI workflow: give it `workflow_call` plus `push` / `workflow_dispatch` as needed but **no `pull_request` trigger**, then register it in `ci-gate.yml` with a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
`GitHub Scripts` is the one row that is a plain job inside `ci-gate.yml` rather than a called workflow, because a reusable workflow wrapping two `node` invocations would be more file than test. It runs on the `github_scripts` filter, which covers `.github/scripts/**` plus every file those tests read: `pr-gate.yml`, `vouch-check-pr.yml`, `issue-labeler.yml`, and `VOUCHED.td`. Add a new `.github/scripts/*.test.js` and it is picked up with no wiring; make a test read a new file and that file belongs in the filter.
## Branch protection on `main`
A repository ruleset named `Main Branch Rule`, id `11813754`. It enforces squash-only merges, linear history, no deletion, no force-push, and one approving review. Two status checks belong in its `required_status_checks` rule:
@@ -55,7 +60,9 @@ Requiring `CI Gate` also means fork PRs from first-time contributors cannot merg
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
| DeepSeek Harness Plugin | `dsh-mem0-cd.yml` | `dsh-mem0-v*` | npm (`@mem0/dsh-mem0`) |
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`@mem0/n8n-nodes-mem0`) |
| strands-mem0 | `strands-mem0-cd.yml` | `strands-mem0-v*` | PyPI (`strands-mem0`) |
- Package CD workflows are `workflow_dispatch`-only, with `tag` and `prerelease` inputs. They check out and build the given tag.
- All publishing uses **OIDC trusted publishing**. No tokens, no secrets.
@@ -69,9 +76,9 @@ Requiring `CI Gate` also means fork PRs from first-time contributors cannot merg
| Workflow | File | Purpose |
|----------|------|---------|
| PR Gate | `pr-gate.yml` | Closes PRs that do not link an issue labeled `accepted`, with a reopen path. Exempts members, bots, drafts, and docs-only changes. Never checks out PR code. |
| Vouch (check PR) | `vouch-check-pr.yml` | Comments on PRs from authors absent from `VOUCHED.td`. Comment-only mode (`auto-close: false`). |
| Vouch (manage list) | `vouch-manage-by-issue.yml` | Maintainers edit the trust list by commenting `!vouch @user`, `!denounce @user`, or `!unvouch @user` on any issue. Commits back to `VOUCHED.td` through a GitHub App token. |
| PR Gate | `pr-gate.yml` | Closes PRs that do not link an issue labeled `accepted`, and reopens them when that label arrives. Exempts members, bots, drafts, and docs-only changes. Never checks out PR code. |
| Vouch (check PR) | `vouch-check-pr.yml` | Closes PRs from authors denounced in `VOUCHED.td`. Comments once on PRs from authors merely absent from it, and blocks nothing in that case. |
| Vouch (manage list) | `vouch-manage-by-issue.yml` | Maintainers edit the trust list by commenting `!vouch @user`, `!denounce @user`, or `!unvouch @user` on any issue. Opens a PR against `VOUCHED.td` through a GitHub App token, for a maintainer to merge. |
| Issue Labeler | `issue-labeler.yml` | Labels issues from the `component` field in the issue forms |
| PR Labeler | `pr-labeler.yml` | Path-based labels, plus propagating labels from linked issues |
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
@@ -79,7 +86,53 @@ Requiring `CI Gate` also means fork PRs from first-time contributors cannot merg
`pr-gate.yml` and `vouch-check-pr.yml` use `pull_request_target`, which is required to label and close fork PRs. Neither checks out PR code and neither has a `run:` step, so there is no pwn-request or script-injection surface. Keep it that way: any future `run:` step in these files must never interpolate `github.event.*` text.
`GATE_EFFECTIVE_FROM` in `pr-gate.yml` is a `created_at` cutoff. `edited`, `reopened`, and `ready_for_review` fire on PRs opened long before the gate existed, so without the cutoff the whole open backlog would be closed by a rule that did not exist when those PRs were filed. Set it to the actual merge date in UTC.
Both workflows exempt maintainers twice, and the second guard is the one that holds. `author_association` is rendered for the viewer, and a webhook payload has no privileged viewer: `MEMBER` needs the author's org membership to be **public**, `COLLABORATOR` needs a **direct** repository invite. An org member with private membership whose `maintain` comes through a team matches neither and arrives as `CONTRIBUTOR`, which is how PR #6948 was closed by its own author's gate. So the guard also skips any PR whose head branch lives in this repository (`head.repo.full_name == github.repository`). Pushing a branch here already requires write access and outside contributors always arrive from a fork, so that test means the same thing without depending on who is looking. Keep both: the `author_association` arm still covers members who work from their own fork.
`pr-gate.yml` carries two jobs whose `if:` conditions are deliberately disjoint. `gate` closes, and only ever runs on `opened`, `reopened`, and `ready_for_review`. `reopen` reopens, and only ever runs on `edited` or on `issues: labeled` with the `accepted` label. Nothing can both close and reopen on the same event, which is the property to preserve when editing either guard.
That split exists because the two halves of a gated PR's recovery arrive in either order. A maintainer usually labels the issue `accepted` at triage, before the author has linked it; sometimes the link lands first and the label follows. So `reopen` handles both directions. From `issues: labeled` it walks `closedByPullRequestsReferences` back to the pull requests that link the issue. From `edited` it takes the edited pull request directly. Both paths then apply the same four tests: the author is not denounced in `VOUCHED.td`, the PR is `CLOSED`, it links an issue labeled `accepted`, and it carries the `<!-- pr-gate -->` marker comment. Without the label path, a maintainer's label is inert. Without the `edited` path, an author who links the issue after it was labeled is stuck, since no other event fires.
The denounce test is what keeps the two gates from cancelling each other out. A denounced author whose PR also lacked an accepted issue was closed by both workflows, so it carries the `<!-- pr-gate -->` marker, and labeling the linked issue would otherwise reopen it. Vouch cannot undo that: reopening runs through `GITHUB_TOKEN`, which raises no events, so `vouch-check-pr.yml` never fires a second time. Reading the list here is the only place the check can live. It fails open like vouch does, warning and treating nobody as denounced if the file cannot be read, and it is the one piece of vouch semantics duplicated outside `vouch-check-pr.yml`, because `pr-gate.yml` never checks out the repository and so cannot import a shared parser. `.github/scripts/vouch-decision.test.js` covers the parsing and asserts `pr-gate.yml` still filters the list the same way.
`edited` must never reach the `gate` job. It fires on any title or description change, so when `gate` listened for it the gate re-judged pull requests that had been open for days and closed them the moment their author touched the description, which is what closed #6948. Rescuing on `edited` is safe for the same reason closing on it was not: the job can only move a PR from closed to open.
Reopening runs through `GITHUB_TOKEN`, which by design raises no further workflow events, so `gate` cannot bounce a freshly reopened PR straight back out.
The concurrency group is keyed on `github.event.action` as well as `github.event_name` and the number, and both keys carry weight. Without the event name, a maintainer applying `bug` right after `accepted` cancels the reopen mid-flight, since `cancel-in-progress` is on for `pull_request_target` and both label events would land in the same group. Without the action, `opened` and `edited` share a group on the same pull request, and an author who ticks a template checkbox in the seconds after opening cancels the run that was about to gate them: `gate` skips `edited` and `reopen` skips an open pull request, so the cancelled run is never replaced and the pull request stays ungated forever, since `opened` fires exactly once. Rapid successive edits still cancel each other, which is the dedup that was wanted.
The `edited` arm of `reopen` requires `github.event.pull_request.state == 'closed'`, so ordinary description edits on open pull requests do not start a runner.
Two known gaps, both mild. A PR that the gate closed, that someone reopened, and that a maintainer then closed deliberately still carries the marker, so labeling its issue reopens it again; a maintainer closes it once more. And an author who strips `Closes #<number>` out after passing keeps an open PR, which a reviewer sees anyway.
`GATE_EFFECTIVE_FROM` in `pr-gate.yml` is a `created_at` cutoff. `reopened` and `ready_for_review` still fire on PRs opened long before the gate existed, so without the cutoff part of the open backlog would be closed by a rule that did not exist when those PRs were filed. Set it to the actual merge date in UTC.
The gate's docs-only exemption covers `docs/` plus a named allowlist of four root files: `README.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md`. It is an allowlist rather than a rule about top-level markdown because the repository root also holds `AGENTS.md`, `CLAUDE.md`, and `LLM.md`, which are the instructions coding agents read before touching this codebase. Those are functional files that happen to be written in prose, and rewriting them is a change to behaviour, so they stay gated. Markdown nested anywhere else stays gated for the same reason: `skills/**/*.md` and everything under `.github/` are functional too. Adding a genuinely prose root file means adding it to `rootDocs` in `pr-gate.yml`.
`.github/scripts/pr-gate-docs-exemption.test.js` covers that predicate. It pulls the `rootDocs` and `isDocs` lines out of `pr-gate.yml` and evaluates them, so it exercises the shipped rule rather than a copy that could drift from it, and it pins `AGENTS.md`, `CLAUDE.md`, and `LLM.md` on the gated side along with `skills/**/*.md`, nested `.github/` files, and the empty file list. It only accepts those two declarations in a literal one-line form, so keep `rootDocs` a `Set` of quoted names and `isDocs` a single arrow expression.
The two contribution gates answer different questions and neither covers for the other. `pr-gate.yml` judges the change, and the `accepted` label is how a maintainer says yes to it. `vouch-check-pr.yml` judges the author, and `VOUCHED.td` is how a maintainer says no to one. A vouched author with no accepted issue is still closed by the gate; a denounced author with an accepted issue is still closed by vouch. Read either one as a backstop for the other and both get weakened.
Vouch enforces on the denounce axis only, through `require-vouch: false` with `auto-close: true`. That pair is not the obvious reading of either input, so the decision table from v1.5.0 (`vouch/github.nu` at pinned SHA `d66fa29`) is worth stating outright:
| Author | `status` | Effect |
|---|---|---|
| ends in `[bot]` | `skipped` | nothing |
| collaborator with write or admin | `vouched` | nothing |
| listed in `VOUCHED.td` | `vouched` | nothing |
| listed as `-handle` | `closed` | action comments and closes |
| absent from the file | `allowed` | workflow comments, nothing closed |
`require-vouch: true` would close every first-time contributor, which is the opposite of what a trust list is for: the funnel has to stay open or nobody ever earns a vouch. `auto-close: false` is the setting that looked safe and did nothing at all, since in v1.5.0 both the unvouched and the denounced branch return before posting anything, leaving only a line in the run log. That is why `!denounce` was decorative until this pair landed.
Only the `allowed` arm is ours: a `github-script` step posts the soft comment, keyed on a `<!-- vouch-check -->` marker so a reopen does not comment twice. The `closed` arm belongs to the action, message and all. Keeping the two arms disjoint is what stops a denounced author getting two comments, so if that step is ever re-keyed off `allowed`, check the overlap first.
`.github/scripts/vouch-decision.test.js` holds that table as a `decide()` function and asserts the workflow's `require-vouch`, `auto-close`, and comment-step gating still produce it, comment counts included. Be clear about what that does and does not prove. `decide()` is a **hand transcription** of `gh-check-pr`, read from `vouch/github.nu` at the pinned SHA; the test cannot run the action, so it cannot notice the action changing underneath it. Left alone it would agree with itself forever, which makes bumping the pinned SHA the one edit it would otherwise sail through. So it also asserts `vouch-check-pr.yml` still pins `PINNED_VOUCH_SHA`, and a bump fails it on purpose: re-read `gh-check-pr` at the new revision, correct `decide()` and the table above, then move the constant. CI runs it through the `GitHub Scripts` job on any change to the scripts or the files they read.
Failure is open by design. If the action cannot read `VOUCHED.td` it falls back to an empty list, every author reads as absent, and nobody is closed by an API hiccup.
`vouch-manage-by-issue.yml` runs with `merge-immediately: "false"`. The `Main Branch Rule` ruleset requires one approving review and has no bypass actors, so the action's immediate `PUT /pulls/{n}/merge` would return 405 and leave `VOUCHED.td` unchanged on `main`. The bot opens the PR, a maintainer merges it. Setting `pull-request: "false"` is not an alternative: the same ruleset blocks direct pushes.
That workflow also needs `VOUCH_APP_ID` and `VOUCH_APP_PRIVATE_KEY` repository secrets. Without them it fails at the token step before doing anything. `vouch-check-pr.yml` needs neither.
## Issue forms and templates
@@ -99,4 +152,4 @@ Current field ids:
`VOUCHED.td` is one GitHub username per line, `#` for comments. Seeded from every author with at least one merged PR in this repository, then filtered: accounts at or below a 16% merge rate across five or more attempts were dropped, since landing one change out of many is the signature of automated submission rather than contribution.
Vouch's only built-in exemptions are accounts ending in `[bot]` and repo collaborators with `write` or `admin`. **Organization membership alone is not one of them.** So `vouch-check-pr.yml` carries a job-level `if:` that skips the check for `OWNER`, `MEMBER`, and `COLLABORATOR` authors, the same exemption `pr-gate.yml` already applies. `author_association` is `MEMBER` for every org member regardless of repository permission, so no member can be flagged even if their `VOUCHED.td` entry is missing, misspelled, or miscased. Org members are still listed in the file as a fallback, but the workflow guard is what actually holds.
Vouch's only built-in exemptions are accounts ending in `[bot]` and repo collaborators with `write` or `admin`. **Organization membership alone is not one of them.** So `vouch-check-pr.yml` carries a job-level `if:` that skips the check for `OWNER`, `MEMBER`, and `COLLABORATOR` authors, the same exemption `pr-gate.yml` already applies. Org members are still listed in the file as a fallback, but the workflow guard is what actually holds.
+20 -6
View File
@@ -1,15 +1,29 @@
# The list of vouched (or denounced) users for this repository.
#
# Only vouched users can open pull requests here. A denounced user (prefixed
# with a minus) is blocked outright.
# A denounced user (prefixed with a minus) is blocked outright: their pull
# requests are closed on sight, whatever they link. Being absent from this file
# blocks nothing. An unvouched author gets one comment saying so and their pull
# request is reviewed like anyone else's, because a first contribution has to
# start somewhere. Vouching is how that comment stops.
#
# This list is about who, and it is the only thing that judges who. Whether a
# change is wanted is a separate question, answered by the accepted label and
# enforced by pr-gate.yml. Neither gate substitutes for the other: a vouched
# author still needs an accepted issue, and a denounced author is turned away
# even holding one.
#
# Vouch automatically allows two kinds of account without consulting this file:
# accounts ending in [bot], and repo collaborators with write or admin
# permission. Org membership on its own is NOT one of them, so
# vouch-check-pr.yml skips the check entirely for OWNER, MEMBER, and
# COLLABORATOR authors. mem0ai org members are listed below as well, but that
# workflow guard is what actually protects them: a missing, misspelled, or
# miscased entry here can never cause a member to be flagged.
# vouch-check-pr.yml skips the check for OWNER, MEMBER, and COLLABORATOR
# authors, and for any branch pushed to this repository.
#
# Keep every mem0ai member listed below anyway. The author_association arm of
# that guard is weaker than it looks: MEMBER needs the member's org membership
# to be public and COLLABORATOR needs a direct repo invite, so a member with
# private membership and a team-derived role reads as CONTRIBUTOR. Working from
# a branch here covers them, working from their own fork leaves this file as
# the only thing that does. A missing or miscased entry is a real gap.
#
# Syntax:
# - One handle per line (without @), sorted alphabetically.
@@ -0,0 +1,60 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const gate = fs.readFileSync(path.join(__dirname, '..', 'workflows', 'pr-gate.yml'), 'utf8');
const rootDocsLine = gate.match(/^\s*(const rootDocs = new Set\(\['[\w.-]+'(?:, '[\w.-]+')*\]\);)\s*$/m);
const isDocsLine = gate.match(/^\s*(const isDocs = \(\w+\) => [\w.'"()[\]\/, |&!=><+-]+;)\s*$/m);
assert.ok(
rootDocsLine,
'pr-gate.yml no longer declares rootDocs as a single-line Set of quoted filenames. ' +
'This test evaluates that line to exercise the shipped predicate rather than a copy of it, ' +
'and only accepts a literal shape, so widen the pattern deliberately or keep the declaration literal.',
);
assert.ok(
isDocsLine,
'pr-gate.yml no longer declares isDocs as a single-line arrow expression. ' +
'This test evaluates that line to exercise the shipped predicate rather than a copy of it, ' +
'and refuses anything with a statement body, so keep it an expression.',
);
const isDocs = new Function(`${rootDocsLine[1]}\n${isDocsLine[1]}\nreturn isDocs;`)();
const exempt = (files) => files.length > 0 && files.every(isDocs);
const cases = [
[['docs/a.mdx'], true],
[['docs/platform/quickstart.mdx'], true],
[['README.md'], true],
[['CONTRIBUTING.md'], true],
[['CODE_OF_CONDUCT.md'], true],
[['SECURITY.md'], true],
[['README.md', 'CONTRIBUTING.md', 'docs/x.mdx'], true],
[['AGENTS.md'], false],
[['CLAUDE.md'], false],
[['LLM.md'], false],
[['README.md', 'AGENTS.md'], false],
[['README.md', 'mem0/memory/main.py'], false],
[['skills/mem0/SKILL.md'], false],
[['.github/AGENTS.md'], false],
[['.github/workflows/ci.yml'], false],
[['docs-site/index.md'], false],
[[], false],
];
let failures = 0;
for (const [files, expected] of cases) {
const actual = exempt(files);
const label = files.length ? files.join(', ') : '(no files)';
if (actual === expected) {
console.log(`ok ${label} -> ${actual ? 'exempt' : 'gated'}`);
} else {
failures += 1;
console.log(`FAIL ${label} -> ${actual ? 'exempt' : 'gated'}, expected ${expected ? 'exempt' : 'gated'}`);
}
}
console.log(failures === 0 ? '\nPASS' : `\nFAIL (${failures} cases)`);
process.exit(failures === 0 ? 0 : 1);
+122
View File
@@ -0,0 +1,122 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const workflowPath = path.join(__dirname, '..', 'workflows', 'vouch-check-pr.yml');
const workflow = fs.readFileSync(workflowPath, 'utf8');
const PINNED_VOUCH_SHA = 'd66fa29a64600490892131ad87597c30c91fcac4';
assert.ok(
workflow.includes(`mitchellh/vouch/action/check-pr@${PINNED_VOUCH_SHA}`),
`decide() below is a hand transcription of gh-check-pr from vouch/github.nu at ${PINNED_VOUCH_SHA} (v1.5.0). ` +
'It reads the action, it does not run it, so on its own it agrees with itself whatever the action does. ' +
'vouch-check-pr.yml now pins a different revision: re-read gh-check-pr there, update decide() and the ' +
'decision table in .github/AGENTS.md to match it, then set PINNED_VOUCH_SHA to the new SHA.',
);
const actionDefaults = { 'require-vouch': true, 'auto-close': false };
const booleanInput = (name) => {
const match = workflow.match(new RegExp(`^\\s+${name}:\\s*"?(true|false)"?\\s*$`, 'm'));
return match ? match[1] === 'true' : actionDefaults[name];
};
const requireVouch = booleanInput('require-vouch');
const autoClose = booleanInput('auto-close');
const commentedStatus = (() => {
const match = workflow.match(/steps\.vouch\.outputs\.status == '(\w+)'/);
assert.ok(match, 'the follow-up comment step is not keyed on a vouch status');
return match[1];
})();
const decide = (author) => {
if (author === 'bot') return { status: 'skipped', closed: false, actionComments: false };
if (author === 'collaborator' || author === 'vouched') {
return { status: 'vouched', closed: false, actionComments: false };
}
if (author === 'denounced') {
if (!autoClose) return { status: 'closed', closed: false, actionComments: false };
return { status: 'closed', closed: true, actionComments: true };
}
if (!requireVouch) return { status: 'allowed', closed: false, actionComments: false };
if (!autoClose) return { status: 'closed', closed: false, actionComments: false };
return { status: 'closed', closed: true, actionComments: true };
};
const outcome = (author) => {
const result = decide(author);
return { ...result, workflowComments: result.status === commentedStatus };
};
const cases = [
{ author: 'bot', closed: false, comments: 0 },
{ author: 'collaborator', closed: false, comments: 0 },
{ author: 'vouched', closed: false, comments: 0 },
{ author: 'unvouched', closed: false, comments: 1 },
{ author: 'denounced', closed: true, comments: 1 },
];
let failures = 0;
for (const expected of cases) {
const actual = outcome(expected.author);
const comments = Number(actual.actionComments) + Number(actual.workflowComments);
try {
assert.strictEqual(actual.closed, expected.closed, `${expected.author}: closed`);
assert.strictEqual(comments, expected.comments, `${expected.author}: comment count`);
console.log(`ok ${expected.author} -> ${actual.status}, closed=${actual.closed}, comments=${comments}`);
} catch (error) {
failures += 1;
console.log(`FAIL ${expected.author} -> ${actual.status}, closed=${actual.closed}, comments=${comments}`);
console.log(` ${error.message}: expected ${JSON.stringify(expected)}`);
}
}
console.log(`\nvouch@${PINNED_VOUCH_SHA.slice(0, 7)} require-vouch=${requireVouch} auto-close=${autoClose} comment-on=${commentedStatus}`);
const parseDenounced = (contents) => new Set(contents
.split('\n')
.map((line) => line.trim())
.filter((line) => line.startsWith('-'))
.map((line) => line.slice(1).split(/\s+/)[0].split(':').pop().toLowerCase())
.filter(Boolean));
const gate = fs.readFileSync(path.join(__dirname, '..', 'workflows', 'pr-gate.yml'), 'utf8');
assert.ok(
gate.includes(".filter((line) => line.startsWith('-'))"),
'pr-gate.yml no longer parses the denounce list the way this test does',
);
const vouched = fs.readFileSync(path.join(__dirname, '..', 'VOUCHED.td'), 'utf8');
const denouncedNow = parseDenounced(vouched);
const sample = parseDenounced([
'# -notacomment is a comment line',
'-SpamBot seeded 2026-08-12',
'-github:OtherSpammer',
'realcontributor',
'',
].join('\n'));
let parseFailures = 0;
for (const [label, actual, expected] of [
['denounce entry, with note', sample.has('spambot'), true],
['denounce entry, platform prefixed', sample.has('otherspammer'), true],
['comment line is not an entry', sample.has('notacomment'), false],
['vouched entry is not denounced', sample.has('realcontributor'), false],
['live file parses without throwing', denouncedNow instanceof Set, true],
]) {
try {
assert.strictEqual(actual, expected, label);
console.log(`ok ${label}`);
} catch (error) {
parseFailures += 1;
console.log(`FAIL ${label}: ${error.message}`);
}
}
console.log(`denounced in VOUCHED.td: ${denouncedNow.size}`);
const total = failures + parseFailures;
console.log(total === 0 ? 'PASS' : `FAIL (${total} assertions)`);
process.exit(total === 0 ? 0 : 1);
+53
View File
@@ -41,9 +41,12 @@ jobs:
mem0_plugin: ${{ steps.filter.outputs.mem0_plugin }}
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
dsh_mem0: ${{ steps.filter.outputs.dsh_mem0 }}
n8n_nodes_mem0: ${{ steps.filter.outputs.n8n_nodes_mem0 }}
zapier_mem0: ${{ steps.filter.outputs.zapier_mem0 }}
strands_mem0: ${{ steps.filter.outputs.strands_mem0 }}
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
github_scripts: ${{ steps.filter.outputs.github_scripts }}
steps:
- uses: dorny/paths-filter@v3
id: filter
@@ -87,6 +90,10 @@ jobs:
- 'integrations/pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
- '.github/workflows/ci-gate.yml'
dsh_mem0:
- 'integrations/dsh-mem0/**'
- '.github/workflows/dsh-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
n8n_nodes_mem0:
- 'integrations/n8n-nodes-mem0/**'
- '.github/workflows/n8n-nodes-mem0-checks.yml'
@@ -94,6 +101,10 @@ jobs:
- 'integrations/zapier-mem0/**'
- '.github/workflows/zapier-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
strands_mem0:
- 'integrations/strands-mem0/**'
- '.github/workflows/strands-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
docs_llms_txt:
- 'docs/**/*.mdx'
- 'docs/llms.txt'
@@ -101,6 +112,13 @@ jobs:
- 'scripts/llms-txt-ignore.txt'
- '.github/workflows/docs-llms-txt-check.yml'
- '.github/workflows/ci-gate.yml'
github_scripts:
- '.github/scripts/**'
- '.github/VOUCHED.td'
- '.github/workflows/pr-gate.yml'
- '.github/workflows/vouch-check-pr.yml'
- '.github/workflows/issue-labeler.yml'
- '.github/workflows/ci-gate.yml'
python-sdk:
name: Python SDK
@@ -158,6 +176,13 @@ jobs:
uses: ./.github/workflows/pi-agent-plugin-checks.yml
secrets: inherit
dsh-mem0:
name: DeepSeek Harness Plugin
needs: changes
if: needs.changes.outputs.dsh_mem0 == 'true'
uses: ./.github/workflows/dsh-mem0-checks.yml
secrets: inherit
n8n-nodes-mem0:
name: n8n Node
needs: changes
@@ -170,6 +195,13 @@ jobs:
uses: ./.github/workflows/zapier-mem0-checks.yml
secrets: inherit
strands-mem0:
name: strands-mem0
needs: changes
if: needs.changes.outputs.strands_mem0 == 'true'
uses: ./.github/workflows/strands-mem0-checks.yml
secrets: inherit
docs-llms-txt:
name: docs llms.txt
needs: changes
@@ -177,6 +209,24 @@ jobs:
uses: ./.github/workflows/docs-llms-txt-check.yml
secrets: inherit
github-scripts:
name: GitHub Scripts
needs: changes
if: needs.changes.outputs.github_scripts == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Run .github/scripts tests
run: |
for test in .github/scripts/*.test.js; do
echo "::group::$test"
node "$test"
echo "::endgroup::"
done
gate:
name: CI Gate
needs:
@@ -189,9 +239,12 @@ jobs:
- mem0-plugin
- opencode-plugin
- pi-agent-plugin
- dsh-mem0
- n8n-nodes-mem0
- zapier-mem0
- strands-mem0
- docs-llms-txt
- github-scripts
if: always()
runs-on: ubuntu-latest
steps:
+60
View File
@@ -0,0 +1,60 @@
name: Publish @mem0/dsh-mem0 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# dsh-mem0-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. dsh-mem0-v0.1.1)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/dsh-mem0 📦 to npm
if: startsWith(inputs.tag, 'dsh-mem0-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/dsh-mem0
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
+90
View File
@@ -0,0 +1,90 @@
name: dsh-mem0 checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/dsh-mem0/**'
- '.github/workflows/dsh-mem0-checks.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
- name: Type check
run: cd integrations/dsh-mem0 && pnpm exec tsc --noEmit
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
- name: Run tests
run: cd integrations/dsh-mem0 && pnpm exec vitest run
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
- name: Build
run: cd integrations/dsh-mem0 && pnpm build
- name: Verify dist output exists
run: |
test -f integrations/dsh-mem0/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
test -f integrations/dsh-mem0/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
+118 -7
View File
@@ -2,11 +2,13 @@ name: PR Gate
on:
pull_request_target:
types: [opened, reopened, edited, ready_for_review]
types: [opened, reopened, ready_for_review, edited]
issues:
types: [labeled]
concurrency:
group: pr-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
group: pr-gate-${{ github.event_name }}-${{ github.event.action }}-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
env:
GATE_EFFECTIVE_FROM: '2026-08-12T00:00:00Z'
@@ -19,8 +21,11 @@ permissions:
jobs:
gate:
if: >-
github.event_name == 'pull_request_target' &&
github.event.action != 'edited' &&
github.event.pull_request.draft == false &&
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.head.repo.full_name != github.repository &&
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
steps:
@@ -47,7 +52,9 @@ jobs:
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: pr.number, per_page: 100,
});
if (files.length > 0 && files.every((file) => file.filename.startsWith('docs/'))) {
const rootDocs = new Set(['README.md', 'CONTRIBUTING.md', 'CODE_OF_CONDUCT.md', 'SECURITY.md']);
const isDocs = (filename) => filename.startsWith('docs/') || rootDocs.has(filename);
if (files.length > 0 && files.every((file) => isDocs(file.filename))) {
core.info('Docs-only PR, gate skipped');
return;
}
@@ -75,6 +82,7 @@ jobs:
}
const body = [
'<!-- pr-gate -->',
'Thanks for taking the time to open this.',
'',
'We only review pull requests that fix an issue we have already agreed to take on, so this one is closed for now.',
@@ -84,10 +92,9 @@ jobs:
'',
'1. Make sure an issue describes the problem, with the version you are on, a runnable reproduction, and the real output or traceback you saw.',
'2. Link it from this pull request description with `Closes #<number>`.',
'3. Ask a maintainer to label that issue `accepted`.',
'4. Reopen this pull request. The check runs again and it stays open.',
'3. Ask a maintainer to label that issue `accepted`. This pull request reopens by itself when they do.',
'',
'Already linked an accepted issue? Edit the description to include `Closes #<number>` and reopen. The check reruns automatically.',
'Issue already labeled `accepted`? Just add `Closes #<number>` to the description. That reopens this too.',
'',
'Documentation-only changes skip this gate entirely.',
'',
@@ -101,3 +108,107 @@ jobs:
owner, repo, pull_number: pr.number, state: 'closed',
});
core.info(`Closed #${pr.number}: no accepted issue linked`);
reopen:
if: >-
(github.event_name == 'issues' && github.event.label.name == 'accepted') ||
(github.event.action == 'edited' && github.event.pull_request.state == 'closed')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const marker = '<!-- pr-gate -->';
const denounced = await (async () => {
try {
const { data } = await github.rest.repos.getContent({
owner, repo, path: '.github/VOUCHED.td',
ref: context.payload.repository.default_branch,
});
return new Set(Buffer.from(data.content, 'base64').toString('utf8')
.split('\n')
.map((line) => line.trim())
.filter((line) => line.startsWith('-'))
.map((line) => line.slice(1).split(/\s+/)[0].split(':').pop().toLowerCase())
.filter(Boolean));
} catch (error) {
core.warning(`Could not read VOUCHED.td, treating nobody as denounced: ${error.message}`);
return new Set();
}
})();
const isReopenable = async (number) => {
const { repository } = await github.graphql(
`query ($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
state
author { login }
closingIssuesReferences(first: 20) {
nodes { labels(first: 50) { nodes { name } } }
}
}
}
}`,
{ owner, repo, number },
);
const pullRequest = repository.pullRequest;
if (denounced.has(pullRequest.author?.login?.toLowerCase())) {
core.info(`#${number} is from a denounced author. Vouch outranks this gate.`);
return false;
}
return pullRequest.state === 'CLOSED' &&
pullRequest.closingIssuesReferences.nodes.some((issue) =>
issue.labels.nodes.some((label) => label.name === 'accepted'));
};
let candidates;
if (context.eventName === 'issues') {
const { repository } = await github.graphql(
`query ($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) {
closedByPullRequestsReferences(first: 20, includeClosedPrs: true) {
nodes { number }
}
}
}
}`,
{ owner, repo, number: context.payload.issue.number },
);
candidates = repository.issue.closedByPullRequestsReferences.nodes.map((pr) => pr.number);
} else {
candidates = [context.payload.pull_request.number];
}
for (const number of candidates) {
if (!(await isReopenable(number))) {
core.info(`#${number} is not a closed pull request linking an accepted issue. Skipped.`);
continue;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: number, per_page: 100,
});
if (!comments.some((comment) => comment.body?.startsWith(marker))) {
core.info(`#${number} was not closed by this gate. Left alone.`);
continue;
}
try {
await github.rest.pulls.update({
owner, repo, pull_number: number, state: 'open',
});
} catch (error) {
core.warning(`Could not reopen #${number}: ${error.message}`);
continue;
}
await github.rest.issues.createComment({
owner, repo, issue_number: number,
body: 'An `accepted` issue is linked now, so this is open again and ready for review.',
});
core.info(`Reopened #${number}`);
}
+2
View File
@@ -45,7 +45,9 @@ jobs:
openclaw-v*) workflow="openclaw-cd.yml" ;;
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
dsh-mem0-v*) workflow="dsh-mem0-cd.yml" ;;
n8n-nodes-mem0-v*) workflow="n8n-nodes-mem0-cd.yml" ;;
strands-mem0-v*) workflow="strands-mem0-cd.yml" ;;
v*) workflow="cd.yml" ;;
*)
echo "::error::Release tag '$TAG' does not match any known package prefix — nothing will be published. See the tag prefix table in AGENTS.md."
+50
View File
@@ -0,0 +1,50 @@
name: Publish strands-mem0 🐍 distribution 📦 to PyPI
# Dispatched by release.yml (Release Router) when a release tagged
# strands-mem0-v* is published. Can also be dispatched manually to re-publish
# a tag. Publishing uses PyPI Trusted Publishing (OIDC), so no API token is
# stored; the `strands-mem0` PyPI project must have a trusted publisher
# configured for mem0ai/mem0 + this workflow.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. strands-mem0-v0.1.0)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish strands-mem0 📦 to PyPI
if: startsWith(inputs.tag, 'strands-mem0-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/strands-mem0/python
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Hatch
run: pip install hatch
- name: Build a binary wheel and a source tarball
run: hatch build --clean
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: integrations/strands-mem0/python/dist/
+82
View File
@@ -0,0 +1,82 @@
name: strands-mem0 CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/strands-mem0/**'
- '.github/workflows/strands-mem0-checks.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dev dependencies
working-directory: integrations/strands-mem0/python
run: pip install -e ".[dev]"
- name: Lint with ruff
working-directory: integrations/strands-mem0/python
run: ruff check .
- name: Check formatting
working-directory: integrations/strands-mem0/python
run: ruff format --check .
- name: Type-check with mypy
working-directory: integrations/strands-mem0/python
run: mypy src
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dev dependencies
working-directory: integrations/strands-mem0/python
run: pip install -e ".[dev]"
- name: Run tests
working-directory: integrations/strands-mem0/python
run: pytest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Hatch
run: pip install hatch
- name: Build
working-directory: integrations/strands-mem0/python
run: hatch build --clean
- name: Verify dist output
run: |
ls integrations/strands-mem0/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
ls integrations/strands-mem0/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
+33 -1
View File
@@ -16,12 +16,44 @@ jobs:
check:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.head.repo.full_name != github.repository &&
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
steps:
- uses: mitchellh/vouch/action/check-pr@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
id: vouch
with:
pr-number: ${{ github.event.pull_request.number }}
auto-close: false
require-vouch: false
auto-close: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: steps.vouch.outputs.status == 'allowed'
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const marker = '<!-- vouch-check -->';
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100,
});
if (comments.some((comment) => comment.body?.startsWith(marker))) {
core.info('Vouch comment already posted, skipped.');
return;
}
const body = [
marker,
`Hi @${context.payload.pull_request.user.login}, thanks for opening this pull request.`,
'',
"This is just a soft check: you are not yet in this repo's vouched contributor list (`.github/VOUCHED.td`). Nothing is blocked and there is nothing you need to do.",
'',
`A maintainer can vouch for you by commenting \`!vouch @${context.payload.pull_request.user.login}\` on any issue.`,
].join('\n');
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number, body,
});
+1 -1
View File
@@ -37,6 +37,6 @@ jobs:
denounce-keyword: "!denounce"
unvouch-keyword: "!unvouch"
pull-request: "true"
merge-immediately: "true"
merge-immediately: "false"
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
+4
View File
@@ -191,3 +191,7 @@ qdrant_storage/
testing.ipynb
.weave/
# TypeScript incremental build info and local, uncommitted e2e scripts (used by the integrations, e.g. integrations/dsh-mem0)
*.tsbuildinfo
*.local.mjs
+21 -1
View File
@@ -10,6 +10,7 @@ This is a polyglot monorepo and **every package sets its own rules**. Read the `
## Do NOT
- Open a pull request without a signed CLA. It will not be reviewed. See [The CLA is not optional](#the-cla-is-not-optional).
- Open a pull request that does not link an issue carrying the `accepted` label. A bot closes it within a minute. See [Two gates decide whether your pull request stays open](#two-gates-decide-whether-your-pull-request-stays-open).
- Modify anything in `.github/workflows/` without explicit maintainer approval. Publishing credentials are pinned to workflow filenames.
- Commit `.env` files, API keys, or credentials.
- Skip pre-commit hooks.
@@ -121,6 +122,24 @@ Full guide: [`CONTRIBUTING.md`](CONTRIBUTING.md). Conduct: [`CODE_OF_CONDUCT.md`
6. Open the PR against `main` and fill in [the template](.github/PULL_REQUEST_TEMPLATE.md). Do not paraphrase it; GitHub prefills it.
7. **Sign the CLA.**
### Two gates decide whether your pull request stays open
Two workflows run on every pull request from a fork. They judge different things and neither covers for the other, so a pull request has to get past both.
**The [PR Gate](.github/workflows/pr-gate.yml) judges the change.** It closes any pull request that does not link an issue carrying the `accepted` label. Closed is a queue decision, not a verdict: when a maintainer applies the label the pull request reopens by itself. Drafts, documentation-only changes, and branches pushed to this repository rather than a fork are all exempt.
**The [vouch check](.github/workflows/vouch-check-pr.yml) judges the account.** It reads [`.github/VOUCHED.td`](.github/VOUCHED.td), which has three possible answers about any given person:
| The list says | Meaning | Effect on the pull request |
|---|---|---|
| `-handle` | a maintainer ran `!denounce` after the code of conduct process | closed, even with an accepted issue |
| nothing at all | everybody who has not contributed here before | **none.** One comment saying nothing is blocked. |
| `handle` | a maintainer ran `!vouch` | none, and the comment stops appearing |
Being vouched grants nothing. It is a "we have seen this person before" flag that mutes the newcomer comment, not permission to skip the accepted-issue rule. Being absent from the list costs nothing.
If you are an agent opening a pull request on someone's behalf, the practical consequence is one rule: **get the linked issue labelled `accepted` before you open the pull request, or expect the pull request to be closed and to reopen later.** Do not work around either gate, do not reopen a gated pull request by hand, and do not re-file the same change under a new pull request when one is closed.
### The CLA is not optional
**A pull request from a contributor who has not signed the Contributor License Agreement is not accepted, not reviewed, and not merged.** This is not a formality applied at merge time. An unsigned pull request does not enter the review queue at all: maintainers do not read the diff, do not leave feedback, and do not discuss the approach. It sits until the CLA is signed, and it is closed if it goes stale.
@@ -151,5 +170,6 @@ Beyond the CLA and the accepted-issue gate, the [Contribution Conduct](CODE_OF_C
| Documentation contributions | `docs/contributing/documentation.mdx` |
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
| Issue forms | `.github/ISSUE_TEMPLATE/` |
| Trust list (vouch) | `.github/VOUCHED.td` |
| Contribution gates | [Two gates decide whether your pull request stays open](#two-gates-decide-whether-your-pull-request-stays-open) |
| Trust list (vouch) | [`.github/VOUCHED.td`](.github/VOUCHED.td) |
| CI/CD, gates, rulesets | [`.github/AGENTS.md`](.github/AGENTS.md) |
+31 -3
View File
@@ -40,9 +40,24 @@ agree the change is one we want.
Pull requests that don't link an accepted issue are closed automatically by the
[PR Gate](./.github/workflows/pr-gate.yml). **Closed does not mean rejected.** It
means the change isn't in the queue yet. Once a maintainer labels the issue,
reopen the pull request and it stays open. Documentation-only changes skip the
gate entirely.
means the change isn't in the queue yet. Once a maintainer labels the issue the
pull request reopens itself, and you don't have to do anything. Documentation-only
changes skip the gate entirely.
A second check looks at who opened the pull request rather than what it changes.
If you are not yet in this repo's contributor list
([`.github/VOUCHED.td`](./.github/VOUCHED.td)) you get one comment saying so.
**Nothing is blocked and there is nothing you need to do.** A maintainer can add
you by commenting `!vouch @you` on any issue, which only stops that comment from
appearing again. Being on the list is not permission to skip the accepted-issue
rule, and being absent from it costs you nothing.
The list has a negative side too. A maintainer can `!denounce` an account that
has been through the
[code of conduct](./CODE_OF_CONDUCT.md#contribution-conduct) enforcement process,
and pull requests from that account are closed whether or not they link an
accepted issue. This is rare, it is never where anyone starts, and it is
reversible.
Security fixes are the one exception, and they don't go through public pull
requests at all. Follow the [Security Policy](./SECURITY.md) instead, which uses
@@ -85,6 +100,19 @@ sign. Signing takes less than a minute and only needs to be done once. Pull
requests from contributors who have not signed the CLA will be blocked from
merging.
## First Contribution Fast Path
Fixing a typo or a small docs issue? You don't need the full workflow below.
1. **Pick something small.** Look for issues labeled `documentation` or `good first issue`, or a typo/broken link you noticed while reading the docs.
2. **Branch from `main`** with a name that says what you're fixing, e.g. `docs/fix-quickstart-typo` or `fix/broken-crewai-link`.
3. **Make the change, then run only what applies:**
- Docs-only change (`docs/**`): preview with `make docs`. If you added or removed an `.mdx` page, run `python scripts/check-llms-txt-coverage.py --write` so `docs/llms.txt` stays in sync.
- Code change: run the linter and tests for the package you touched, see [Development Workflow](#development-workflow) below.
4. **Open a PR** against `main` with `Closes #<issue-number>` and a one-line description of what you fixed.
For anything larger than a docs fix or a small bug, follow the full workflow below.
## Repository Layout
The two most common contribution targets are the SDKs:
+1 -1
View File
@@ -11,7 +11,7 @@ install:
hatch env create
install_all:
pip install ruff==0.16.0 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
pip install ruff==0.16.0 groq together boto3 'litellm>=1.83.7,<1.98.0' ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk valkey
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/cli",
"version": "0.2.12",
"version": "0.2.13",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.2.11"
version = "0.2.12"
description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md"
license = "Apache-2.0"
+1 -1
View File
@@ -1,3 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.2.11"
__version__ = "0.2.12"
+49
View File
@@ -4,6 +4,55 @@ description: "Major product launches, headline features, and milestones for Mem0
mode: "wide"
---
<Update label="2026-08-24" description="DeepSeek Harness plugin">
**DeepSeek Harness: Mem0 as a Native Cordis Plugin**
The DeepSeek Harness agent forgets everything between sessions. [`@mem0/dsh-mem0`](https://www.npmjs.com/package/@mem0/dsh-mem0) gives it two Mem0-backed tools, so recall and writes persist across runs against the same memory bank you already use from Claude Code, Codex, and every other connected agent.
- **Two agent-callable tools:** `search_memory` recalls facts relevant to a query, `add_memory` stores a fact for future sessions. Both accept per-call `userId` / `agentId` / `runId` scope overrides.
- **Native Cordis lifecycle:** The plugin declares `inject = ['tools']` so it waits for the harness tool registry, then registers through `ctx.tools.register()`. Unmounting the plugin removes the tools automatically.
- **Managed backend, not a memory file:** Server-side extraction, semantic dedup, and conflict resolution, rather than a Markdown file the agent has to maintain itself.
- **Config:** `userId` is required, `apiKey` defaults to `$MEM0_API_KEY`, and `host` optionally targets a dedicated Mem0 Platform base URL.
See [DeepSeek Harness](/integrations/dsh-mem0) for setup and [SDK & Tools](/changelog/sdk) for PR links.
<Note>
Developer preview. Auto-capture and auto-recall, where memory reaches the context with no explicit tool call, are planned but not yet built.
</Note>
</Update>
<Update label="2026-08-24" description="Strands Agents integration">
**Strands Agents: Mem0 as a Native MemoryStore**
[`strands-mem0`](https://pypi.org/project/strands-mem0/) plugs Mem0 into AWS's [Strands Agents](https://strandsagents.com/) SDK as a native `MemoryStore`, so recall and writes happen inside the agent loop rather than as tool calls the model has to remember to make.
- **Automatic recall:** The `MemoryManager` drives the store on every turn, searching Mem0 and injecting the results into the prompt with no tool call required.
- **Server-side extraction:** Because the store implements `add_messages`, enabling extraction routes raw conversation turns straight to Mem0's extraction pipeline, skipping the extra client-side model call needed to distill facts first.
- **Hosted or self-hosted:** An API key targets the hosted Mem0 Platform; a config dict targets self-hosted Mem0 OSS.
- **Entity scoping:** Accepts `user_id`, `agent_id`, `run_id`, and `app_id`, with at least one required and invalid combinations rejected at construction rather than on the first write.
See [Strands Agents](/integrations/strands) for setup and [SDK & Tools](/changelog/sdk) for PR links.
</Update>
<Update label="2026-08-13" description="Kimi Code plugin">
**Kimi Code: Mem0 Joins the Editor Plugin Family**
The shared Mem0 editor plugin now covers Kimi Code alongside Claude Code, Cursor, Codex, and Antigravity, running on the same scripts, skills, and memory bank, so context written in one editor is available in the others.
- **Hosted MCP server:** Registers `https://mcp.mem0.ai/mcp/`, authenticated with `MEM0_API_KEY` as a bearer token.
- **Automatic capture and recall:** SessionStart loads context through the `context-loader` skill, and hooks on prompt submit, file reads, Bash output, stop, and pre-compact capture and inject memory without an explicit tool call.
- **Guardrails:** Direct `Write` / `Edit` / `MultiEdit` to memory files is blocked, and metadata defaults are enforced on every Mem0 MCP tool call.
- **Kimi hook adapter:** A shim normalizes Kimi Code's hook contract to the shape the shared scripts already expect, resolving the real project directory and translating the differing prompt, tool-output, and MCP tool-name fields.
See [SDK & Tools](/changelog/sdk) for version details and PR links.
</Update>
<Update label="2026-07-30" description="n8n and Zapier integrations">
**Workflow Automation: Mem0 Memory in n8n and Zapier**
+161
View File
@@ -7,6 +7,19 @@ mode: "wide"
<Tabs>
<Tab title="Python">
<Update label="2026-08-24" description="v2.0.19">
**Bug Fixes:**
- **Embeddings:** `HuggingFaceEmbedding` now falls back to the `HUGGINGFACE_API_KEY` env var, then a placeholder key, when `huggingface_base_url` is set and no `api_key` is configured. The OpenAI-compatible client used to talk to TEI endpoints raises at construction when no key resolves at all, so a TEI deployment that doesn't require a real key previously failed to initialize ([#6947](https://github.com/mem0ai/mem0/pull/6947))
- **Core:** `remove_code_blocks()` now accepts list-shaped content (a sequence of `{"text": ...}` blocks, as some agent frameworks pass) by joining each block's text before stripping code fences, instead of raising `AttributeError` from calling `.strip()` on a list ([#6947](https://github.com/mem0ai/mem0/pull/6947))
- **Core:** `create_procedural_memory()` (`Memory` and `AsyncMemory`) now raises a clear `ValueError` when the LLM returns no content for the summary, instead of continuing with empty content that surfaced as a confusing error further down the call ([#6947](https://github.com/mem0ai/mem0/pull/6947))
- **Proxy:** `mem0.proxy` no longer auto-installs `litellm` via a `pip install` subprocess when the import fails; it now raises `ImportError` with instructions to install it yourself. The auto-install could hang or fail silently in restricted environments and ran an unreviewed install on the caller's behalf ([#6947](https://github.com/mem0ai/mem0/pull/6947))
- **Client:** `get_all()` (sync and async) now sends `page` and `page_size` as independent query params instead of requiring both to be set before either was sent. Passing only `page_size` without `page` previously had it silently dropped, so results came back at the server's default page size ([#6900](https://github.com/mem0ai/mem0/pull/6900))
- **LLMs:** Add `provider_override` to `AWSBedrockConfig`, an explicit provider name (for example `"anthropic"`) for when `model` is an application inference profile ARN whose opaque ID has no provider substring for `extract_provider()` to detect. Without it, those ARNs raised `ValueError: Unable to determine provider` ([#6899](https://github.com/mem0ai/mem0/pull/6899))
- **LLMs:** `VllmConfig` now falls back to the `VLLM_BASE_URL` env var when `vllm_base_url` isn't passed explicitly. The default was filled in before the env var was ever checked, so `VLLM_BASE_URL` was silently ignored ([#6897](https://github.com/mem0ai/mem0/pull/6897))
</Update>
<Update label="2026-08-11" description="v2.0.18">
**Bug Fixes:**
@@ -1206,6 +1219,19 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
<Tab title="TypeScript">
<Update label="2026-08-24" description="v3.1.7">
**Bug Fixes:**
- **Vector Stores:** Redis and Valkey `search()` / `get()` / `list()` now preserve `agent_id`, `run_id`, and `user_id` as snake_case in the returned payload. The shared payload formatter camelCased every key including those three identity fields, so entity ids came back as `agentId` / `runId` / `userId`, inconsistent with every other vector store ([#6902](https://github.com/mem0ai/mem0/pull/6902))
- **Memory (OSS):** Embedding-cache lookups now use `Object.prototype.hasOwnProperty.call()` instead of the `in` operator or a falsy `||` check. Memory text matching an inherited `Object.prototype` property name (`constructor`, `toString`, and similar) previously short-circuited the lookup and resolved to that inherited value instead of computing a real embedding, silently corrupting the stored vector ([#6903](https://github.com/mem0ai/mem0/pull/6903))
- **Client:** `getAll()` now sends `page` and `pageSize` as independent query params instead of requiring both to be set before either was sent. Passing only `pageSize` without `page` previously had it silently dropped, so results came back at the server's default page size ([#6900](https://github.com/mem0ai/mem0/pull/6900))
- **LLMs:** Add `providerOverride` to the Bedrock `LLMConfig`, an explicit provider name for when `model` is an application inference profile ARN whose opaque ID has no provider substring for `extractProvider()` to detect. Without it, those ARNs threw before the provider-specific settings could be initialized ([#6899](https://github.com/mem0ai/mem0/pull/6899))
**Security:**
- **Dependencies:** Resolved 17 additional high and critical severity dependency vulnerabilities across 5 pnpm workspaces (`mem0-ts`, `vercel-ai-sdk`, `n8n-nodes-mem0`, `zapier-mem0`, `server/dashboard`) via `pnpm.overrides` and a `tar` patch ([#7032](https://github.com/mem0ai/mem0/pull/7032))
</Update>
<Update label="2026-08-11" description="v3.1.6">
**New Features:**
@@ -1823,6 +1849,17 @@ See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to
<Tab title="CLI">
<Update label="2026-08-24" description="Python v0.2.12 / Node v0.2.13">
**New Features:**
- **`version`:** New `mem0 version` subcommand, alongside the existing `--version` flag, so scripts and agent harnesses can read the CLI version as a regular subcommand instead of a root-level flag (Python and Node [#6907](https://github.com/mem0ai/mem0/pull/6907))
- **`add`:** New `--agent-custom-instructions` flag, threaded through to `agent_custom_instructions` on the `/v3/memories/add/` payload: a second extraction instruction set that applies only to agent-scoped memories, matching the SDKs' `agentCustomInstructions` / `agent_custom_instructions` support (Python and Node [#6910](https://github.com/mem0ai/mem0/pull/6910))
**Documentation:**
- **`search --filter`:** The `--filter` help text and `docs/platform/cli.mdx` now spell out the JSON shape (`{"AND": [...]}` / `{"OR": [...]}`) with a concrete example instead of just calling it "an advanced filter expression," and a matching example command was added to both the CLI help text and the docs page (Python and Node [#6907](https://github.com/mem0ai/mem0/pull/6907))
</Update>
<Update label="2026-08-04" description="Python v0.2.11 / Node v0.2.12">
**New Features:**
@@ -2008,6 +2045,22 @@ A full-featured command-line interface for Mem0, available in both Python and No
<Tabs>
<Tab title="Mem0 Plugin">
<Update label="2026-08-24" description="mem0-plugin v0.2.15">
**Fixes:**
- **Search:** A failed search request now prints `[mem0] search request failed: <error>` to stderr before returning no results. `search_memories()` swallowed every exception and returned `[]`, so an expired API key, a network failure, or a 500 from the backend was indistinguishable from a genuine "nothing stored yet" and the agent carried on with no context and no warning. Shared by Claude Code, Cursor, Codex, Antigravity, and Kimi ([#6898](https://github.com/mem0ai/mem0/pull/6898))
- **Cursor:** `mcpServers` in `.cursor-plugin/plugin.json` now points at `./.cursor-mcp.json` instead of `.cursor-mcp.json`. The un-prefixed path resolved inconsistently depending on Cursor's working directory when it loaded the plugin ([#6948](https://github.com/mem0ai/mem0/pull/6948))
- **Codex:** `install_codex_hooks.py` now prints all six registered events (`PreToolUse, SessionStart, UserPromptSubmit, PostToolUse, Stop, PreCompact`) after installing, instead of a stale four-event list left over from an earlier version of the installer. The README's hook table is corrected to match, documenting the three `PreToolUse` handlers and two `PostToolUse` handlers that were previously undocumented ([#6948](https://github.com/mem0ai/mem0/pull/6948))
**Documentation:**
- New [Claude.ai](/integrations/claude-ai) integration page ([#6948](https://github.com/mem0ai/mem0/pull/6948))
<Note>
The Claude Code, Cursor, and Codex per-editor manifests (`.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.codex-plugin/plugin.json`) had drifted to `0.2.13` while the `.claude-plugin/marketplace.json` and `.cursor-plugin/marketplace.json` listings had already moved to `0.2.14`, so installs were pinned one release behind what the marketplace advertised. This release realigns every manifest and marketplace listing to `0.2.15`.
</Note>
</Update>
<Update label="2026-08-04" description="mem0-plugin v0.2.14">
**Fixes:**
@@ -2360,6 +2413,13 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
<Tab title="Antigravity">
<Update label="2026-08-24" description="Antigravity plugin v0.1.7">
**Fixes:**
- **Hooks:** The `mem0-ensure-deps` and `mem0-session-start` hook commands in `hooks.json` no longer redirect stderr to `/dev/null`. Both commands still end in `|| true` so a failure can't block startup, but a broken dependency install or session bootstrap now shows up in the Antigravity hook log instead of failing invisibly ([#6948](https://github.com/mem0ai/mem0/pull/6948))
</Update>
<Update label="2026-08-04" description="Antigravity plugin v0.1.6">
**Fixes:**
@@ -2427,8 +2487,31 @@ Existing memories written by the previous versions are not rewritten. If your me
</Tab>
<Tab title="Kimi">
<Update label="2026-08-24" description="kimi-plugin v0.1.0">
**Initial release** of the Mem0 plugin for Kimi Code, sharing its scripts, skills, and marketplace listing with the Claude Code / Cursor / Codex / Antigravity plugin family ([#6919](https://github.com/mem0ai/mem0/pull/6919))
**New Features:**
- **MCP server:** Registers the hosted Mem0 MCP server at `https://mcp.mem0.ai/mcp/`, authenticated via the `MEM0_API_KEY` env var as a bearer token.
- **Lifecycle hooks:** Wires SessionStart (loads context through the `context-loader` skill), UserPromptSubmit, three PreToolUse hooks (blocks direct `Write`/`Edit`/`MultiEdit` to memory files, enforces metadata defaults on Mem0 MCP tool calls, and injects context on file reads), two PostToolUse hooks (post-tool tracking and Bash-output scanning), Stop, and PreCompact.
- **Hook adapter:** `kimi_hook_shim.sh` normalizes Kimi Code's hook contract to what the shared hook scripts expect: it resolves the real project directory from the payload's `cwd` (Kimi forces the plugin root as the working directory), converts the array-shaped `prompt` field and the `tool_output` / `tool_input.path` field names to the Claude-style shapes the scripts already handle, and translates the plugin-scoped MCP tool name prefix.
- **Shared policy skill:** Bundles the `/mem0:policy` skill for managing the `## Instructions` and `## Agent Instructions` sections of `mem0.md`.
</Update>
</Tab>
<Tab title="OpenClaw">
<Update label="2026-08-24" description="openclaw-mem0 v1.0.16">
**Security:**
- **Dependencies:** Tightened the `undici` pnpm override from `<6.27.0 → >=6.27.0 <8.0.0` to `<7.29.0 → >=7.29.0 <8.0.0`, closing a newer CVE range the previous floor didn't cover, as part of a wider dependency patch sweep across the pnpm workspaces ([#6847](https://github.com/mem0ai/mem0/pull/6847))
</Update>
<Update label="2026-08-01" description="openclaw-mem0 v1.0.15">
**Improvements:**
@@ -2692,6 +2775,13 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="Pi Agent">
<Update label="2026-08-24" description="Pi Agent plugin v0.1.5">
**Security:**
- **Dependencies:** Tightened the `undici` pnpm override from `<6.27.0 → >=6.27.0 <8.0.0` / `>=8.0.0 <8.5.0 → >=8.5.0` to `<7.29.0 → >=7.29.0 <8.0.0` / `>=8.0.0 <8.9.0 → >=8.9.0 <9.0.0`, closing a newer CVE range the previous floors didn't cover, as part of a wider dependency patch sweep across the pnpm workspaces ([#6847](https://github.com/mem0ai/mem0/pull/6847))
</Update>
<Update label="2026-08-01" description="Pi Agent plugin v0.1.4">
**Security:**
@@ -2749,8 +2839,57 @@ Existing memories written by the previous versions are not rewritten. If your me
</Tab>
<Tab title="Strands">
<Update label="2026-08-24" description="strands-mem0 v0.1.0">
**Initial release** of [`strands-mem0`](https://pypi.org/project/strands-mem0/), a native `MemoryStore` that plugs Mem0 into the [Strands Agents](https://strandsagents.com/) `MemoryManager` ([#7021](https://github.com/mem0ai/mem0/pull/7021))
**New Features:**
- **Automatic recall and injection:** `Mem0MemoryStore.search()` runs every turn through the `MemoryManager`, so relevant memories are searched and prepended to the prompt with no explicit tool call required.
- **Server-side extraction:** `add_messages()` renders raw conversation turns to text and hands them to Mem0's own extraction pipeline (`infer=True`), so enabling extraction skips an extra client-side model call to distill facts first.
- **Verbatim writes:** `add()` stores a single fact exactly as given (`infer=False`), the sink used by the `add_memory` tool or a client-side extractor.
- **Entity scoping:** Accepts `user_id`, `agent_id`, `run_id`, and `app_id`; at least one is required, and mixing the platform-only `app_id` with a self-hosted `config` raises at construction instead of failing on the first write.
- **Hosted or self-hosted:** Defaults to the hosted Mem0 Platform via `api_key` (or `$MEM0_API_KEY`), or pass a `config` dict for a self-hosted Mem0 OSS backend.
- **Non-blocking construction:** The underlying Mem0 client is built lazily on first use inside `asyncio.to_thread`, so API-key validation and OSS embedder/vector-store setup never block the event loop.
<Note>
`Mem0MemoryStore` is the automatic-recall store for the `MemoryManager`. For a model-called tool instead, use the [`mem0_memory`](https://github.com/strands-agents/tools) tool from `strands-agents-tools`; both share the same Mem0 backend and namespace. See [Strands Agents](/integrations/strands) for setup.
</Note>
</Update>
</Tab>
<Tab title="DeepSeek Harness">
<Update label="2026-08-24" description="dsh-mem0 v0.1.0">
**Initial release** of [`dsh-mem0`](https://www.npmjs.com/package/@mem0/dsh-mem0), a native DeepSeek Harness (Cordis) plugin that registers Mem0 as two agent-callable tools ([#7027](https://github.com/mem0ai/mem0/pull/7027))
**New Features:**
- **`search_memory`:** Recalls facts relevant to a query, with an optional `limit` (default 10) and per-call `userId` / `agentId` / `runId` scope overrides.
- **`add_memory`:** Stores a fact for future sessions, tagged `source: "DEEPSEEK_HARNESS"` for backend attribution; extraction runs asynchronously server-side, so a stored fact may take a moment to become searchable.
- **Cordis lifecycle:** `apply(ctx, config)` declares `inject = ['tools']`, so the plugin waits for the harness tool registry to exist, and both tools are registered via `ctx.tools.register()` so they auto-unregister when the plugin unmounts.
- **Config:** `userId` is required; `apiKey` defaults to `$MEM0_API_KEY`; `host` optionally points at a dedicated Mem0 Platform base URL (not a switch to self-hosted Mem0 OSS).
<Note>
Developer preview: auto-capture and auto-recall (memory injected into context automatically, without an explicit tool call) are planned but not yet built. The backend's `KNOWN_EVENT_SOURCES` allowlist also needs `"DEEPSEEK_HARNESS"` added before usage surfaces by name in telemetry rather than bucketing into "OTHERS". See [DeepSeek Harness](/integrations/dsh-mem0) for setup.
</Note>
</Update>
</Tab>
<Tab title="Vercel AI SDK">
<Update label="2026-08-24" description="Vercel AI SDK v3.0.2">
**Security:**
- **Dependencies:** Tightened the `js-yaml` pnpm overrides from `<3.15.0 → >=3.15.0 <4.0.0` / `>=4.0.0 <4.3.0 → >=4.3.0 <5.0.0` to `<3.15.1 → >=3.15.1 <4.0.0` / `>=4.0.0 <4.3.1 → >=4.3.1 <5.0.0`, closing a newer CVE range the previous floors didn't cover, as part of a wider dependency patch sweep across the pnpm workspaces ([#7032](https://github.com/mem0ai/mem0/pull/7032))
</Update>
<Update label="2026-08-01" description="Vercel AI SDK v3.0.1">
**Security:**
@@ -2843,6 +2982,13 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="n8n">
<Update label="2026-08-24" description="n8n-nodes-mem0 v0.1.4">
**Security:**
- **Dependencies:** Add `js-yaml` pnpm overrides (`<3.15.1 → >=3.15.1 <4.0.0`, `>=4.0.0 <4.3.1 → >=4.3.1 <5.0.0`) to close a HIGH/CRITICAL severity advisory, as part of a wider dependency patch sweep across the pnpm workspaces ([#7032](https://github.com/mem0ai/mem0/pull/7032))
</Update>
<Update label="2026-08-05" description="n8n-nodes-mem0 v0.1.3">
**Changes:**
@@ -2887,6 +3033,21 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="Zapier">
<Update label="2026-08-24" description="Zapier app v0.1.2">
**Changes:**
- **Connection label:** The saved connection now shows the account's email (`{{user_email}}`, read from the `/v1/ping/` test response) in the Zap editor instead of a static "Mem0" label, so a user with more than one Mem0 connection can tell them apart ([#6985](https://github.com/mem0ai/mem0/pull/6985))
- **Action and search copy:** Reworded labels and descriptions to address Zapier's publishing review: **Get Memories** is now **Find Memories by User**, **Search Memories** is now **Find Memories**, and every description now reads as a third-person statement of what the step does ([#6985](https://github.com/mem0ai/mem0/pull/6985))
- **Attribution:** Add Memory now tags writes with `source: "ZAPIER"` in the request body, so usage is attributed to this integration server-side ([#6985](https://github.com/mem0ai/mem0/pull/6985))
**Removed:**
- **Client-side telemetry:** Deleted the embedded PostHog telemetry client (`telemetry.ts`) and its call sites in Add Memory, Get Memories, and Search Memories. Usage attribution now happens server-side via the `source: "ZAPIER"` tag above instead of a separate fire-and-forget analytics call from inside the published app ([#6985](https://github.com/mem0ai/mem0/pull/6985))
**Security:**
- **Dependencies:** Tightened the `undici` pnpm override to `<7.29.0 → >=7.29.0 <8.0.0` / `>=8.0.0 <8.9.0 → >=8.9.0 <9.0.0` and added a `brace-expansion` override ([#6847](https://github.com/mem0ai/mem0/pull/6847)), then added a `js-yaml` override (`<3.15.1 → >=3.15.1 <4.0.0`, `>=4.0.0 <4.3.1 → >=4.3.1 <5.0.0`) closing a further HIGH/CRITICAL severity advisory ([#7032](https://github.com/mem0ai/mem0/pull/7032))
</Update>
<Update label="2026-08-04" description="Zapier app v0.1.1">
**Bug Fixes:**
@@ -99,6 +99,7 @@ Here are the parameters available for configuring the Hugging Face embedder:
| `embedding_dims` | Dimensions of the embedding model | `selected_model_dimensions` |
| `model_kwargs` | Additional arguments for the model | `None` |
| `huggingface_base_url` | URL to connect to Text Embeddings Inference (TEI) API | `None` |
| `api_key` | API key for the endpoint; falls back to the `HUGGINGFACE_API_KEY` env var. Only used on the `huggingface_base_url` path | `"hf"` |
</Tab>
<Tab title="TypeScript">
| Parameter | Description | Default Value |
+27
View File
@@ -79,6 +79,33 @@ For detailed guidance on pull requests, refer to [GitHub's documentation](https:
---
## Installing from Source
If you just want to run the latest, unreleased SDK code instead of the published `mem0ai` package, for example to try out a fix before it ships, or to depend on a fork, install directly from a local clone rather than setting up the full contributor environment below.
### Python SDK
```bash
git clone https://github.com/mem0ai/mem0.git
cd mem0
pip install -e .
```
This installs `mem0ai` in editable mode, so edits under `mem0/` take effect immediately without reinstalling. Add an extra if you need one, e.g. `pip install -e ".[vector-stores]"` (see `pyproject.toml` for the full list). If you are contributing to the SDK itself and need every optional dependency for the test suite, use `hatch` instead, see [Dependency Management](#dependency-management).
### TypeScript SDK
```bash
git clone https://github.com/mem0ai/mem0.git
cd mem0/mem0-ts
pnpm install
pnpm run build
```
This builds `mem0-ts/dist` (CJS + ESM). To use it from another local project, add it as a `file:` dependency pointing at `mem0-ts`, or run `pnpm link --global` inside `mem0-ts` and `pnpm link --global mem0ai` in the consuming project.
---
## Python SDK (`mem0/`)
### Dependency Management
@@ -1,519 +0,0 @@
---
title: Control Memory Ingestion
description: "Filter speculation, enforce formats, and gate low-confidence data before it persists."
---
<Info icon="cloud">
**Works with:** Mem0 Platform (`MemoryClient`)
</Info>
AI assistants plugged with memory systems face a problem - they often store everything. Not every conversation needs to be remembered, and not every detail should go to the memory store. Without proper controls, memory systems accumulate unreliable data.
Mem0 lets you control your memory ingestion pipeline. In this cookbook, we'll demonstrate these controls using a medical assistant example - showing how to filter unwanted data, enforce data formats, and implement confidence-based storage.
---
## Overview
Without controls, everything gets stored - speculation, low-confidence data, and information that shouldn't persist. This uncontrolled ingestion leads to cluttered memory and retrieval failures.
Mem0 provides **three tools to control** what gets stored:
1. **Custom instructions** define what to remember and what to ignore.
2. **Confidence thresholds** ensure only verified facts persist.
3. **Memory updates** let you change information without creating duplicates.
In this tutorial, we will:
- Filter speculative statements with custom instructions
- Configure confidence thresholds for fact verification
- Update stored information without duplication
- Build a complete ingestion pipeline
---
## Setup
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="your-api-key")
```
<Note>
Replace `your-api-key` with your actual Mem0 API key from the <a href="https://app.mem0.ai?utm_source=oss&utm_medium=cookbook-memory-ingestion" rel="nofollow">dashboard</a>. Without proper API authentication, memory operations will fail.
</Note>
---
## The Problem
Uncontrolled ingestion stores everything, including speculation:
```python
# Patient mentions speculation
messages = [{"role": "user", "content": "I think I might be allergic to penicillin"}]
client.add(messages, user_id="patient_123")
# Check what got stored
results = client.search("patient allergies", filters={"user_id": "patient_123"})
print(results['results'][0]['memory'])
```
**Output:**
```
Patient is allergic to penicillin
```
<Warning>
Without custom instructions, AI assistants treat speculation as confirmed facts. "I think I might be allergic" becomes "Patient is allergic": a dangerous transformation in sensitive domains like healthcare, legal, or financial services.
</Warning>
The speculation became a confirmed fact. Let's add controls.
---
## Custom Instructions
Custom instructions tell Mem0 what to store and what to ignore.
```python
instructions = """
Only store CONFIRMED medical facts.
Store:
- Confirmed diagnoses from doctors
- Known allergies with documented reactions
- Current medications being taken
Ignore:
- Speculation (words like "might", "maybe", "I think")
- Unverified symptoms
- Casual mentions without confirmation
"""
client.project.update(custom_instructions=instructions)
# Same speculative statement
messages = [{"role": "user", "content": "I think I might be allergic to penicillin"}]
client.add(messages, user_id="patient_123")
# Check what got stored
results = client.get_all(filters={"user_id": "patient_123"})
print(f"Memories stored: {len(results['results'])}")
```
**Output:**
```
Memories stored: 0
```
<Info>
**Expected output:** Zero memories stored. The speculative statement "I think I might be allergic" was filtered out before reaching storage. Custom instructions are actively blocking unreliable data.
</Info>
The speculation was filtered out.
---
## Designing Custom Instructions
When designing instructions, consider the trade-off between precision and recall:
**Too restrictive:** You'll miss important information (false negatives)
```python
# Too strict - filters out useful context
"""
Only store information if explicitly stated by a doctor with full name,
date, time, and medical license number.
"""
```
**Too permissive:** You'll store unreliable data (false positives)
```python
# Too loose - stores speculation as fact
"""
Store any health-related information mentioned.
"""
```
**Balanced approach:**
```python
# Clear categories with examples
"""
Store CONFIRMED facts:
- Diagnoses: "Dr. Smith diagnosed hypertension on March 15th"
- Allergies: "Patient had hives reaction to penicillin"
- Medications: "Taking Lisinopril 10mg daily"
Ignore SPECULATION:
- "I think I might have..."
- "Maybe it's..."
- "Could be related to..."
"""
```
<Tip>
Start with strict instructions (only store confirmed facts), then relax them based on your use case. It's easier to allow more data than to clean up polluted memory. Test with sample conversations before deploying to production.
</Tip>
Start with clear categories and iterate based on retrieval quality.
---
## Confidence Thresholds
Mem0 assigns confidence scores to extracted memories. Use these to filter low-quality data.
### Setting Thresholds
Setting the right confidence threshold depends on your application:
- **High-stakes domains** (medical, legal): Require 0.8+ confidence
- **General assistants**: 0.6+ confidence is often sufficient
- **Exploratory systems**: Lower thresholds (0.4+) capture more data
Test your pipeline with multiple input examples and threshold combinations to find what works for your use case.
```python
# Configure stricter instructions
client.project.update(
custom_instructions="""
Only extract memories with HIGH confidence.
Require specific details (dates, dosages, doctor names) for medical facts.
Skip vague or uncertain statements.
"""
)
# Test with uncertain statement
messages = [{"role": "user", "content": "The doctor mentioned something about my blood pressure"}]
result1 = client.add(messages, user_id="patient_123")
# Test with confirmed fact
messages = [{"role": "user", "content": "Dr. Smith diagnosed me with hypertension on March 15th"}]
result2 = client.add(messages, user_id="patient_123")
print("Vague statement stored:", len(result1['results']) > 0)
print("Confirmed fact stored:", len(result2['results']) > 0)
```
**Output:**
```
Vague statement stored: False
Confirmed fact stored: True
```
<Info icon="check">
**Expected behavior:** Low-confidence extractions are now filtered out automatically. Only verified facts with specific details (names, dates, dosages) persist in memory. The confidence threshold is working.
</Info>
The vague statement was filtered for low confidence. The confirmed fact with specific details was stored.
---
## Filtering Sensitive Information
Custom instructions can prevent storing personal identifiers:
```python
client.project.update(
custom_instructions="""
Medical memory rules:
STORE:
- Confirmed diagnoses
- Verified allergies
- Current medications
NEVER STORE:
- Social Security Numbers
- Insurance policy numbers
- Credit card information
- Full addresses
- Phone numbers
Replace identifiers with generic references if mentioned.
"""
)
# Test with PII
messages = [
{"role": "user", "content": "My SSN is 123-45-6789 and I'm allergic to penicillin"}
]
client.add(messages, user_id="patient_123")
# Check what was stored
results = client.get_all(filters={"user_id": "patient_123"})
for result in results['results']:
print(result['memory'])
```
**Output:**
```
Patient is allergic to penicillin
```
The SSN was filtered out, but the allergy was stored.
---
## Updating Memories
When information changes, update existing memories instead of creating duplicates.
```python
# Initial allergy stored
result = client.add(
[{"role": "user", "content": "Patient confirmed allergy to penicillin with documented hives reaction"}],
user_id="patient_123"
)
memory_id = result['results'][0]['id']
print(f"Stored memory: {memory_id}")
# Later, patient gets retested - allergy was false positive
client.update(
memory_id=memory_id,
text="Patient tested negative for penicillin allergy on April 2nd, 2025. Previous allergy was false positive.",
metadata={"verified": True, "updated_date": "2025-04-02"}
)
# Retrieve the updated memory
updated = client.get(memory_id)
print(f"\\nUpdated memory: {updated['memory']}")
print(f"Metadata: {updated['metadata']}")
```
**Output:**
```
Stored memory: mem_abc123
Updated memory: Patient tested negative for penicillin allergy on April 2nd, 2025. Previous allergy was false positive.
Metadata: {'verified': True, 'updated_date': '2025-04-02'}
```
### Benefits of Updating
**Preserves history:**
- `created_at` shows when the memory was first stored
- `updated_at` shows when it was modified
- Audit trail for compliance
**Avoids conflicts:**
- No duplicate or contradicting memories
- Single source of truth for each fact
<Warning>
That “no duplicates” promise comes from the inference pipeline. Keep `infer=True` when you rely on automatic updates. Raw imports (`infer=False`) skip conflict checks, so mixing the two modes for the same fact will create duplicates.
</Warning>
### Pick the right inference mode
| Mode | What it does | Best for | Watch out for |
| --- | --- | --- | --- |
| `infer=True` *(default)* | Runs the LLM pipeline so Mem0 extracts structured facts and resolves conflicts automatically. | Daily conversations, preference tracking, anything you want deduped. | Slightly slower because inference runs on every write. |
| `infer=False` | Stores your payload exactly as-is: no inference, no dedupe. | Bulk imports, compliance snapshots, curated facts you already trust. | Later `infer=True` calls for the same fact will create duplicates you must clean manually. |
<Tip>
Stay consistent per data source. If you need both behaviors, keep them in separate scopes (e.g., different `app_id` or `run_id`) so you always know which memories are inferred vs direct imports.
</Tip>
---
## Update vs Delete
When should you update vs delete?
### Update when:
- Information changes but remains relevant
- You need audit history
- The memory has relationships to other data
```python
# Medication dosage changed
client.update(
memory_id=med_id,
text="Taking Lisinopril 20mg daily (increased from 10mg on March 1st)"
)
```
### Delete when:
- Information was completely wrong
- Memory is no longer relevant
- Duplicate entry
```python
# Duplicate entry
client.delete(memory_id)
```
---
## Putting It Together
Here's a complete ingestion pipeline with all controls:
```python
from mem0 import MemoryClient
import os
# Initialize client
client = MemoryClient(api_key=os.getenv("MEM0_API_KEY"))
# Configure custom instructions
client.project.update(
custom_instructions="""
Medical memory assistant rules:
STORE:
- Confirmed diagnoses (with doctor name and date)
- Verified allergies (with reaction details)
- Current medications (with dosage)
IGNORE:
- Speculation (might, maybe, possibly)
- Unverified symptoms
- Personal identifiers (SSN, insurance numbers)
CONFIDENCE:
Require high confidence. Reject vague or uncertain statements.
Require specific details: names, dates, dosages.
"""
)
# Helper function for safe ingestion
def add_medical_memory(content, user_id, metadata=None):
"""Add memory with automatic filtering."""
result = client.add(
[{"role": "user", "content": content}],
user_id=user_id,
metadata=metadata or {}
)
if result['results']:
print(f"✓ Stored: {result['results'][0]['memory']}")
else:
print(f"✗ Filtered: {content}")
return result
# Test cases
print("Testing ingestion pipeline:\\n")
test_cases = [
"I think I might be allergic to penicillin",
"Dr. Johnson confirmed penicillin allergy on Jan 15th with hives reaction",
"Patient SSN is 123-45-6789",
"Currently taking Lisinopril 10mg daily for hypertension",
"Feeling tired lately",
"Dr. Martinez diagnosed Type 2 diabetes on February 3rd, 2025"
]
for content in test_cases:
add_medical_memory(content, user_id="patient_123")
print()
```
**Output:**
```
Testing ingestion pipeline:
✗ Filtered: I think I might be allergic to penicillin
✓ Stored: Patient has confirmed penicillin allergy diagnosed by Dr. Johnson on January 15th with hives reaction
✗ Filtered: Patient SSN is 123-45-6789
✓ Stored: Patient is currently taking Lisinopril 10mg daily for hypertension
✗ Filtered: Feeling tired lately
✓ Stored: Patient diagnosed with Type 2 diabetes by Dr. Martinez on February 3rd, 2025
```
---
## Per-Call Instructions
You can override project-level instructions for specific conversations:
First define custom instructions
```python
custom_instructions="""Emergency intake mode:Store ALL symptoms and observations immediately.
Flag for later review and verification."""
```
```python
# Emergency intake - store everything temporarily
emergency_messages = [
{"role": "user", "content": "Patient arrived with chest pain and shortness of breath"}
]
client.add(
emergency_messages,
user_id="patient_456",
custom_instructions=custom_instructions,
metadata={"type": "emergency", "review_required": True}
)
```
This is useful for:
- Different conversation types (emergency vs routine)
- Channel-specific rules (phone vs in-person)
- Temporary data collection that needs review
---
## What You Built
You now have a medical assistant with production-grade memory controls:
- **Custom instructions** - Filter speculation and enforce confirmed facts only
- **Confidence thresholds** - Gate extractions below 0.7 confidence score
- **Memory updates** - Modify stored information without creating duplicates
- **Per-call instructions** - Apply temporary rules for specific conversations
- **PII filtering** - Block sensitive data (SSNs, insurance numbers) automatically
These controls prevent retrieval failures and ensure your AI assistant works with reliable, verified information.
---
## Summary
Start with conservative filters (only store confirmed facts) and iterate based on your application's needs. Combine custom instructions with confidence thresholds for the most reliable memory ingestion pipeline.
<Card title="Build a Mem0 Companion" icon="users" href="/cookbooks/essentials/building-ai-companion">
Learn core memory patterns including temporary vs permanent data handling.
</Card>
<Snippet file="star-on-github.mdx" />
@@ -332,10 +332,10 @@ You learned how to:
href="/platform/features/v2-memory-filters"
/>
<Card
title="Control Memory Ingestion"
description="Pair scoped storage with rules that block low-quality facts."
title="Custom Instructions"
description="Pair scoped storage with instructions that steer what Mem0 extracts and stores."
icon="shield-check"
href="/cookbooks/essentials/controlling-memory-ingestion"
href="/platform/features/custom-instructions"
/>
</CardGroup>
@@ -287,8 +287,8 @@ Use **`get_all()`** for bulk retrieval, **`search()`** for specific questions, a
<Card title="Build a Mem0 Companion" icon="users" href="/cookbooks/essentials/building-ai-companion">
Learn core memory patterns including temporary vs permanent data handling.
</Card>
<Card title="Control Memory Ingestion" icon="filter" href="/cookbooks/essentials/controlling-memory-ingestion">
Ensure only verified insights make it into your export pipeline.
<Card title="Custom Instructions" icon="filter" href="/platform/features/custom-instructions">
Steer what Mem0 extracts so only verified insights make it into your export pipeline.
</Card>
</CardGroup>
@@ -249,8 +249,8 @@ Categories make retrieval faster and compliance easier. Define 3-5 clear categor
Instead of searching through everything, agents jump directly to the information type they need: billing issues, account details, or support tickets.
<CardGroup cols={2}>
<Card title="Control Memory Ingestion" icon="filter" href="/cookbooks/essentials/controlling-memory-ingestion">
Keep categories meaningful by filtering noise before it lands in storage.
<Card title="Custom Instructions" icon="filter" href="/platform/features/custom-instructions">
Keep categories meaningful by steering what Mem0 extracts before it lands in storage.
</Card>
<Card title="Export Tagged Memories" icon="download" href="/cookbooks/essentials/exporting-memories">
Use categories to drive audits, migrations, and compliance reports.
@@ -293,8 +293,8 @@ run().catch(console.error);
<Card title="Agents SDK Tool with Mem0" icon="robot" href="/cookbooks/integrations/agents-sdk-tool">
Extend the OpenAI Agents SDK with Mem0 integration capabilities.
</Card>
<Card title="Control Memory Ingestion" icon="filter" href="/cookbooks/essentials/controlling-memory-ingestion">
Fine-tune what memories get stored during tool calls.
<Card title="Custom Instructions" icon="filter" href="/platform/features/custom-instructions">
Steer what memories get stored during tool calls.
</Card>
</CardGroup>
@@ -360,8 +360,8 @@ Mem0 enables a seamless, intelligent content-writing workflow, perfect for conte
---
<CardGroup cols={2}>
<Card title="Control Memory Ingestion" icon="filter" href="/cookbooks/essentials/controlling-memory-ingestion">
Filter and curate content examples to maintain consistent writing style.
<Card title="Custom Instructions" icon="filter" href="/platform/features/custom-instructions">
Steer what Mem0 extracts and stores to maintain a consistent writing style.
</Card>
<Card title="Email Automation with Mem0" icon="envelope" href="/cookbooks/operations/email-automation">
Automate email drafting with memory-powered context and tone matching.
+12 -6
View File
@@ -35,7 +35,7 @@ Every cookbook opens with a **Works with** badge naming the SDK surface it uses.
| [Memory-Powered Support Agent](/cookbooks/operations/support-inbox) | Operations | OSS only |
</Tab>
<Tab title="Hosted Platform">
Twenty-one cookbooks run on the hosted Platform, using `MemoryClient` or the Mem0 MCP server with a `MEM0_API_KEY`.
Twenty cookbooks run on the hosted Platform, using `MemoryClient` or the Mem0 MCP server with a `MEM0_API_KEY`.
| Cookbook | Category | Works with |
| --- | --- | --- |
@@ -43,7 +43,6 @@ Every cookbook opens with a **Works with** badge naming the SDK surface it uses.
| [Research Assistant for YouTube](/cookbooks/companions/youtube-research) | Companions | Platform only |
| [Voice-First AI Companion](/cookbooks/companions/voice-companion-openai) | Companions | Platform only |
| [Build a Companion with Mem0](/cookbooks/essentials/building-ai-companion) | Essentials | OSS and Platform |
| [Control Memory Ingestion](/cookbooks/essentials/controlling-memory-ingestion) | Essentials | Platform only |
| [Export Stored Memories](/cookbooks/essentials/exporting-memories) | Essentials | Platform only |
| [Partition Memories by Entity](/cookbooks/essentials/entity-partitioning-playbook) | Essentials | Platform only |
| [Tag and Organize Memories](/cookbooks/essentials/tagging-and-organizing-memories) | Essentials | Platform only |
@@ -97,11 +96,18 @@ The most popular cookbooks to get going fast:
Balance personalization with consistent behavior across users, agents, and apps.
</Card>
<Card
title="Control Memory Ingestion"
icon="filter"
href="/cookbooks/essentials/controlling-memory-ingestion"
title="Tag and Organize Memories"
icon="tags"
href="/cookbooks/essentials/tagging-and-organizing-memories"
>
Filter speculation and low-confidence data.
Use categories to keep retrieval fast and audits simple.
</Card>
<Card
title="Export Memories"
icon="download"
href="/cookbooks/essentials/exporting-memories"
>
Back up, migrate, and audit stored memory data.
</Card>
</CardGroup>
+66 -69
View File
@@ -1,68 +1,69 @@
---
title: Memory Types
description: "See how Mem0 layers conversation, session, and user memories to keep agents contextual."
description: "What memory_type actually does in Mem0: procedural memory is implemented, semantic and episodic are not."
icon: "tag"
iconType: "solid"
---
# How Mem0 Organizes Memory
# Memory Types
Mem0 separates memory into layers so agents remember the right detail at the right time. Think of it like a notebook: a sticky note for the current task, a daily journal for the session, and an archive for everything a user has shared.
Mem0's Python SDK exposes a `memory_type` parameter on `add()`. The underlying `MemoryType` enum defines three values, but only one of them is wired up. This page states plainly which is which so you don't build against a type that doesn't exist yet.
## Key terms
## Status
- **Conversation memory**: In-flight messages inside a single turn (what was just said).
- **Session memory**: Short-lived facts that apply for the current task or channel.
- **User memory**: Long-lived knowledge tied to a person, account, or workspace.
- **Organizational memory**: Shared context available to multiple agents or teams.
| Type | Enum value | Status | Notes |
| --- | --- | --- | --- |
| Procedural memory | `procedural_memory` | **Implemented** | Python OSS only (`Memory`/`AsyncMemory`). Pass `memory_type="procedural_memory"` and `agent_id` to `add()`. Not available on the Platform `MemoryClient`, and not available in the TypeScript SDK (OSS or Platform). |
| Semantic memory | `semantic_memory` | **Not implemented** | Defined in the `MemoryType` enum but never read anywhere else in the codebase. Passing it to `add()` raises a validation error. There is no evidence in this repo of a roadmap date for this. |
| Episodic memory | `episodic_memory` | **Not implemented** | Same as above: defined, never wired into the extraction pipeline, rejected by validation, no documented roadmap. |
```mermaid
graph LR
A[Conversation turn] --> B[Session memory]
B --> C[User memory]
C --> D[Org memory]
C --> E[Mem0 retrieval layer]
```
<Warning>
Only `procedural_memory` is a real, working value. Calling `memory.add(messages, memory_type="semantic_memory")` (or `episodic_memory`) is rejected and tells you to pass `procedural_memory` instead. Sync `Memory.add()` raises `Mem0ValidationError`; `AsyncMemory.add()` raises a plain `ValueError`.
</Warning>
## Short-term vs long-term memory
## Procedural memory
Short-term memory keeps the current conversation coherent. It includes:
- **Conversation history**: recent turns in order so the agent remembers what was just said.
- **Working memory**: temporary state such as tool outputs or intermediate calculations.
- **Attention context**: the immediate focus of the assistant, similar to what a person holds in mind mid-sentence.
Long-term memory preserves knowledge across sessions. It captures:
- **Factual memory**: user preferences, account details, and domain facts.
- **Episodic memory**: summaries of past interactions or completed tasks.
- **Semantic memory**: relationships between concepts so agents can reason about them later.
Mem0 maps these classic categories onto its layered storage so you can decide what should fade quickly versus what should last for months.
## How does it work?
Mem0 stores each layer separately and merges them when you query:
1. **Capture**: Messages enter the conversation layer while the turn is active.
2. **Promote**: Relevant details persist to session or user memory based on your `user_id`, `run_id`, and metadata.
3. **Retrieve**: The search pipeline pulls from all layers, ranking user memories first, then session notes, then raw history.
Procedural memory stores step-by-step task knowledge (how an agent performs a workflow) rather than facts about a user. It requires `agent_id`:
```python
import os
from mem0 import Memory
memory = Memory()
# Sticky note: conversation memory
memory.add(
["I'm Alex and I prefer boutique hotels."],
[
{"role": "user", "content": "Book a flight from SFO to NYC"},
{"role": "assistant", "content": "1. Search flights. 2. Filter by price. 3. Confirm booking."},
],
agent_id="travel-agent",
memory_type="procedural_memory",
)
```
Omit `memory_type` entirely and Mem0 stores the messages as an ordinary memory: there is no semantic/episodic pathway for it to fall into. Any other explicit value is rejected by validation rather than quietly falling back to an ordinary memory.
## How every other memory is scoped
Outside of the `procedural_memory` special case, Mem0 does not sort memories into named types. Every memory is scoped by the identifiers you pass in, and the same identifiers are used to retrieve it later:
- **`user_id`**: ties a memory to a specific person or account.
- **`agent_id`**: ties a memory to a specific agent or assistant persona.
- **`run_id`**: ties a memory to a specific session, task, or conversation thread.
- **`app_id`** (Platform only): ties a memory to a specific application or tenant, in addition to the three above. See <Link href="/platform/features/entity-scoped-memory">Entity-Scoped Memory</Link>.
At least one identifier is required on `add()`. Passing more than one narrows the scope further (for example, `user_id` + `run_id` together).
```python
from mem0 import Memory
memory = Memory()
memory.add(
"I'm Alex and I prefer boutique hotels.",
user_id="alex",
run_id="trip-planning-2025",
)
# Later in the session, pull long-term + session context
results = memory.search(
"Any hotel preferences?",
filters={"user_id": "alex", "run_id": "trip-planning-2025"},
@@ -70,52 +71,48 @@ results = memory.search(
```
<Tip>
Use `run_id` when you want short-term context to expire automatically; rely on `user_id` for lasting personalization.
Use `run_id` when you want a set of memories to stay tied to one session or task; use `user_id` alone for anything that should persist across every session for that person.
</Tip>
## When should you use each layer?
## How memories are extracted and updated
- **Conversation memory**: Tool calls or chain-of-thought that only matter within the current turn.
- **Session memory**: Multi-step tasks (onboarding flows, debugging sessions) that should reset once complete.
- **User memory**: Personal preferences, account state, or compliance details that must persist across interactions.
- **Organizational memory**: Shared FAQs, product catalogs, or policies that every agent should recall.
When `infer=True` (the default) on `add()`, Mem0 runs a single pipeline rather than routing through separate type-specific paths:
## How it compares
1. **Context gathering**: pulls the most recent messages already stored for the same `user_id`/`agent_id`/`run_id` scope.
2. **Existing memory retrieval**: embeds the new messages and runs a vector search against memories already in that same scope, to find candidates that might need to change.
3. **Extraction**: a single LLM call compares the new messages against the retrieved candidates and decides, per fact, whether to `ADD`, `UPDATE`, `DELETE`, or leave a memory alone.
| Layer | Lifetime | Short or long term | Best for | Trade-offs |
| --- | --- | --- | --- | --- |
| Conversation | Single response | Short-term | Tool execution detail | Lost after the turn finishes |
| Session | Minutes to hours | Short-term | Multi-step flows | Clear it manually when done |
| User | Weeks to forever | Long-term | Personalization | Requires consent/governance |
| Org | Configured globally | Long-term | Shared knowledge | Needs owner to keep current |
Alongside this, both OSS and Platform extract named entities (people, places, organizations) from memory text and use shared entities between memories to boost related results at search time. On Platform, that entity graph is also queryable directly; see <Link href="/platform/features/graph-memory">Graph Memory</Link>. In OSS, entities only affect ranking, there is no separate graph to query.
<Warning>
Avoid storing secrets or unredacted PII in user or org memories: Mem0 is retrievable by design. Encrypt or hash sensitive values first.
Avoid storing secrets or unredacted PII in memories: they are retrievable by design. Encrypt or hash sensitive values before calling `add()`.
</Warning>
## Put it into practice
- Use the <Link href="/core-concepts/memory-operations/add">Add Memory</Link> guide to persist user preferences.
- Follow <Link href="/platform/advanced-memory-operations">Advanced Memory Operations</Link> to tune metadata and retrieval.
## See it live
- <Link href="/cookbooks/companions/ai-tutor">AI Tutor with Mem0</Link> shows session vs user memories in action.
- <Link href="/cookbooks/operations/support-inbox">Support Inbox with Mem0</Link> demonstrates shared org memory.
{/* DEBUG: verify CTA targets */}
<CardGroup cols={2}>
<Card
title="Explore Memory Operations"
description="Dive into the add/search/update/delete concepts next."
description="Dive into the add/search/update/delete operations next."
icon="circle-check"
href="/core-concepts/memory-operations/add"
/>
<Card
title="See a Cookbook"
description="Apply layered memories inside a customer support agent."
title="Advanced Memory Operations"
description="Tune metadata, filters, and retrieval on Platform."
icon="sliders"
href="/platform/advanced-memory-operations"
/>
<Card
title="AI Tutor Cookbook"
description="See user_id-scoped memory used in a real tutoring agent."
icon="rocket"
href="/cookbooks/companions/ai-tutor"
/>
<Card
title="Support Inbox Cookbook"
description="See user_id-scoped memory used in a support workflow."
icon="inbox"
href="/cookbooks/operations/support-inbox"
/>
</CardGroup>
+74 -24
View File
@@ -325,7 +325,8 @@
"integrations/google-ai-adk",
"integrations/mastra",
"integrations/vercel-ai-sdk",
"integrations/chatdev"
"integrations/chatdev",
"integrations/strands"
]
},
{
@@ -368,6 +369,7 @@
"icon": "terminal",
"pages": [
"integrations/claude-code",
"integrations/claude-ai",
"integrations/cursor",
"integrations/codex",
"integrations/opencode",
@@ -380,7 +382,8 @@
"pages": [
"integrations/openclaw",
"integrations/hermes",
"integrations/pi-agent"
"integrations/pi-agent",
"integrations/dsh-mem0"
]
}
]
@@ -401,7 +404,6 @@
"pages": [
"cookbooks/essentials/building-ai-companion",
"cookbooks/essentials/entity-partitioning-playbook",
"cookbooks/essentials/controlling-memory-ingestion",
"cookbooks/essentials/tagging-and-organizing-memories",
"cookbooks/essentials/exporting-memories"
]
@@ -952,24 +954,20 @@
"destination": "/platform/features/memory-export"
},
{
"source": "/v0x/components/:a/:b/:c",
"destination": "/components/:a/:b/:c"
"source": "/v0x/components/:slug*",
"destination": "/components/:slug*"
},
{
"source": "/v0x/components/:a/:b",
"destination": "/components/:a/:b"
"source": "/v0x/core-concepts/:slug*",
"destination": "/core-concepts/:slug*"
},
{
"source": "/v0x/core-concepts/:a/:b",
"destination": "/core-concepts/:a/:b"
"source": "/v0x/integrations/:slug*",
"destination": "/integrations/:slug*"
},
{
"source": "/v0x/integrations/:slug",
"destination": "/integrations/:slug"
},
{
"source": "/v0x/open-source/:slug",
"destination": "/open-source/:slug"
"source": "/v0x/open-source/:slug*",
"destination": "/open-source/:slug*"
},
{
"source": "/v0x/introduction",
@@ -1044,8 +1042,8 @@
"destination": "/platform/features/graph-memory"
},
{
"source": "/features/:slug",
"destination": "/platform/features/:slug"
"source": "/features/:slug*",
"destination": "/platform/features/:slug*"
},
{
"source": "/platform/features/online-memory",
@@ -1172,7 +1170,7 @@
"destination": "/open-source/overview"
},
{
"source": "/self-hosting/:slug",
"source": "/self-hosting/:slug*",
"destination": "/open-source/overview"
},
{
@@ -1180,7 +1178,7 @@
"destination": "/open-source/overview"
},
{
"source": "/self-hosted/:slug",
"source": "/self-hosted/:slug*",
"destination": "/open-source/overview"
},
{
@@ -1188,7 +1186,7 @@
"destination": "/platform/quickstart"
},
{
"source": "/getting-started/:slug",
"source": "/getting-started/:slug*",
"destination": "/platform/quickstart"
},
{
@@ -1196,7 +1194,7 @@
"destination": "/open-source/setup"
},
{
"source": "/deployment/:slug",
"source": "/deployment/:slug*",
"destination": "/open-source/setup"
},
{
@@ -1208,12 +1206,12 @@
"destination": "/open-source/overview"
},
{
"source": "/oss/:slug",
"source": "/oss/:slug*",
"destination": "/open-source/overview"
},
{
"source": "/concepts/:slug",
"destination": "/core-concepts/:slug"
"source": "/concepts/:slug*",
"destination": "/core-concepts/:slug*"
},
{
"source": "/pricing",
@@ -1250,6 +1248,58 @@
{
"source": "/integrations/keywords",
"destination": "/integrations/respan"
},
{
"source": "/cookbooks/essentials/controlling-memory-ingestion",
"destination": "/platform/features/custom-instructions"
},
{
"source": "/open-source/quickstart",
"destination": "/open-source/overview"
},
{
"source": "/open-source/graph-memory/overview",
"destination": "/open-source/overview"
},
{
"source": "/what-is-mem0",
"destination": "https://mem0.ai/"
},
{
"source": "/platform-vs-oss",
"destination": "/platform/platform-vs-oss"
},
{
"source": "/components/overview",
"destination": "/components/llms/overview"
},
{
"source": "/v0x/core-concepts/memory-operations/add",
"destination": "/core-concepts/memory-operations/add"
},
{
"source": "/v0x/integrations/llama-index",
"destination": "/integrations/llama-index"
},
{
"source": "/api-reference/event/get-event",
"destination": "/api-reference/events/get-event"
},
{
"source": "/api-reference/webhook/get-webhooks",
"destination": "/api-reference/webhook/get-webhook"
},
{
"source": "/api-reference/organization/update-organization-members",
"destination": "/api-reference/organization/update-org-member"
},
{
"source": "/api-reference/organization/delete-project",
"destination": "/api-reference/project/delete-project"
},
{
"source": "/api-reference/entities/get-entities",
"destination": "/api-reference/entities/get-users"
}
]
}
+85
View File
@@ -0,0 +1,85 @@
---
title: Claude.ai
description: "Add persistent memory to Claude.ai with the Mem0 remote MCP server: custom connector setup and native-memory troubleshooting."
---
Add persistent memory to [**Claude.ai**](https://claude.ai) (the hosted web app, not Claude Code) using Mem0's remote MCP server. Claude.ai connects to MCP servers over the internet as **custom connectors**; there is no local plugin or hook system here, since chats run in Anthropic's cloud, not on your machine.
## Prerequisites
1. A Mem0 Platform account: <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-claude-ai" rel="nofollow">sign up at app.mem0.ai</a>
2. A Claude.ai account on any plan (free, Pro, Max, Team, or Enterprise)
You don't need an API key up front: the connector uses browser-based sign-in the first time Claude calls a Mem0 tool (see [Signing in](#signing-in)).
## Installation
### Individual accounts (Pro, Max, or free)
1. Go to **Customize > Connectors**
2. Click **+**, then **Add custom connector**
3. Enter the server URL: `https://mcp.mem0.ai/mcp/`
4. Click **Add**
<Note>
Free-tier accounts are limited to one custom connector. Pro, Max, Team, and Enterprise accounts can add multiple.
</Note>
### Team and Enterprise organizations
An organization owner registers the connector once for everyone:
1. Go to **Organization Settings > Connectors**
2. Click **Add**, hover **Custom**, then select **Web**
3. Enter the server URL: `https://mcp.mem0.ai/mcp/`
4. Click **Add**
Members then connect individually: **Customize > Connectors**, find **mem0**, and click **Connect**.
### Enabling in a chat
Connectors are opt-in per conversation. Click the **+** button next to the chat prompt, open **Connectors**, and toggle **mem0** on before you start.
## Signing in
The first time Claude calls a Mem0 tool, your browser opens a sign-in prompt to authorize the connector against your Mem0 account. Approve it once; Claude.ai stores and refreshes the resulting token for you. There's no API key to paste into the connector UI itself.
## What's Included
| Component | Included |
|-----------|:--------:|
| MCP Server (9 memory tools) | Yes |
| Lifecycle Hooks | No (Claude.ai has no local hook system) |
| Mem0 SDK Skill | No (Claude.ai has no local skills directory) |
## Available MCP Tools
| Tool | Description |
|------|-------------|
| `add_memory` | Save text or conversation history for a user/agent |
| `search_memories` | Semantic search across memories with filters |
| `get_memories` | List memories with filters and pagination |
| `get_memory` | Retrieve a specific memory by ID |
| `update_memory` | Overwrite a memory's text by ID |
| `delete_memory` | Delete a single memory by ID |
| `delete_all_memories` | Bulk delete all memories in scope |
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
## Troubleshooting
- **Claude never calls the mem0 tools, even though the connector shows as connected**: Claude.ai ships its own native memory (Settings > Capabilities > Memory, on by default), which synthesizes a running summary from your chat history automatically. When both are active, Claude's system prompt tends to favor its built-in memory and rarely reaches for a third-party memory tool on its own. Ask explicitly ("search my mem0 memories for...", "save this to mem0") to force the tool call, or pause Claude's native memory (Settings > Capabilities > Memory > Pause) if you want Mem0 to be the primary memory store for that account.
- **"Connection failed" or the connector won't add**: Confirm the URL is exactly `https://mcp.mem0.ai/mcp/`. Custom connectors reach your MCP server from Anthropic's cloud, not your device, so a localhost URL will never work here.
- **No tools appearing after adding the connector**: Make sure you toggled **mem0** on for the current conversation under the **+ > Connectors** menu; adding a connector doesn't enable it in every chat automatically.
- **Can't add a second connector**: Free-tier accounts are capped at one custom connector; upgrade to Pro/Max/Team/Enterprise for more.
<CardGroup cols={2}>
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
Detailed MCP configuration for all clients
</Card>
<Card title="Claude Code Integration" icon="/images/provider-icons/anthropic.svg" href="/integrations/claude-code">
Add Mem0 memory to Claude Code workflows
</Card>
</CardGroup>
<Snippet file="star-on-github.mdx" />
+29 -3
View File
@@ -91,12 +91,23 @@ To update, run `codex plugin marketplace upgrade` to pull the latest from the Me
After either option, start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
</Info>
## Codex Cloud
[Codex Cloud](https://developers.openai.com/codex/cloud/environments) tasks run setup scripts and the agent in separate phases with different variable scoping:
- **Environment Variables** persist for the full duration of the task, through both the setup script and the agent phase.
- **Secrets** are only available to the setup script; they are wiped before the agent phase starts, so the agent itself cannot read them.
Because the `mem0` MCP server authenticates on every tool call the agent makes (not just during setup), set `MEM0_API_KEY` as an **Environment Variable** in your Codex Cloud environment configuration, not as a Secret. A Secret will let a setup script authenticate but the agent will lose access to `MEM0_API_KEY` once the task phase begins, breaking Mem0 MCP calls.
Lifecycle hooks that shell out to local scripts (Option A) are not applicable in Codex Cloud's ephemeral containers; use Option B (Direct MCP) with `MEM0_API_KEY` set as above.
## What's Included
| Component | Plugin Install | MCP Only |
|-----------|:--------------:|:--------:|
| MCP Server (9 memory tools) | Yes | Yes |
| Lifecycle Hooks | Yes | No |
| Lifecycle Hooks | Opt-in (see below) | No |
| Mem0 SDK Skill | Yes | No |
## Available MCP Tools
@@ -117,7 +128,22 @@ Once installed, the following tools are available in every Codex session:
## Lifecycle Hooks
When installed via the plugin marketplace, Mem0 hooks into Codex's lifecycle to automatically manage memory:
Unlike Claude Code, Codex has no plugin-host mechanism for auto-wiring hooks from an installed plugin: it only reads hooks from `~/.codex/hooks.json` (or `<repo>/.codex/hooks.json`). Installing the plugin (Option A) does **not** turn hooks on by itself. To enable them, run the bundled installer once against your local clone:
```bash
python3 <path-to-your-clone>/integrations/mem0-plugin/scripts/install_codex_hooks.py
```
This merges Mem0's entries into `~/.codex/hooks.json` and is idempotent (safe to re-run after upgrading). It also requires the `codex_hooks` feature flag in `~/.codex/config.toml`:
```toml
[features]
codex_hooks = true
```
The installer prints a reminder if the flag isn't set. Restart Codex after installing hooks or editing the config. To remove: `python3 .../install_codex_hooks.py --uninstall`.
Once enabled, Mem0 hooks into Codex's lifecycle to automatically manage memory:
| Hook | Event | What it does |
|------|-------|-------------|
@@ -155,7 +181,7 @@ You: Add WebSocket support for real-time notification delivery.
- **"Connection failed"**: Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`
- **No tools appearing**: Restart your Codex session after installation
- **Duplicate `mem0` MCP / "tool collision" errors**: You combined Option A with Option B. Remove the `[mcp_servers.mem0]` block from `~/.codex/config.toml`; the plugin registers it automatically
- **Hooks not firing**: Ensure the plugin is installed via the marketplace (Option A). MCP-only installs do not include hooks
- **Hooks not firing**: Hooks are opt-in and are not installed by the marketplace install itself. Run `scripts/install_codex_hooks.py` (see [Lifecycle Hooks](#lifecycle-hooks)), confirm `codex_hooks = true` is set under `[features]` in `~/.codex/config.toml`, and restart Codex. MCP-only installs (Option B) never include hooks
<CardGroup cols={2}>
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
+38 -23
View File
@@ -39,6 +39,10 @@ os.environ["SERPER_API_KEY"] = "your-serper-api-key"
client = MemoryClient()
```
<Note>
Newer versions of CrewAI removed the `memory_config={"provider": "mem0"}` shortcut on `Crew(...)` that older guides referenced. CrewAI still offers a native Mem0 path through its `ExternalMemory` API, so that option remains open; check [CrewAI's memory documentation](https://docs.crewai.com/en/concepts/memory) for the shape your version expects. This guide wires Mem0 in explicitly through `MemoryClient` instead, which keeps retrieval under your control and stays valid as CrewAI's memory API changes.
</Note>
## Store User Preferences
Set up initial conversation and preferences storage:
@@ -69,9 +73,21 @@ messages = [
store_user_preferences("crew_user_1", messages)
```
## Retrieve Relevant Memories
Look up what Mem0 already knows about the user before planning a trip, so the crew's output reflects their actual preferences:
```python
def get_user_context(user_id: str, query: str) -> str:
"""Fetch relevant memories and format them for a task description"""
relevant_memories = client.search(query, filters={"user_id": user_id})
memories = [m["memory"] for m in relevant_memories.get("results", [])]
return "\n".join(f"- {memory}" for memory in memories)
```
## Create CrewAI Agent
Define an agent with memory capabilities:
Define an agent with search capabilities:
```python
def create_travel_agent():
@@ -83,61 +99,60 @@ def create_travel_agent():
goal="Plan personalized travel itineraries",
backstory="""You are a seasoned travel planner, known for your meticulous attention to detail.""",
allow_delegation=False,
memory=True,
tools=[search_tool],
)
```
## Define Tasks
Create tasks for your agent:
Create a task that folds the retrieved memories into its description, so the agent plans around the user's known preferences:
```python
def create_planning_task(agent, destination: str):
"""Create a travel planning task"""
def create_planning_task(agent, destination: str, user_context: str):
"""Create a travel planning task personalized with the user's stored preferences"""
return Task(
description=f"""Find places to live, eat, and visit in {destination}.""",
expected_output=f"A detailed list of places to live, eat, and visit in {destination}.",
description=f"""Find places to live, eat, and visit in {destination}.
Known preferences for this user:
{user_context or "No stored preferences yet."}
""",
expected_output=f"A detailed list of places to live, eat, and visit in {destination}, tailored to the user's preferences.",
agent=agent,
)
```
## Set Up Crew
Configure the crew with memory integration:
Configure the crew. Mem0 handles persistence outside of CrewAI, so the crew itself does not need `memory=True` or a `memory_config`:
```python
def setup_crew(agents: list, tasks: list):
"""Set up a crew with Mem0 memory integration"""
"""Set up a crew; memory is managed through Mem0, not CrewAI's memory_config"""
return Crew(
agents=agents,
tasks=tasks,
process=Process.sequential,
memory=True,
memory_config={
"provider": "mem0",
"config": {"user_id": "crew_user_1"},
}
)
```
## Main Execution Function
Implement the main function to run the travel planning system:
Implement the main function to run the travel planning system: retrieve context from Mem0, run the crew, then store the new conversation back:
```python
def plan_trip(destination: str, user_id: str):
# Create agent
travel_agent = create_travel_agent()
# Create task
planning_task = create_planning_task(travel_agent, destination)
# Setup crew
user_context = get_user_context(user_id, f"travel preferences for {destination}")
planning_task = create_planning_task(travel_agent, destination, user_context)
crew = setup_crew([travel_agent], [planning_task])
result = crew.kickoff()
# Execute and return results
return crew.kickoff()
client.add(
[{"role": "user", "content": f"Planned a trip to {destination}."}],
user_id=user_id,
)
return result
# Example usage
if __name__ == "__main__":
+89
View File
@@ -0,0 +1,89 @@
---
title: DeepSeek Harness
description: "Add persistent Mem0 memory to the DeepSeek Harness (Cordis) agent with two native tools: search and add."
---
Add persistent memory to the [**DeepSeek Harness**](https://github.com/deepseek-ai/deepseek-harness) with `@mem0/dsh-mem0`. The Harness agent forgets everything between sessions. This plugin gives it two Mem0-backed tools so recall and writes persist across runs, sharing the same memory bank you already use from Claude Code, Codex, and other agents.
## Overview
The plugin registers two agent-callable tools:
| Tool | Does |
|---|---|
| `search_memory` | Recall facts from Mem0 relevant to a query |
| `add_memory` | Store a fact in Mem0 for future sessions |
Unlike file-based memory plugins, Mem0 is a managed backend: server-side extraction, semantic dedup, and conflict resolution, with the same memory reusable across every agent you connect.
## How it works
A Cordis plugin is a module exporting `apply(ctx, config)`. This one declares `inject = ['tools']` so it waits for the harness tool registry, then registers the two tools via `ctx.tools.register(...)`. When the plugin unmounts, the tools are removed automatically (Cordis revertible effects).
## Prerequisites
1. A Mem0 Platform account and API key:
- <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-dsh-mem0" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-dsh-mem0" rel="nofollow">Get your API key</a> (starts with `m0-`)
2. The DeepSeek Harness installed.
3. Your API key exported in your shell:
<CodeGroup>
```bash zsh
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
source ~/.zshrc
```
```bash bash
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
source ~/.bashrc
```
</CodeGroup>
## Try it locally
1. Build the plugin:
```sh
cd integrations/dsh-mem0
pnpm install
pnpm build
```
2. Point the Harness at it. Copy `cordis.example.yml`, set the absolute path to `dist/index.js` and your `userId`, then load it:
```sh
pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
```
3. Open the web UI and ask the agent to remember something, then recall it in a later turn.
The `cordis.yml` entry looks like this:
```yaml
- name: "@deepseek-ai/dsh-system-prompt"
- name: "@deepseek-ai/dsh-tools"
- insert:
- id: mem0
name: "/absolute/path/to/integrations/dsh-mem0/dist/index.js"
config:
# apiKey is read from MEM0_API_KEY when omitted here.
userId: "your-user-id"
# host: "https://your-onprem.mem0.ai" # optional: Platform on-prem / dedicated base URL
```
For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that base URL (defaults to `api.mem0.ai`). `host` is a Platform base-URL override, not a switch to self-hosted Mem0 OSS.
## Configuration
| Field | Required | Default | Notes |
|---|---|---|---|
| `apiKey` | no | `$MEM0_API_KEY` | Mem0 platform API key |
| `userId` | yes | | Default entity that owns the memories |
| `host` | no | `api.mem0.ai` | Platform base URL (on-prem / dedicated) |
Both tools also accept optional per-call `userId`, `agentId`, and `runId` params so a single install can partition memory by entity, agent, or session; when omitted they fall back to the configured `userId`.
## Telemetry
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0's backend can attribute usage to this integration.
+69
View File
@@ -0,0 +1,69 @@
---
title: Strands Agents
description: "Add persistent long-term memory to AWS Strands agents with Mem0, as a native MemoryStore that plugs into the agent loop."
---
Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [Strands Agents](https://github.com/strands-agents/sdk-python), AWS's open-source SDK for building AI agents. The [`strands-mem0`](https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0) package ships a native `MemoryStore`, so recall and writes happen automatically inside the agent loop, not as tool calls the model has to remember.
## Overview
1. A `MemoryStore` the `MemoryManager` drives on every turn: it searches Mem0 and injects the results into the prompt, and writes memory back when extraction is enabled.
2. Server-side extraction: because the store implements `add_messages`, enabling `extraction` routes raw conversation turns to Mem0's own extraction pipeline, with no extra client-side model call.
3. Works with the hosted Mem0 Platform (an API key) or self-hosted Mem0 OSS (a config dict).
## Prerequisites
Before setting up Mem0 with Strands, ensure you have:
1. Installed the required packages:
```bash
pip install strands-mem0
```
2. A valid API key:
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-strands" rel="nofollow">Mem0 API Key</a> (set as `MEM0_API_KEY`)
## Basic Integration Example
Hand a `Mem0MemoryStore` to a `MemoryManager`, and the agent gets automatic recall and memory writes:
```python
import os
from strands import Agent
from strands.memory import MemoryManager
from strands_mem0 import Mem0MemoryStore
os.environ["MEM0_API_KEY"] = "your-mem0-api-key"
# extraction=True routes conversation turns to Mem0's server-side extraction.
store = Mem0MemoryStore(user_id="alex", extraction=True)
agent = Agent(memory_manager=MemoryManager(stores=[store]))
agent("Remember I use Neovim and deploy on Fridays.") # writes memory
print(agent("What editor do I use?")) # recalls it, injected automatically
```
Scope memories with any of `user_id`, `agent_id`, `run_id`, or `app_id` (`app_id` is platform-only). Pass `max_search_results` to bound how many memories are injected per turn.
## Self-hosted Mem0 (OSS)
To run against self-hosted Mem0 instead of the platform, pass a `config` dict:
```python
store = Mem0MemoryStore(
user_id="alex",
extraction=True,
config={
"vector_store": {"provider": "qdrant", "config": {"host": "localhost", "port": 6333}},
},
)
```
## Explicit memory tool
If you want the model to call memory explicitly instead of (or alongside) the automatic store, use the `mem0_memory` tool from `strands-agents-tools`. A store and the tool can share the same Mem0 backend and namespace.
## Learn more
- [strands-mem0 on GitHub](https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0)
- [Strands Agents documentation](https://strandsagents.com)
+4 -2
View File
@@ -185,7 +185,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
## Core Concepts
- [How Mem0 Works](https://docs.mem0.ai/core-concepts/how-it-works) [Both]: Use when explaining the end-to-end pipeline: extraction (ADD-only distillation), storage across vector/entity/history stores, and multi-signal retrieval.
- [Memory Types](https://docs.mem0.ai/core-concepts/memory-types) [Both]: Use when explaining working, factual, episodic, and semantic memory distinctions.
- [Memory Types](https://docs.mem0.ai/core-concepts/memory-types) [Both]: Use when checking which `memory_type` values actually work: `procedural_memory` is implemented, `semantic_memory` and `episodic_memory` are defined in the enum but rejected by validation.
- [Memory Operations - Add](https://docs.mem0.ai/core-concepts/memory-operations/add) [Both]: Use when explaining how `add()` extracts facts, resolves conflicts, and writes to both stores.
- [Memory Operations - Search](https://docs.mem0.ai/core-concepts/memory-operations/search) [Both]: Use when explaining how queries are processed and ranked.
- [Memory Operations - Update](https://docs.mem0.ai/core-concepts/memory-operations/update) [Both]: Use when memories need to be edited in place or reconciled against new info.
@@ -257,14 +257,17 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
- [ChatDev](https://docs.mem0.ai/integrations/chatdev) [Platform]: Use when the user is on ChatDev.
- [Hermes](https://docs.mem0.ai/integrations/hermes) [Both]: Use when the user is on Hermes.
- [Pi Agent](https://docs.mem0.ai/integrations/pi-agent) [Platform]: Use when adding persistent memory to Pi Agent with the Mem0 plugin.
- [DeepSeek Harness](https://docs.mem0.ai/integrations/dsh-mem0) [Platform]: Use when adding persistent memory to the DeepSeek Harness (Cordis) agent via the Mem0 plugin.
- [OpenAI Agents SDK](https://docs.mem0.ai/integrations/openai-agents-sdk) [Platform]: Use when the user is on the OpenAI Agents SDK.
- [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) [Platform]: Use when the user is on Google's Agent Development Kit.
- [Mastra](https://docs.mem0.ai/integrations/mastra) [Platform]: Use when the user is on Mastra (TypeScript).
- [OpenClaw](https://docs.mem0.ai/integrations/openclaw) [Both]: Use when wiring Mem0 into Claude Code or editors via OpenClaw.
- [Vercel AI SDK](https://docs.mem0.ai/integrations/vercel-ai-sdk) [Both]: Use when the user is on the Vercel AI SDK.
- [Strands Agents](https://docs.mem0.ai/integrations/strands) [Both]: Use when the user is on AWS Strands and wants a native MemoryStore.
### AI Coding Tools
- [Claude Code](https://docs.mem0.ai/integrations/claude-code) [Both]: Use when wiring memory into Claude Code.
- [Claude.ai](https://docs.mem0.ai/integrations/claude-ai) [Platform]: Use when connecting Mem0 to Claude.ai (the hosted web app) via a custom remote MCP connector, or when Claude's native memory seems to be crowding out mem0 tool calls.
- [Cursor](https://docs.mem0.ai/integrations/cursor) [Platform]: Use when wiring memory into Cursor.
- [Codex](https://docs.mem0.ai/integrations/codex) [Platform]: Use when wiring memory into Codex / other editor assistants.
- [OpenCode](https://docs.mem0.ai/integrations/opencode) [Platform]: Use when wiring memory into OpenCode.
@@ -294,7 +297,6 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
### Essentials
- [Building an AI Companion](https://docs.mem0.ai/cookbooks/essentials/building-ai-companion) [Both]: Use when starting a companion app from scratch.
- [Partition Memories by Entity](https://docs.mem0.ai/cookbooks/essentials/entity-partitioning-playbook) [Both]: Use when isolating multi-tenant memories.
- [Controlling Memory Ingestion](https://docs.mem0.ai/cookbooks/essentials/controlling-memory-ingestion) [Both]: Use when deciding what to store and what to skip.
- [Tagging and Organizing Memories](https://docs.mem0.ai/cookbooks/essentials/tagging-and-organizing-memories) [Both]: Use when memory taxonomy matters.
- [Exporting Memories](https://docs.mem0.ai/cookbooks/essentials/exporting-memories) [Both]: Use when backing up or migrating memory data.
@@ -6,6 +6,10 @@ icon: "filter"
Enhanced metadata filtering in Mem0 lets you run complex queries across memory metadata. Combine comparisons, logical operators, and wildcard matches to zero in on the exact memories your agent needs.
<Info>
This page covers the self-hosted `Memory` / `AsyncMemory` grammar. Sibling top-level keys are implicitly ANDed on both self-hosted and the hosted Platform API, so a flat filter like `{"user_id": "alice", "category": "work"}` works without wrapping it in `AND` on either. Two real differences remain: the `nin` operator documented below is not part of the Platform contract, and Platform validates every top-level key against a fixed allow-list, rejecting anything else with a 400. See [Memory Filters (Platform)](/platform/features/v2-memory-filters) for the hosted grammar.
</Info>
---
## Feature anatomy
@@ -19,7 +23,7 @@ Enhanced metadata filtering in Mem0 lets you run complex queries across memory m
| `lt` / `lte` | Less than / less than or equal | Cap numeric values (e.g., ratings, timestamps). |
| `in` / `nin` | In list / not in list | Pre-approve or block sets of values without chaining multiple filters. |
| `contains` / `icontains` | Case-sensitive / case-insensitive substring match | Scan text fields for keywords. |
| `*` | Wildcard | Require that a field exists, regardless of value. |
| `*` | Wildcard | Match regardless of value. Exact semantics (field-must-exist vs. no-op) vary by vector store, see [Wildcard matching](#wildcard-matching). |
| `AND` / `OR` / `NOT` | Combine filters | Build logic trees so multiple conditions work together. |
</Accordion>
</AccordionGroup>
@@ -111,7 +115,7 @@ results = m.search(
### Wildcard matching
Allow any value for a field while still requiring the field to exist: handy when the mere presence of a field matters.
Match any value for a field, handy when the mere presence of a field matters.
```python
# Match any value for a field
@@ -124,10 +128,18 @@ results = m.search(
)
```
<Warning>
Wildcard semantics differ by vector store. pgvector requires the key to exist in the payload (a real "field exists" check). Qdrant and Chroma have no native "field exists" filter, so `*` is a no-op there: the field condition is dropped and every record passes, including ones where the field is missing entirely. Do not rely on `*` to exclude records with a missing field unless you have confirmed your store's behavior in `mem0/vector_stores/<provider>.py`.
</Warning>
### Logical combinations
Combine filters with `AND`, `OR`, and `NOT` to express complex decision trees. Nest logical operators to encode multi-branch workflows.
<Warning>
The examples on this page use `search()`. `get_all()` accepts the same entity and comparison-operator filters, but the `AND` / `OR` / `NOT` wrapper is only translated at the `search()` layer before it reaches the vector store. Whether it also works on `get_all()` depends on your vector store: Qdrant recognizes raw `AND` / `OR` / `NOT` keys natively, but pgvector does not, so a logical wrapper passed to `get_all()` on pgvector silently matches nothing. Stick to `search()` for logical trees, or use plain sibling keys (implicitly ANDed) with `get_all()`.
</Warning>
```python
# Logical AND
results = m.search(
@@ -245,25 +257,18 @@ avoid_filters = {
When you reorder filters so indexed fields come first (`good_filters` example), queries typically return faster than the `avoid_filters` pattern where expensive text searches run before simple checks.
</Info>
Vector store support varies. Confirm operator coverage before shipping:
Vector store support varies widely. This table reflects what each provider's filter-translation code (`mem0/vector_stores/<provider>.py`) actually implements, confirm before shipping if you use a store not listed:
<AccordionGroup>
<Accordion title="Qdrant">
Full comparison, list, and logical support. Handles deeply nested boolean logic efficiently.
</Accordion>
<Accordion title="Chroma">
Equality and basic comparisons only. Limited nesting: break large trees into smaller calls.
</Accordion>
<Accordion title="Pinecone">
Comparisons plus `in`/`nin`. Text operators are constrained; rely on tags where possible.
</Accordion>
<Accordion title="Weaviate">
Full operator coverage with advanced text filters. Best option when you need hybrid text + metadata queries.
</Accordion>
</AccordionGroup>
| Store | `eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`in`/`nin` | `contains`/`icontains` | `AND`/`OR`/`NOT` | `*` wildcard |
| --- | --- | --- | --- | --- |
| Qdrant | Full support (`in`/`nin` must be a list, or the Qdrant client raises a validation error) | Yes | Yes, nested | No-op: matches every record regardless of whether the field is present |
| pgvector | Full support (`in`/`nin` must be a list, or the call raises `ValueError`) | Yes, via SQL `LIKE`/`ILIKE` | Yes, nested | Requires the key to exist in the payload |
| Chroma | Full support | No: silently falls back to equality | Yes, one level of nesting | No-op: the filter is dropped |
| Pinecone | Full support | Not implemented | Not implemented: filters are only ANDed field-by-field | Not implemented: `"*"` is matched as the literal string `"*"`, not a wildcard |
| Weaviate | Not implemented: only `user_id`, `agent_id` and `run_id` are filtered on, as exact equality. Every other key, including all metadata, is silently dropped | Not implemented | Not implemented: only an implicit AND across the three supported keys | Not implemented |
<Warning>
If an operator is unsupported, most stores silently ignore that branch. Add validation before execution so you can fall back to simpler queries instead of returning empty results.
If an operator is unsupported, most stores silently ignore it or fall back to equality rather than raising an error. Test filters against your actual store instead of assuming operator parity with Qdrant.
</Warning>
### Migrate from earlier filters
@@ -427,4 +432,7 @@ except ValueError as e:
<Card title="Tag and Organize Memories" icon="tag" href="/cookbooks/essentials/tagging-and-organizing-memories">
Practice building workflows that label and retrieve memories with clear metadata filters.
</Card>
<Card title="Memory Filters (Platform)" icon="cloud" href="/platform/features/v2-memory-filters">
Using the hosted API instead? See the allow-listed top-level fields, the missing `nin` operator, and Platform-only fields like `created_at` and `memory_ids`.
</Card>
</CardGroup>
+2 -12
View File
@@ -76,18 +76,8 @@ await client.add(messages, { userId: "alice" })
```json Output
{
"results": [
{
"memory": "Name is Alice",
"event": "ADD",
"id": "7ae113a3-3cb5-46e9-b6f7-486c36391847"
},
{
"memory": "Likes large pizza with toppings including cherry tomatoes, black olives, green spinach, yellow bell peppers, diced ham, and sliced mushrooms",
"event": "ADD",
"id": "56545065-7dee-4acf-8bf2-a5b2535aabb3"
}
]
"event_id": "evt-uuid",
"status": "PENDING"
}
```
</CodeGroup>
+4 -9
View File
@@ -82,11 +82,11 @@ client.add(messages, { userId: "user1", timestamp: unixTimestamp })
```
```bash cURL
curl -X POST "https://api.mem0.ai/v1/memories/" \
curl -X POST "https://api.mem0.ai/v3/memories/add/" \
-H "Authorization: Token your-api-key" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "I'm travelling to SF"}],
"messages": [{"role": "user", "content": "Travelling to SF"}],
"user_id": "user1",
"timestamp": 1721577600
}'
@@ -94,13 +94,8 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \
```json Output
{
"results": [
{
"id": "a1b2c3d4-e5f6-4g7h-8i9j-k0l1m2n3o4p5",
"data": {"memory": "Travelling to SF"},
"event": "ADD"
}
]
"event_id": "evt-uuid",
"status": "PENDING"
}
```
+57 -8
View File
@@ -29,6 +29,46 @@ Filters use a nested JSON structure with logical operators at the root:
}
```
A single bare condition needs no wrapper:
```python
# Works: one condition, no wrapper needed
filters = {"user_id": "user_123"}
```
Sibling top-level keys in a flat object are accepted too: the API implicitly ANDs them, so the flat form and the explicit `AND` form are equivalent:
```python
# Works: sibling keys are implicitly ANDed
filters = {
"user_id": "user_123",
"categories": {"in": ["finance"]}
}
# Equivalent, explicit form
filters = {
"AND": [
{"user_id": "user_123"},
{"categories": {"in": ["finance"]}}
]
}
```
Reach for an explicit `AND`, `OR`, or `NOT` wrapper when you need `OR` or `NOT` semantics, or when you need to nest conditions. What the API does reject is an unrecognized top-level key:
```python
# Rejected: unknown top-level key
filters = {"user_id": "user_123", "bogus_key": "z"}
# 400: Top-level key must be a logical operator or an allowed field:
# ['AND', 'OR', 'NOT', 'user_id', 'agent_id', 'app_id', 'run_id',
# 'created_at', 'updated_at', 'timestamp', 'expiration_date', 'text',
# 'categories', 'metadata', 'keywords_search', 'memory_ids', 'keywords']
```
<Callout type="info" icon="cloud" color="#00A8FF">
Both the hosted Platform API and the self-hosted OSS SDK implicitly AND sibling top-level keys, so the flat and explicit `AND` forms behave the same way on either. The real differences: Platform validates each top-level key against a fixed allow-list and returns a 400 for anything else, and Platform has no `nin` operator. See [Enhanced Metadata Filtering](/open-source/features/metadata-filtering) for the OSS grammar.
</Callout>
## Available fields and operators
### Entity fields
@@ -42,14 +82,15 @@ Filters use a nested JSON structure with logical operators at the root:
### Time fields
| Field | Operators | Example |
|-------|-----------|---------|
| `created_at` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` | `{"created_at": {"gte": "2024-01-01"}}` |
| `updated_at` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` | `{"updated_at": {"lt": "2024-12-31"}}` |
| `timestamp` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` | `{"timestamp": {"gt": "2024-01-01"}}` |
| `created_at` | `gt`, `gte`, `lt`, `lte`, `ne`, `in`. Explicit `eq` is rejected, pass a bare value instead | `{"created_at": {"gte": "2024-01-01"}}` |
| `updated_at` | `gt`, `gte`, `lt`, `lte`, `ne`, `in`. Explicit `eq` is rejected, pass a bare value instead | `{"updated_at": {"lt": "2024-12-31"}}` |
| `timestamp` | `gt`, `gte`, `lt`, `lte`, `ne`, `in`. Explicit `eq` is rejected, pass a bare value instead | `{"timestamp": {"gt": "2024-01-01"}}` |
| `expiration_date` | `gt`, `gte`, `lt`, `lte`, `ne`, `in`. Explicit `eq` is rejected, pass a bare value instead | `{"expiration_date": {"lte": "2026-12-31"}}` |
### Content fields
| Field | Operators | Example |
|-------|-----------|---------|
| `categories` | `eq`, `ne`, `in` (matches any category in the list), `contains` (case-insensitive) | `{"categories": {"in": ["finance"]}}` |
| `categories` | `in` (takes a list, matches any category in it), `contains` (case-insensitive). `eq` and `ne` are rejected | `{"categories": {"in": ["finance"]}}` |
| `metadata` | `eq`, `ne`, `contains` | `{"metadata": {"key": "value"}}` |
| `keywords` | `contains` (case-sensitive), `icontains` (case-insensitive) | `{"keywords": {"icontains": "invoice"}}` |
@@ -66,6 +107,14 @@ The `*` wildcard matches any non-null value. Records with null values for that f
Use operator keywords exactly as shown (`eq`, `ne`, `gte`, etc.). SQL-style symbols such as `>=` or `!=` are rejected by the Platform API.
</Callout>
<Callout type="warning" icon="exclamation-triangle" color="#E74C3C">
There is no `nin` ("not in") operator on Platform. It is an OSS-only operator; see [Enhanced Metadata Filtering](/open-source/features/metadata-filtering). To exclude a set of values on Platform, wrap an `in` clause in `NOT`:
```python
{"NOT": {"categories": {"in": ["spam", "test"]}}}
```
</Callout>
## Common filter patterns
Use these ready-made filters to target typical retrieval scenarios without rebuilding logic from scratch.
@@ -328,7 +377,7 @@ Level up foundational patterns with compound filters that coordinate entity scop
## Best practices
<Callout type="tip" icon="lightbulb" color="#26A17B">
The root does not have to be `AND`, `OR`, or `NOT`. A bare filter like `{"user_id": "alice"}` works on its own; wrap conditions in a logical operator only when you need to combine more than one.
The root does not have to be `AND`, `OR`, or `NOT`. A bare filter like `{"user_id": "alice"}` works on its own, and several top-level keys in one flat filter are implicitly ANDed. Wrap conditions in a logical operator when you need OR/NOT semantics or nested grouping.
</Callout>
<Callout type="tip" icon="lightbulb" color="#26A17B">
@@ -392,7 +441,7 @@ A filter object with both an `AND` key and a sibling `OR` key at the same level
<AccordionGroup>
<Accordion title="Do I need AND/OR/NOT?">
No. A bare filter like `{"user_id": "u1"}` works on its own. Add `AND`, `OR`, or `NOT` only when you need to combine more than one condition.
No. A bare filter like `{"user_id": "u1"}` works on its own, and several top-level keys in one flat filter like `{"user_id": "u1", "agent_id": "a1"}` are implicitly ANDed. Reach for `AND`, `OR`, or `NOT` when you need OR/NOT semantics or nested grouping, not merely to combine conditions.
</Accordion>
<Accordion title="What does * match?">
@@ -421,8 +470,8 @@ A filter object with both an `AND` key and a sibling `OR` key at the same level
"AND": [
{"user_id": "user_123"},
{"OR": [
{"categories": "finance"},
{"categories": "health"}
{"categories": {"in": ["finance"]}},
{"categories": {"in": ["health"]}}
]}
]
}
+12 -19
View File
@@ -28,11 +28,15 @@ npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
--clients "claude code,cursor,windsurf,vscode,opencode"
```
`mcp-add` is a helper that writes the Mem0 server into each client's own MCP config file, so you do not have to edit them by hand. Name only the clients you actually use. If you would rather see the change yourself, every client's manual config is under [Client-specific setup](#client-specific-setup).
<Note>
Claude Desktop is not in the list above: it rejects the `mcp-add` command. Add it through Settings instead, see [Claude Desktop](#client-specific-setup) below.
</Note>
Restart each client afterwards so it picks up the new server.
## Signing in
@@ -81,25 +85,14 @@ You can also configure individual clients:
<AccordionGroup>
<Accordion title="Claude Desktop">
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude"
```
Claude Desktop does not support the `mcp-add` command, it rejects the server as "not a valid MCP server." Add the Mem0 server through the Settings UI instead:
Or manually add to your Claude Desktop configuration (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"mem0-mcp": {
"type": "http",
"url": "https://mcp.mem0.ai/mcp"
}
}
}
```
1. Open Claude Desktop and go to **Settings > Connectors**.
2. Click **Add custom connector**.
3. Enter a name (for example `mem0-mcp`) and the URL `https://mcp.mem0.ai/mcp`.
4. Save, then restart Claude Desktop.
The first time you use a Mem0 tool, Claude Desktop opens a browser window to sign in, see [Signing in](#signing-in).
</Accordion>
<Accordion title="Claude Code">
+52 -56
View File
@@ -6,7 +6,7 @@ icon: "code-compare"
## Which Mem0 is right for you?
Mem0 offers two powerful ways to add memory to your AI applications. Choose based on your priorities:
Mem0 offers two ways to add memory to your AI applications. Both run the same core extraction and retrieval logic; the Platform adds hosting, a small set of v3-only capabilities, and management surfaces that OSS does not have.
<CardGroup cols={2}>
<Card
@@ -16,7 +16,7 @@ Mem0 offers two powerful ways to add memory to your AI applications. Choose base
>
**Managed, hassle-free**
Get started in 5 minutes with our hosted solution. Perfect for fast iteration and production apps.
Get started in 5 minutes with our hosted solution. No vector store, LLM, or embedder to configure.
</Card>
<Card
@@ -32,89 +32,85 @@ Mem0 offers two powerful ways to add memory to your AI applications. Choose base
---
## Feature Comparison
## What's the same
The core memory loop is identical on both: `add`, `search`, `get`, `get_all`, `update`, `delete`, `delete_all`, and per-memory `history` all exist on the self-hosted `Memory`/`AsyncMemory` classes and on the hosted `MemoryClient`/`AsyncMemoryClient`. Both support:
- **Entity scoping** by `user_id`, `agent_id`, and `run_id`
- **Filter grouping**: both accept `AND`/`OR`/`NOT` wrappers, both implicitly AND a flat multi-key filter like `{"user_id": "alice", "agent_id": "a1"}`, and both accept `*` as a wildcard value. Which fields you may filter on, and which operators each field accepts, differ (see below)
- **Entity-aware ranking**: both extract entities from memory text and use shared entities to boost related results at search time
- **Multimodal input**, **memory expiration** (`expiration_date`), **reranking**, **procedural memory** (Python), and **custom extraction instructions** (`custom_instructions`)
- Python and JavaScript SDKs, plus a REST API (self-hosted via `server/`, or hosted)
## What's actually different
<AccordionGroup>
<Accordion title="Setup & Getting Started" icon="rocket">
<Accordion title="Hosting & infrastructure" icon="server">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **Time to first memory** | 5 minutes | 15-30 minutes |
| **Infrastructure needed** | None | Vector DB + Python/Node env |
| **API key setup** | One environment variable | Configure LLM + embedder + vector DB |
| **Maintenance** | Fully managed by Mem0 | Self-managed |
| **Vector store, LLM, embedder** | Run and tuned by Mem0 | You provision, configure, and pay for each |
| **Scaling & availability** | Managed | Your responsibility |
| **Web dashboard** | `app.mem0.ai` | Not included |
| **Time to first memory** | Minutes (API key only) | Depends on your vector DB / LLM setup |
</Accordion>
<Accordion title="Core Memory Features" icon="brain">
<Accordion title="Entity scoping & workspace structure" icon="layer-group">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **User & agent memories** | ✅ | ✅ |
| **Smart deduplication** | ✅ | ✅ |
| **Semantic search** | ✅ | ✅ |
| **Memory updates** | ✅ | ✅ |
| **Multi-language SDKs** | Python, JavaScript | Python, JavaScript |
| **Scoping identifiers** | `user_id`, `agent_id`, `run_id`, plus `app_id` for app/tenant separation | `user_id`, `agent_id`, `run_id` only, no `app_id` |
| **Organizations & projects** | Multi-org, multi-project, with member roles ([API reference](/api-reference/organization/get-org)) | No org/project concept; a single local config |
| **Project-wide event feed** | `GET /v1/events/` lists recent add/search/delete events per org and project, usable for dashboards, alerting, or audit trails | Only per-memory `history(memory_id)`, no project-wide event log |
See [Entity-Scoped Memory](/platform/features/entity-scoped-memory) for the full `app_id` model.
</Accordion>
<Accordion title="Advanced Capabilities" icon="sparkles">
<Accordion title="Search-time ranking (v3-only)" icon="sparkles">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **Multimodal support** | ✅ | ✅ |
| **Custom categories** | ✅ | Limited |
| **Advanced retrieval** | ✅ | ✅ |
| **Temporal reasoning** | ✅ (v3) | ❌ |
| **Memory decay** | ✅ (v3) | ❌ |
| **Graph memory** | ✅ Built-in | ✅ External graph store |
| **Memory filters v2** | ✅ | ⚠️ (via metadata) |
| **Webhooks** | ✅ | ❌ |
| **Memory export** | ✅ | ❌ |
| **Graph Memory** | Native, always-on graph over extracted entities; connections feed directly into the ranking `score` ([details](/platform/features/graph-memory)) | Removed. OSS previously connected external graph stores (Neo4j, Memgraph, Kuzu, Apache AGE); that integration was dropped when the v3 pipeline landed. OSS still extracts entities and uses them to boost ranking, but there is no queryable graph and no `relations` field |
| **Memory Decay** | Opt-in per project; reinforces recently-used memories and gently dampens stale ones at search time ([details](/platform/features/memory-decay)) | Not supported. Passing `decay` raises `"The decay parameter is not supported by the OSS Memory SDK."` |
| **Temporal Reasoning** | Boosts memories whose event dates match the time expressed in a query (`timestamp` / `reference_date`) ([details](/platform/features/temporal-reasoning)) | Not supported. Both parameters raise a "not supported by the OSS Memory SDK" error |
| **Dream (background consolidation)** | Continuously synthesizes patterns, supersedes outdated facts, and merges duplicates per user ([details](/platform/features/dream)) | Not available |
</Accordion>
<Accordion title="Infrastructure & Scaling" icon="server">
<Accordion title="Configuration & data operations" icon="sliders">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **Hosting** | Managed by Mem0 | Self-hosted |
| **Auto-scaling** | ✅ | Manual |
| **High availability** | ✅ Built-in | DIY setup |
| **Vector DB choice** | Managed | 20+ stores: Qdrant, Pinecone, Chroma, Weaviate, Milvus, pgvector |
| **LLM choice** | Managed (optimized) | 15+ providers: OpenAI, Anthropic, Gemini, Groq, Ollama, Together |
| **Data residency** | US (expandable) | Your choice |
| **Custom categories** | Set per project or per `add` call ([details](/platform/features/custom-categories)) | Not supported. `Memory.add()` has no `custom_categories` parameter, and OSS project updates are rejected outright |
| **Webhooks** | Project-scoped HTTP callbacks on memory and ingest events ([details](/platform/features/webhooks)) | Not available |
| **Memory Export** | Schema-driven structured export jobs over filtered memories ([details](/platform/features/memory-export)) | Not available |
| **Batch operations** | `batch_update` and `batch_delete` apply up to 1000 memories per call | Not available. Loop over the single-memory `update`/`delete` calls |
| **Feedback** | `feedback(memory_id, ...)` records `POSITIVE`, `NEGATIVE`, or `VERY_NEGATIVE` signals against a retrieved memory ([details](/platform/features/feedback-mechanism)) | Not available |
| **Summaries** | `get_summary(filters)` returns a generated summary over the matching memories | Not available |
| **Filterable fields** | Top-level filter keys come from a fixed allowlist: `user_id`, `agent_id`, `app_id`, `run_id`, `created_at`, `updated_at`, `timestamp`, `expiration_date`, `categories`, `metadata`, `keywords`, `memory_ids`, plus the `AND`/`OR`/`NOT` operators and a few endpoint-specific extras. Any other key is rejected with a `400` ([details](/platform/features/v2-memory-filters)) | Any metadata key is filterable directly, with no allowlist |
| **Comparison operators** | Depends on the field: `metadata` accepts `eq`, `ne`, `contains`; `categories` accepts `contains`, `in`; the date fields accept the range operators | The filter layer accepts the full set on any key: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `icontains`. What actually runs depends on the vector store you configured, so confirm operator coverage for yours ([details](/open-source/features/metadata-filtering)) |
</Accordion>
<Accordion title="Pricing & Cost" icon="dollar-sign">
| Aspect | Platform | Open Source |
|--------|----------|-------------|
| **License** | Usage-based pricing | Apache 2.0 (free) |
| **Infrastructure costs** | Included in pricing | You pay for VectorDB + LLM + hosting |
| **Support** | Included | Community + GitHub |
| **Best for** | Fast iteration, production apps | Cost-sensitive, custom requirements |
</Accordion>
<Accordion title="Development & Integration" icon="code">
| Feature | Platform | Open Source |
<Accordion title="Support" icon="life-ring">
| Channel | Platform | Open Source |
|---------|----------|-------------|
| **REST API** | ✅ | ✅ (self-hosted server) |
| **Python SDK** | ✅ | ✅ |
| **JavaScript SDK** | ✅ | ✅ |
| **Framework integrations** | LangChain, CrewAI, LlamaIndex, and 20+ more | Same |
| **Dashboard** | ✅ Web-based | ❌ |
| **Analytics** | ✅ Built-in | DIY |
| **Community & maintainers** | Discord, GitHub Discussions, direct calls with the founders | Same: Discord, GitHub Discussions, direct calls with the founders |
Support channels are currently the same for both. If you need something contractual (a support SLA, for example), ask before assuming it exists: it is not documented as a Platform benefit today.
</Accordion>
</AccordionGroup>
User Profiles are not listed above. The feature is still being finalized internally, so this page does not present it as an available Platform benefit.
---
## Decision Guide
**Choose Platform if you want:**
- Fast time to market: get your AI app with memory live in hours, not weeks.
- Production-ready hosting: auto-scaling, high availability, and managed infrastructure.
- Built-in analytics: track memory usage, query patterns, and user engagement through the dashboard.
- Advanced features: webhooks, memory export, custom categories, and priority support.
- Zero infrastructure: no vector store, LLM, or embedder to provision or tune.
- The v3-only ranking features: Graph Memory, Memory Decay, Temporal Reasoning, and Dream.
- App-level and org/project-level scoping, plus webhooks, memory export, and custom categories.
**Choose Open Source if you need:**
- Full data control: host everything on your infrastructure with complete data residency.
- Custom configuration: choose your own vector DB, LLM provider, embedder, and deployment strategy.
- Extensibility: modify the codebase, add custom features, and contribute back to the community.
- Cost optimization: use local LLMs (Ollama), self-hosted vector DBs, and optimize for your use case.
- Full data control: host everything on your own infrastructure.
- Custom configuration: your own vector DB, LLM provider, and embedder ([25 vector stores](/components/vectordbs/overview), [18 LLM providers](/components/llms/overview), [11 embedders](/components/embedders/overview) at the time of writing).
- Extensibility: modify the codebase, add custom providers, and contribute back.
- Cost optimization: local LLMs (Ollama), self-hosted vector DBs, no usage-based billing.
---
+2 -3
View File
@@ -25,7 +25,7 @@ Cookbooks are narrative tutorials. They start with a real problem, show the brok
## ✅ COPY THIS: Content Skeleton
Paste the block below into a new cookbook, then replace all placeholders. Remove any section you don't need **only after** the happy path works.
```mdx
````mdx
---
title: [Cookbook title: action oriented]
description: [1 sentence outcome]
@@ -175,7 +175,7 @@ Call out the most common mistake or edge case for this layer.
href="#next-link"
/>
</CardGroup>
```
````
---
@@ -189,7 +189,6 @@ Call out the most common mistake or edge case for this layer.
- [ ] Linked related docs (cookbooks, guides, reference) in Next Steps.
Stick to the skeleton above. If you need to deviate, document the rationale in the PR so we can update the template for everyone else.
```
## Browse Other Templates
+5 -2
View File
@@ -9,10 +9,12 @@ Agent and editor integrations. Each subdirectory is self-contained: its own `pac
| `mem0-plugin/` | Claude Code / Cursor / Codex plugin | none | none | pytest |
| `mem0-plugin/.opencode-plugin/` | `@mem0/opencode-plugin` | Bun | none | tsc type-check |
| `pi-agent-plugin/` | `@mem0/pi-agent-plugin` | tsup | none | vitest |
| `dsh-mem0/` | `@mem0/dsh-mem0` | tsup (ESM) | none | vitest |
| `n8n-nodes-mem0/` | `@mem0/n8n-nodes-mem0` | tsc | ESLint (n8n-nodes-base) | none |
| `zapier-mem0/` | `@mem0/zapier` | tsc | none | offline unit tests + `zapier validate` |
| `strands-mem0/` | `strands-mem0` (PyPI) | hatch | Ruff + mypy | pytest |
pnpm everywhere except `.opencode-plugin/`, which uses Bun. Never npm, never yarn.
pnpm everywhere except `.opencode-plugin/` (Bun) and `strands-mem0/` (Python: pip / hatch). Never npm, never yarn.
## Commands
@@ -39,9 +41,10 @@ Run the type check after every TypeScript change: `pnpm run typecheck` or `tsc -
- **`vercel-ai-sdk/`** wraps the Vercel AI SDK through a `createMem0` provider. Integrations for AI-SDK repos go through this wrapper, not raw `MemoryClient`.
- **`mem0-plugin/`** connects Claude Code, Cursor, and Codex to the MCP server at `mcp.mem0.ai` and installs lifecycle hooks for automatic memory capture. Exposes 9 MCP tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`.
- **`openclaw/`**, **`pi-agent-plugin/`** are editor and agent plugins with the same shape.
- **`openclaw/`**, **`pi-agent-plugin/`**, **`dsh-mem0/`** are editor and agent plugins with the same shape. `dsh-mem0/` registers Mem0 search/add tools as a native DeepSeek Harness (Cordis) plugin.
- **`n8n-nodes-mem0/`** is an n8n community node: add, search, get, update, delete.
- **`zapier-mem0/`** is a Zapier Platform CLI app: add, search, get, delete. It deploys to Zapier, not npm, so it is **not** in the release router. Deploy it with `gh workflow run zapier-mem0-cd.yml --ref main` (needs the `ZAPIER_DEPLOY_KEY` secret).
- **`strands-mem0/`** is a native Strands `MemoryStore` (Python, published to PyPI as `strands-mem0`). It plugs into the Strands `MemoryManager` for automatic recall and server-side extraction, over the hosted Mem0 platform or self-hosted Mem0 OSS. The package lives under `strands-mem0/python/`.
## Adding an integration
+7
View File
@@ -0,0 +1,7 @@
# The DeepSeek Harness SDK (@deepseek-ai/dsh-tools) declares the rest of the
# harness runtime (dsh-llm, dsh-agent, dsh-scope, ...) as peer dependencies.
# The host harness provides those at runtime; this adapter only needs cordis +
# dsh-tools to build and type-check. Auto-installing the full peer graph pulls
# unpublished internal packages (e.g. dsh-type-meta), so keep it off.
auto-install-peers=false
package-manager-strict-version=false
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [Taranjeet Singh]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+60
View File
@@ -0,0 +1,60 @@
# dsh-mem0
[Mem0](https://mem0.ai) long-term memory as a native [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (Cordis) plugin.
It gives a Harness agent two memory tools backed by the Mem0 SDK, so recall and writes persist across sessions:
| Tool | Does |
|---|---|
| `search_memory` | Recall facts from Mem0 relevant to a query |
| `add_memory` | Store a fact in Mem0 for future sessions |
Unlike the local/file-based memory plugins in the ecosystem, Mem0 is a managed backend: server-side extraction, semantic dedup and conflict resolution, and the same memory bank reusable across Harness, Claude Code, Codex, and other agents.
## How it works
A Cordis plugin is a module exporting `apply(ctx, config)`. This one declares `inject = ['tools']` so it waits for the harness tool registry, then registers the two tools via `ctx.tools.register(defineTool(...))`. When the plugin unmounts, the tools are removed automatically (Cordis revertible effects).
```
[ mem0ai SDK ] <-- managed memory, owned by Mem0
|
[ dsh-mem0: apply(ctx) -> ctx.tools.register(...) ] <-- this package
|
[ DeepSeek Harness ] <-- the agent, loaded via cordis.yml
```
## Try it locally
1. Build the plugin:
```sh
cd integrations/dsh-mem0
pnpm install
pnpm build
```
2. Set your Mem0 key:
```sh
export MEM0_API_KEY=...
```
3. Point Harness at it. Copy `cordis.example.yml`, set the absolute path to `dist/index.js` and your `userId`, then:
```sh
pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
```
4. Open http://127.0.0.1:3080 and ask the agent to remember something, then recall it in a later turn.
For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that base URL (defaults to `api.mem0.ai`). `host` is a Platform base-URL override — it is not a switch to self-hosted Mem0 OSS, whose server exposes a different API surface.
## Configuration
| Field | Required | Default | Notes |
|---|---|---|---|
| `apiKey` | no | `$MEM0_API_KEY` | Mem0 platform API key |
| `userId` | yes | | Entity that owns the memories |
| `host` | no | `api.mem0.ai` | Platform base URL (on-prem / dedicated) |
## Telemetry
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0's backend can attribute usage to this integration. For it to surface by name (rather than bucketing into `OTHERS`), `DEEPSEEK_HARNESS` must be present in the backend's `KNOWN_EVENT_SOURCES` allowlist, a one-line platform change matching the existing `ZAPIER` / `STRANDS` sources.
## Status
Developer preview. Tracks the DeepSeek Harness v0.1 plugin API (`@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`), which is young and moving; pin versions once it stabilizes. Auto-capture (store turns without an explicit tool call) and auto-recall (inject memory into the prompt at assembly) are planned once the harness session/assembly event API is confirmed.
+19
View File
@@ -0,0 +1,19 @@
# Example DeepSeek Harness config that loads dsh-mem0 alongside the built-in
# tools plugin. Load with:
#
# pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
#
# The tools plugin (and its system-prompt dependency) must be present, because
# the memory tools contribute schemas the system prompt renders.
- name: "@deepseek-ai/dsh-system-prompt"
- name: "@deepseek-ai/dsh-tools"
- insert:
- id: mem0
# Absolute path to the built plugin (run `pnpm build` first), or point
# at src/index.ts when running through tsx during development.
name: "/absolute/path/to/mem0/integrations/dsh-mem0/dist/index.js"
config:
# apiKey is read from the MEM0_API_KEY env var when omitted here.
userId: "your-user-id"
# host: "https://your-onprem.mem0.ai" # optional: Platform on-prem / dedicated base URL
+59
View File
@@ -0,0 +1,59 @@
{
"name": "@mem0/dsh-mem0",
"version": "0.1.0",
"description": "Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.",
"type": "module",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "integrations/dsh-mem0"
},
"keywords": [
"deepseek-harness",
"dsh",
"dsh-plugin",
"cordis",
"mem0",
"memory",
"agent"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"src",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"mem0ai": "^3.0.7"
},
"peerDependencies": {
"@deepseek-ai/cordis": "*",
"@deepseek-ai/dsh-tools": "*"
},
"devDependencies": {
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/dsh-tools": "0.0.1-rc.1",
"@types/node": "^22.15.0",
"tsup": "^8.5.0",
"typescript": "^5.6.0",
"vitest": "^4.1.7"
}
}
File diff suppressed because it is too large Load Diff
+69
View File
@@ -0,0 +1,69 @@
/**
* Compact, token-cheap rendering of Mem0 results for the model context.
*
* Mirrors the format used by the sibling Mem0 plugins
* (integrations/pi-agent-plugin/src/memory/formatting.ts) so a memory reads the
* same way across every harness: `[category] text (age) [mem0:id]`. Dumping the
* raw search envelope instead would spend most of the tokens on JSON scaffolding.
*/
export interface MemoryLike {
id: string;
memory?: string;
categories?: string[];
createdAt?: Date | string;
}
export function formatAge(date: Date | string): string {
const d = typeof date === "string" ? new Date(date) : date;
const ms = Date.now() - d.getTime();
const minutes = Math.floor(ms / 60_000);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
export function formatMemoryCompact(mem: MemoryLike): string {
const cat = mem.categories?.[0] ?? "uncategorized";
const age = mem.createdAt ? ` (${formatAge(mem.createdAt)})` : "";
return `[${cat}] ${mem.memory ?? "(empty)"}${age} [mem0:${mem.id}]`;
}
export function formatMemoryList(memories: MemoryLike[]): string {
if (memories.length === 0) return "No memories found.";
return memories
.map((m, i) => `${i + 1}. ${formatMemoryCompact(m)}`)
.join("\n");
}
/**
* One-line confirmation for a write.
*
* `client.add` hits the async `/v3/memories/add/` endpoint, which returns
* `{ event_id, status: "PENDING" }` — extraction runs server-side *after* the
* call returns, so the extracted memories are not in this response. Report the
* write as queued in that case; only render a list when the backend actually
* returns memories (older / OSS shapes).
*/
export function formatAddResult(result: unknown): string {
const items: MemoryLike[] = Array.isArray(result)
? (result as MemoryLike[])
: ((result as { results?: MemoryLike[] } | null)?.results ??
(result ? [result as MemoryLike] : []));
const pending = items.find(
(r) => (r as { status?: string }).status === "PENDING",
) as { eventId?: string; event_id?: string } | undefined;
if (pending) {
// The SDK camel-cases response keys (event_id -> eventId); accept either.
const id = pending.eventId ?? pending.event_id;
const evt = id ? ` (event ${id})` : "";
return `Memory queued for background extraction${evt}; it will be searchable shortly.`;
}
if (items.length === 0) return "Memory stored.";
const noun = items.length === 1 ? "memory" : "memories";
return `Stored ${items.length} ${noun}:\n${formatMemoryList(items)}`;
}
+138
View File
@@ -0,0 +1,138 @@
/**
* dsh-mem0: Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.
*
* Registers two agent-callable tools backed by the Mem0 SDK:
* - `search_memory` recalls facts relevant to a query
* - `add_memory` stores a fact for future sessions
*
* A plugin is a Cordis module that exports `apply(ctx, config)`. Declaring
* `inject = ['tools']` holds the plugin until the harness tool registry exists;
* tools registered via `ctx.tools.register(...)` are auto-unregistered when the
* plugin unmounts (Cordis revertible effects).
*/
import type { Context } from "@deepseek-ai/cordis";
import { defineTool } from "@deepseek-ai/dsh-tools";
import { MemoryClient } from "mem0ai";
import { formatMemoryList, formatAddResult } from "./formatting.ts";
import { truncateOutput } from "./output.ts";
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
export const name = "mem0";
export const inject = ["tools"];
// Tags writes so Mem0's backend attributes them to this integration in
// telemetry. The backend keeps recognized values via its KNOWN_EVENT_SOURCES
// allowlist; unknown values bucket into "OTHERS", so "DEEPSEEK_HARNESS" must be
// added to that allowlist for usage to surface by name (a one-line backend PR,
// same pattern as the ZAPIER / STRANDS sources).
const SOURCE = "DEEPSEEK_HARNESS";
const DEFAULT_SEARCH_LIMIT = 10;
export interface Config {
/** Mem0 API key. Defaults to the MEM0_API_KEY env var. */
apiKey?: string;
/** Default entity that owns the memories (Mem0 user scope). */
userId: string;
/** Optional Mem0 Platform base-URL override (on-prem / dedicated); defaults to api.mem0.ai. Not a switch to self-hosted OSS. */
host?: string;
}
// Both tools return a single text string; the render is identical, so lift it
// into one shared declaration instead of repeating the block per tool.
const textOutput = {
schema: { type: "string" } as const,
render: (_args: unknown, value: string) => [
{ type: "text" as const, text: value },
],
};
// Optional per-call scoping params, shared by both tools. A single harness
// install can serve more than one entity, so the model may override the
// mount-time default per call (see scoping.ts).
const scopeParams = {
userId: {
type: "string",
description:
"Entity that owns the memory. Defaults to the plugin's configured userId; set this only to read or write another user's memories.",
},
agentId: {
type: "string",
description: "Optional agent scope, to partition memories by agent.",
},
runId: {
type: "string",
description: "Optional run/session scope, to partition memories by session.",
},
} as const;
export function apply(ctx: Context, config: Config): void {
const apiKey = config.apiKey ?? process.env.MEM0_API_KEY;
if (!apiKey) {
throw new Error("dsh-mem0: set config.apiKey or the MEM0_API_KEY env var");
}
const userId = config.userId;
if (!userId) {
throw new Error("dsh-mem0: config.userId is required");
}
const client = new MemoryClient({
apiKey,
...(config.host ? { host: config.host } : {}),
});
// Recall. The platform rejects top-level entity params on search, so scope
// goes inside `filters` (unlike add below, which takes them top-level).
ctx.tools.register(
defineTool({
name: "search_memory",
description:
"Search the user's long-term Mem0 memory for facts relevant to a query. Use proactively before answering anything that may depend on what the user told you earlier.",
parameters: {
query: { type: "string", description: "What to recall.", required: true },
limit: {
type: "integer",
description: `Max results to return (default ${DEFAULT_SEARCH_LIMIT}).`,
},
...scopeParams,
},
output: textOutput,
async execute({ query, limit, userId: u, agentId, runId }) {
const filters = resolveSearchFilters({ userId: u, agentId, runId }, userId);
try {
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
const { results } = await client.search(query, { filters, topK });
return truncateOutput(formatMemoryList(results ?? []));
} catch (err) {
return `search_memory failed: ${err instanceof Error ? err.message : String(err)}`;
}
},
}),
);
// Write. `source` tags the memory for telemetry attribution.
ctx.tools.register(
defineTool({
name: "add_memory",
description:
"Store a fact in the user's long-term Mem0 memory for later sessions. Extraction runs asynchronously server-side, so a stored fact may take a moment to become searchable; do not immediately search to confirm the write.",
parameters: {
text: { type: "string", description: "The fact to remember.", required: true },
...scopeParams,
},
output: textOutput,
async execute({ text, userId: u, agentId, runId }) {
const addParams = resolveAddParams({ userId: u, agentId, runId }, userId);
try {
const result = await client.add([{ role: "user", content: text }], {
...addParams,
source: SOURCE,
});
return truncateOutput(formatAddResult(result));
} catch (err) {
return `add_memory failed: ${err instanceof Error ? err.message : String(err)}`;
}
},
}),
);
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Hard cap on tool output before it reaches the model context.
*
* Same guard the sibling plugins apply (200 lines / 50KB, see
* integrations/pi-agent-plugin/src/memory/tools.ts): a large recall or a wide
* result set can otherwise flood the context window in a single tool call.
*/
export const MAX_OUTPUT_LINES = 200;
export const MAX_OUTPUT_BYTES = 50_000;
export function truncateOutput(text: string): string {
const lines = text.split("\n");
if (lines.length <= MAX_OUTPUT_LINES && text.length <= MAX_OUTPUT_BYTES) {
return text;
}
const kept = lines.slice(0, MAX_OUTPUT_LINES);
let result = kept.join("\n");
const byteCapped = result.length > MAX_OUTPUT_BYTES;
if (byteCapped) {
result = result.slice(0, MAX_OUTPUT_BYTES);
}
const dropped = lines.length - kept.length;
const reasons: string[] = [];
if (dropped > 0) reasons.push(`showing ${kept.length} of ${lines.length} lines`);
if (byteCapped) reasons.push(`cut at ${Math.floor(MAX_OUTPUT_BYTES / 1000)}KB`);
if (reasons.length > 0) {
result += `\n\n[Output truncated: ${reasons.join(", ")}]`;
}
return result;
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Per-call memory scoping.
*
* The plugin is mounted with one default `userId`, but a single harness install
* can serve more than one entity, so both tools accept optional `userId` /
* `agentId` / `runId` params that override the mount-time default per call.
* A missing or blank param falls back to the configured user.
*
* The two call sites need different key casing, and it is deliberate rather than
* incidental: search passes scope inside `filters`, sent to the platform raw, so
* it must be snake_case; add takes the entity params top-level, through the
* SDK's camel->snake converter, so it must be camelCase. Keeping the split
* explicit (like integrations/pi-agent-plugin/src/memory/scoping.ts) means the
* asymmetry is visible in the code, not load-bearing on a converter no-op.
*/
export interface EntityParams {
userId?: string;
agentId?: string;
runId?: string;
}
const clean = (v: string | undefined) => v?.trim() || undefined;
/** Search: snake_case, spread into `filters` and passed to the platform raw. */
export function resolveSearchFilters(
params: EntityParams,
defaultUserId: string,
): Record<string, string> {
const filters: Record<string, string> = {
user_id: clean(params.userId) ?? defaultUserId,
};
const agentId = clean(params.agentId);
if (agentId) filters.agent_id = agentId;
const runId = clean(params.runId);
if (runId) filters.run_id = runId;
return filters;
}
/** Add: camelCase, top-level params run through the SDK's camel->snake converter. */
export function resolveAddParams(
params: EntityParams,
defaultUserId: string,
): Record<string, string> {
const out: Record<string, string> = {
userId: clean(params.userId) ?? defaultUserId,
};
const agentId = clean(params.agentId);
if (agentId) out.agentId = agentId;
const runId = clean(params.runId);
if (runId) out.runId = runId;
return out;
}
+143
View File
@@ -0,0 +1,143 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Offline mock of the Mem0 SDK so these tests never touch the network.
const mockSearch = vi.fn();
const mockAdd = vi.fn();
vi.mock("mem0ai", () => ({
MemoryClient: class {
search = mockSearch;
add = mockAdd;
},
}));
// The real `@deepseek-ai/dsh-tools` runtime transitively imports harness peer
// packages the host provides at runtime but which aren't installed here. For
// these unit tests we only need `defineTool` to hand back the definition it was
// given, so the registered tool's `execute`/`name` can be exercised directly.
vi.mock("@deepseek-ai/dsh-tools", () => ({
defineTool: (options: unknown) => options,
}));
import { apply, type Config } from "../src/index.ts";
interface RegisteredTool {
name: string;
execute(args: unknown, exec: unknown): Promise<unknown>;
}
function applyAndCollect(config: Config): Map<string, RegisteredTool> {
const tools = new Map<string, RegisteredTool>();
const ctx = {
tools: { register: (t: RegisteredTool) => tools.set(t.name, t) },
};
apply(ctx as never, config);
return tools;
}
let savedKey: string | undefined;
beforeEach(() => {
savedKey = process.env.MEM0_API_KEY;
mockSearch.mockReset();
mockAdd.mockReset();
});
afterEach(() => {
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
else process.env.MEM0_API_KEY = savedKey;
});
describe("apply() config validation", () => {
it("throws when no apiKey is set and MEM0_API_KEY is absent", () => {
delete process.env.MEM0_API_KEY;
expect(() => applyAndCollect({ userId: "u" } as Config)).toThrow(/apiKey|MEM0_API_KEY/);
});
it("throws when userId is missing", () => {
expect(() => applyAndCollect({ apiKey: "k", userId: "" } as Config)).toThrow(/userId/);
});
it("registers both memory tools", () => {
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
expect([...tools.keys()].sort()).toEqual(["add_memory", "search_memory"]);
});
});
describe("search_memory tool", () => {
it("returns a formatted list scoped to the configured user", async () => {
mockSearch.mockResolvedValue({
results: [{ id: "m1", memory: "Likes tea", categories: ["preference"] }],
});
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("search_memory")!.execute({ query: "drink" }, {});
expect(out).toContain("Likes tea");
expect(out).toContain("[mem0:m1]");
expect(mockSearch).toHaveBeenCalledWith("drink", {
filters: { user_id: "u" },
topK: 10,
});
});
it("honors a per-call userId override and limit", async () => {
mockSearch.mockResolvedValue({ results: [] });
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
await tools.get("search_memory")!.execute({ query: "x", userId: "alice", limit: 3 }, {});
expect(mockSearch).toHaveBeenCalledWith("x", {
filters: { user_id: "alice" },
topK: 3,
});
});
it("returns a graceful failure line instead of rejecting on error", async () => {
mockSearch.mockRejectedValue(new Error("network down"));
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("search_memory")!.execute({ query: "x" }, {});
expect(out).toContain("search_memory failed");
expect(out).toContain("network down");
});
});
describe("add_memory tool", () => {
it("reports the write as queued on the async PENDING response, with camelCase scope + source", async () => {
// The real /v3/memories/add/ response — not an array of memories. The SDK
// camel-cases response keys, so it surfaces as `eventId`, not `event_id`.
mockAdd.mockResolvedValue({ eventId: "evt-123", status: "PENDING" });
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "remember this" }, {});
expect(out).toContain("queued");
expect(out).toContain("evt-123");
expect(out).not.toContain("No new distinct memory");
expect(mockAdd).toHaveBeenCalledWith(
[{ role: "user", content: "remember this" }],
{ userId: "u", source: "DEEPSEEK_HARNESS" },
);
});
it("renders a list when the backend returns memories", async () => {
mockAdd.mockResolvedValue([{ id: "m1", memory: "Fact" }]);
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
expect(out).toContain("Stored 1 memory");
expect(out).toContain("[mem0:m1]");
});
it("returns a graceful failure line on error", async () => {
mockAdd.mockRejectedValue(new Error("boom"));
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
expect(out).toContain("add_memory failed");
expect(out).toContain("boom");
});
});
@@ -0,0 +1,71 @@
import { describe, it, expect } from "vitest";
import {
formatAge,
formatMemoryCompact,
formatMemoryList,
formatAddResult,
} from "../src/formatting.ts";
describe("formatAge", () => {
it("formats minutes, hours, and days", () => {
expect(formatAge(new Date(Date.now() - 30 * 60_000))).toBe("30m ago");
expect(formatAge(new Date(Date.now() - 3 * 3_600_000))).toBe("3h ago");
expect(formatAge(new Date(Date.now() - 5 * 86_400_000))).toBe("5d ago");
});
});
describe("formatMemoryCompact", () => {
it("renders one line with category, text, and id", () => {
const line = formatMemoryCompact({
id: "abc-123",
memory: "User prefers dark mode",
categories: ["preference"],
createdAt: new Date(),
});
expect(line).toContain("[preference]");
expect(line).toContain("User prefers dark mode");
expect(line).toContain("[mem0:abc-123]");
});
it("falls back to uncategorized and (empty)", () => {
expect(formatMemoryCompact({ id: "x" })).toContain("[uncategorized]");
expect(formatMemoryCompact({ id: "x" })).toContain("(empty)");
});
});
describe("formatMemoryList", () => {
it("numbers multiple memories", () => {
const output = formatMemoryList([
{ id: "id-1", memory: "Fact one", categories: ["insight"] },
{ id: "id-2", memory: "Fact two", categories: ["convention"] },
]);
expect(output).toContain("1.");
expect(output).toContain("2.");
});
it("returns a plain message when there are no memories", () => {
expect(formatMemoryList([])).toBe("No memories found.");
});
});
describe("formatAddResult", () => {
it("reports queued for the async PENDING response, with the event id", () => {
// SDK camel-cases response keys, so the real shape is `eventId`.
const out = formatAddResult({ eventId: "evt-9", status: "PENDING" });
expect(out).toContain("queued");
expect(out).toContain("evt-9");
});
it("reports the stored count when the backend returns memories", () => {
expect(formatAddResult([{ id: "1", memory: "A" }])).toContain("Stored 1 memory");
expect(formatAddResult([{ id: "1" }, { id: "2" }])).toContain("Stored 2 memories");
});
it("unwraps a { results: [...] } envelope", () => {
expect(formatAddResult({ results: [{ id: "1", memory: "A" }] })).toContain("Stored 1 memory");
});
it("handles an empty result", () => {
expect(formatAddResult([])).toBe("Memory stored.");
});
});
@@ -0,0 +1,30 @@
import { describe, it, expect } from "vitest";
import { truncateOutput, MAX_OUTPUT_LINES } from "../src/output.ts";
describe("truncateOutput", () => {
it("passes small output through untouched", () => {
expect(truncateOutput("a\nb\nc")).toBe("a\nb\nc");
});
it("caps output at MAX_OUTPUT_LINES and appends a notice", () => {
const many = Array.from({ length: MAX_OUTPUT_LINES + 50 }, (_, i) => `line ${i}`).join("\n");
const out = truncateOutput(many);
expect(out.split("\n").length).toBeLessThanOrEqual(MAX_OUTPUT_LINES + 3);
expect(out).toContain("[Output truncated:");
expect(out).toContain(`of ${MAX_OUTPUT_LINES + 50} lines`);
});
it("caps output that is few lines but very large by bytes", () => {
const huge = "x".repeat(60_000);
const out = truncateOutput(huge);
expect(out.length).toBeLessThan(huge.length);
expect(out).toContain("[Output truncated:");
});
it("reports both reasons when the line cap and the byte cap fire together", () => {
const wide = Array.from({ length: MAX_OUTPUT_LINES + 50 }, () => "x".repeat(300)).join("\n");
const out = truncateOutput(wide);
expect(out).toContain(`of ${MAX_OUTPUT_LINES + 50} lines`);
expect(out).toContain("cut at 50KB");
});
});
@@ -0,0 +1,44 @@
import { describe, it, expect } from "vitest";
import { resolveSearchFilters, resolveAddParams } from "../src/scoping.ts";
describe("resolveSearchFilters (snake_case, for filters)", () => {
it("falls back to the configured default userId", () => {
expect(resolveSearchFilters({}, "default-user")).toEqual({ user_id: "default-user" });
});
it("lets a per-call userId override the default", () => {
expect(resolveSearchFilters({ userId: "alice" }, "default-user")).toEqual({
user_id: "alice",
});
});
it("treats a blank/whitespace userId as absent and falls back", () => {
expect(resolveSearchFilters({ userId: " " }, "default-user")).toEqual({
user_id: "default-user",
});
});
it("includes snake_case agent/run scope only when provided", () => {
expect(
resolveSearchFilters({ userId: "alice", agentId: "agent-1", runId: "run-9" }, "d"),
).toEqual({ user_id: "alice", agent_id: "agent-1", run_id: "run-9" });
});
it("omits blank agent/run scope", () => {
const f = resolveSearchFilters({ agentId: " ", runId: "run-9" }, "default");
expect(f).toEqual({ user_id: "default", run_id: "run-9" });
expect(f).not.toHaveProperty("agent_id");
});
});
describe("resolveAddParams (camelCase, for top-level add params)", () => {
it("uses camelCase keys and falls back to the default userId", () => {
expect(resolveAddParams({}, "default-user")).toEqual({ userId: "default-user" });
});
it("includes camelCase agent/run scope only when provided", () => {
expect(
resolveAddParams({ userId: "alice", agentId: "agent-1", runId: "run-9" }, "d"),
).toEqual({ userId: "alice", agentId: "agent-1", runId: "run-9" });
});
});
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"types": ["node"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
sourcemap: true,
clean: true,
// The harness runtime and the Mem0 SDK are provided by the host / installed
// separately; keep them out of the bundle.
external: [/^node:/, /^@deepseek-ai\//, "mem0ai", /^mem0ai\//],
});
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.13",
"version": "0.2.15",
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.13",
"version": "0.2.15",
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.13",
"version": "0.2.15",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
"author": {
"name": "Mem0",
@@ -13,5 +13,5 @@
"keywords": ["mem0", "memory", "mcp", "personalization", "semantic-search"],
"skills": "./skills/",
"hooks": "./hooks/cursor-hooks.json",
"mcpServers": ".cursor-mcp.json"
"mcpServers": "./.cursor-mcp.json"
}
+4 -1
View File
@@ -100,13 +100,16 @@ This points Codex at the repo's `.agents/plugins/marketplace.json`, which refere
python3 ~/codex-plugins/mem0-source/integrations/mem0-plugin/scripts/install_codex_hooks.py
```
This merges three entries into `~/.codex/hooks.json` with absolute paths pointing into your clone:
This merges six event handlers into `~/.codex/hooks.json` with absolute paths pointing into your clone:
| Event | What it does |
|-------|--------------|
| `SessionStart` | Loads prior memories as bootstrap context |
| `UserPromptSubmit` | Injects relevant memories into the prompt |
| `PreToolUse` (3 handlers) | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
| `PostToolUse` (2 handlers) | Tracks stats, scans bash errors for related memories |
| `Stop` | Reminds the agent to persist learnings at turn end |
| `PreCompact` | Stores a summary before the context is compacted |
Re-running the installer is idempotent (replaces the Mem0 entries rather than duplicating) and preserves any other hooks you have. To remove: `python3 .../install_codex_hooks.py --uninstall`. If you move or delete the clone directory, re-run the installer from the new location — the hooks file stores absolute paths.
+3 -3
View File
@@ -7,13 +7,13 @@
{
"name": "mem0-ensure-deps",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/ensure_deps.sh 2>/dev/null || true",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/ensure_deps.sh || true",
"timeout": 60
},
{
"name": "mem0-session-start",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_session_start.sh 2>/dev/null || true"
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_session_start.sh || true"
}
]
}
@@ -24,7 +24,7 @@
{
"name": "mem0-user-prompt",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_user_prompt.sh 2>/dev/null || true",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_user_prompt.sh || true",
"timeout": 8
}
]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "mem0",
"name": "mem0",
"version": "0.1.6",
"version": "0.1.7",
"description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.",
"author": { "name": "Mem0", "email": "support@mem0.ai" },
"publisher": "mem0ai",
@@ -150,7 +150,7 @@ def main() -> int:
print(f"Installed Mem0 hooks into {HOOKS_FILE}")
print(f"Plugin path: {PLUGIN_ROOT}")
print("Events: PreToolUse, SessionStart, UserPromptSubmit, PostToolUse")
print("Events: PreToolUse, SessionStart, UserPromptSubmit, PostToolUse, Stop, PreCompact")
if not feature_flag_enabled():
print_feature_flag_hint()
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/n8n-nodes-mem0",
"version": "0.1.3",
"version": "0.1.4",
"description": "n8n community node for Mem0 — the memory layer for AI agents. Add, search, get, update, and delete long-term memories.",
"keywords": [
"n8n-community-node-package",
@@ -73,7 +73,9 @@
"lodash@<=4.17.23": ">=4.18.0",
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0",
"brace-expansion@>=2.0.0 <2.1.2": ">=2.1.2 <3.0.0",
"brace-expansion@>=5.0.0 <5.0.8": ">=5.0.8"
"brace-expansion@>=5.0.0 <5.0.8": ">=5.0.8",
"js-yaml@<3.15.1": ">=3.15.1 <4.0.0",
"js-yaml@>=4.0.0 <4.3.1": ">=4.3.1 <5.0.0"
}
}
}
+169 -167
View File
@@ -11,6 +11,8 @@ overrides:
brace-expansion@<1.1.16: '>=1.1.16 <2.0.0'
brace-expansion@>=2.0.0 <2.1.2: '>=2.1.2 <3.0.0'
brace-expansion@>=5.0.0 <5.0.8: '>=5.0.8'
js-yaml@<3.15.1: '>=3.15.1 <4.0.0'
js-yaml@>=4.0.0 <4.3.1: '>=4.3.1 <5.0.0'
importers:
@@ -24,7 +26,7 @@ importers:
version: 20.19.43
'@typescript-eslint/parser':
specifier: ^8.0.0
version: 8.62.1(eslint@8.57.1)(typescript@5.9.3)
version: 8.67.0(eslint@8.57.1)(typescript@5.9.3)
eslint:
specifier: ^8.57.0
version: 8.57.1
@@ -42,7 +44,7 @@ importers:
version: 2.16.0
prettier:
specifier: ^3.3.0
version: 3.9.4
version: 3.9.6
rimraf:
specifier: ^5.0.0
version: 5.0.10
@@ -67,8 +69,8 @@ packages:
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.29.7':
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
'@babel/generator@7.29.8':
resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.29.7':
@@ -109,8 +111,8 @@ packages:
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.7':
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
'@babel/parser@7.29.8':
resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -209,19 +211,19 @@ packages:
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.7':
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
'@babel/traverse@7.29.8':
resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
'@babel/types@7.29.8':
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
'@eslint-community/eslint-utils@4.10.1':
resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
@@ -430,60 +432,60 @@ packages:
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
'@typescript-eslint/parser@8.62.1':
resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==}
'@typescript-eslint/parser@8.67.0':
resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.62.1':
resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==}
'@typescript-eslint/project-service@8.67.0':
resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.62.1':
resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==}
'@typescript-eslint/scope-manager@8.67.0':
resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.62.1':
resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==}
'@typescript-eslint/tsconfig-utils@8.67.0':
resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.62.1':
resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==}
'@typescript-eslint/types@8.67.0':
resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.62.1':
resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==}
'@typescript-eslint/typescript-estree@8.67.0':
resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.62.1':
resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==}
'@typescript-eslint/utils@8.67.0':
resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.62.1':
resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==}
'@typescript-eslint/visitor-keys@8.67.0':
resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.2':
resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==}
'@ungap/structured-clone@1.3.3':
resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.17.0:
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
acorn@8.18.0:
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -498,8 +500,8 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
ansi-regex@6.3.0:
resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
@@ -613,8 +615,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.11.1:
resolution: {integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==}
baseline-browser-mapping@2.11.15:
resolution: {integrity: sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -625,22 +627,22 @@ packages:
bl@5.1.0:
resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==}
brace-expansion@1.1.17:
resolution: {integrity: sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==}
brace-expansion@1.1.18:
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
brace-expansion@2.1.3:
resolution: {integrity: sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==}
brace-expansion@2.1.4:
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
brace-expansion@5.0.8:
resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
brace-expansion@5.0.9:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browserslist@4.28.7:
resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==}
browserslist@4.28.8:
resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
@@ -684,8 +686,8 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
caniuse-lite@1.0.30001806:
resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
caniuse-lite@1.0.30001809:
resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -823,8 +825,8 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
electron-to-chromium@1.5.395:
resolution: {integrity: sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==}
electron-to-chromium@1.5.409:
resolution: {integrity: sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ==}
emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
@@ -1013,8 +1015,8 @@ packages:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
flatted@3.4.4:
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
@@ -1322,8 +1324,8 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
isolated-vm@6.1.2:
resolution: {integrity: sha512-GGfsHqtlZiiurZaxB/3kY7LLAXR3sgzDul0fom4cSyBjx6ZbjpTrFWiH3z/nUfLJGJ8PIq9LQmQFiAxu24+I7A==}
isolated-vm@6.2.0:
resolution: {integrity: sha512-UuSlxSHWt2QuJ5WvBhzlIJx2VVZN/a44SqBbEZFKNdvuSyhOvhmyDo8SQ+njVbhnh/njoL/aW0bUTiFYlpweGQ==}
engines: {node: '>=22.0.0'}
istanbul-lib-coverage@3.2.2:
@@ -1492,12 +1494,12 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
js-yaml@3.15.1:
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
hasBin: true
js-yaml@4.3.0:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
js-yaml@4.3.1:
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
hasBin: true
jsesc@3.1.0:
@@ -1629,8 +1631,8 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
minimatch@10.2.6:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.5:
@@ -1673,8 +1675,8 @@ packages:
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
node-releases@2.0.51:
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
node-releases@2.0.53:
resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==}
engines: {node: '>=18'}
normalize-path@3.0.0:
@@ -1820,8 +1822,8 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@3.9.4:
resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==}
prettier@3.9.6:
resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==}
engines: {node: '>=14'}
hasBin: true
@@ -1932,8 +1934,8 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
sax@1.6.1:
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
engines: {node: '>=11.0.0'}
semver-greatest-satisfied-range@2.0.0:
@@ -2172,8 +2174,8 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
update-browserslist-db@1.3.1:
resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -2311,14 +2313,14 @@ snapshots:
'@babel/core@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse': 7.29.8
'@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3
@@ -2328,10 +2330,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/generator@7.29.7':
'@babel/generator@7.29.8':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
@@ -2340,7 +2342,7 @@ snapshots:
dependencies:
'@babel/compat-data': 7.29.7
'@babel/helper-validator-option': 7.29.7
browserslist: 4.28.7
browserslist: 4.28.8
lru-cache: 5.1.1
semver: 6.3.1
@@ -2348,8 +2350,8 @@ snapshots:
'@babel/helper-module-imports@7.29.7':
dependencies:
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse': 7.29.8
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -2358,7 +2360,7 @@ snapshots:
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.7
'@babel/traverse': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -2373,11 +2375,11 @@ snapshots:
'@babel/helpers@7.29.7':
dependencies:
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@babel/parser@7.29.7':
'@babel/parser@7.29.8':
dependencies:
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)':
dependencies:
@@ -2467,29 +2469,29 @@ snapshots:
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@babel/traverse@7.29.7':
'@babel/traverse@7.29.8':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/types': 7.29.8
debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@babel/types@7.29.7':
'@babel/types@7.29.8':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@bcoe/v8-coverage@0.2.3': {}
'@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)':
'@eslint-community/eslint-utils@4.10.1(eslint@8.57.1)':
dependencies:
eslint: 8.57.1
eslint-visitor-keys: 3.4.3
@@ -2504,7 +2506,7 @@ snapshots:
globals: 13.24.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.3.0
js-yaml: 4.3.1
minimatch: 3.1.5
strip-json-comments: 3.1.1
transitivePeerDependencies:
@@ -2544,7 +2546,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.15.0
js-yaml: 3.15.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.6': {}
@@ -2737,7 +2739,7 @@ snapshots:
'@n8n/expression-runtime@0.8.0':
dependencies:
'@n8n/tournament': 1.0.6
isolated-vm: 6.1.2
isolated-vm: 6.2.0
js-base64: 3.7.2
jssha: 3.3.1
lodash: 4.18.1
@@ -2784,24 +2786,24 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.28.0
'@types/babel__generator@7.27.0':
dependencies:
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@types/babel__traverse@7.28.0':
dependencies:
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@types/graceful-fs@4.1.9':
dependencies:
@@ -2834,46 +2836,46 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/parser@8.62.1(eslint@8.57.1)(typescript@5.9.3)':
'@typescript-eslint/parser@8.67.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.62.1
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.62.1
'@typescript-eslint/scope-manager': 8.67.0
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.67.0
debug: 4.4.3
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.62.1(typescript@5.9.3)':
'@typescript-eslint/project-service@8.67.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3)
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3)
'@typescript-eslint/types': 8.67.0
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.62.1':
'@typescript-eslint/scope-manager@8.67.0':
dependencies:
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/visitor-keys': 8.62.1
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/visitor-keys': 8.67.0
'@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.9.3)':
'@typescript-eslint/tsconfig-utils@8.67.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/types@8.62.1': {}
'@typescript-eslint/types@8.67.0': {}
'@typescript-eslint/typescript-estree@8.62.1(typescript@5.9.3)':
'@typescript-eslint/typescript-estree@8.67.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.62.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3)
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/visitor-keys': 8.62.1
'@typescript-eslint/project-service': 8.67.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3)
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/visitor-keys': 8.67.0
debug: 4.4.3
minimatch: 10.2.5
minimatch: 10.2.6
semver: 7.8.5
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -2881,29 +2883,29 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.62.1(eslint@8.57.1)(typescript@5.9.3)':
'@typescript-eslint/utils@8.67.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.62.1
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3)
'@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.67.0
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.62.1':
'@typescript-eslint/visitor-keys@8.67.0':
dependencies:
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/types': 8.67.0
eslint-visitor-keys: 5.0.1
'@ungap/structured-clone@1.3.2': {}
'@ungap/structured-clone@1.3.3': {}
acorn-jsx@5.3.2(acorn@8.17.0):
acorn-jsx@5.3.2(acorn@8.18.0):
dependencies:
acorn: 8.17.0
acorn: 8.18.0
acorn@8.17.0: {}
acorn@8.18.0: {}
ajv@6.15.0:
dependencies:
@@ -2918,7 +2920,7 @@ snapshots:
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
ansi-regex@6.3.0: {}
ansi-styles@4.3.0:
dependencies:
@@ -3003,7 +3005,7 @@ snapshots:
babel-plugin-jest-hoist@29.6.3:
dependencies:
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.28.0
@@ -3046,7 +3048,7 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.11.1: {}
baseline-browser-mapping@2.11.15: {}
binary-extensions@2.3.0: {}
@@ -3056,16 +3058,16 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
brace-expansion@1.1.17:
brace-expansion@1.1.18:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.1.3:
brace-expansion@2.1.4:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.8:
brace-expansion@5.0.9:
dependencies:
balanced-match: 4.0.4
@@ -3073,13 +3075,13 @@ snapshots:
dependencies:
fill-range: 7.1.1
browserslist@4.28.7:
browserslist@4.28.8:
dependencies:
baseline-browser-mapping: 2.11.1
caniuse-lite: 1.0.30001806
electron-to-chromium: 1.5.395
node-releases: 2.0.51
update-browserslist-db: 1.2.3(browserslist@4.28.7)
baseline-browser-mapping: 2.11.15
caniuse-lite: 1.0.30001809
electron-to-chromium: 1.5.409
node-releases: 2.0.53
update-browserslist-db: 1.3.1(browserslist@4.28.8)
bs-logger@0.2.6:
dependencies:
@@ -3124,7 +3126,7 @@ snapshots:
camelcase@6.3.0: {}
caniuse-lite@1.0.30001806: {}
caniuse-lite@1.0.30001809: {}
chalk@4.1.2:
dependencies:
@@ -3258,7 +3260,7 @@ snapshots:
eastasianwidth@0.2.0: {}
electron-to-chromium@1.5.395: {}
electron-to-chromium@1.5.409: {}
emittery@0.13.1: {}
@@ -3299,7 +3301,7 @@ snapshots:
eslint-plugin-n8n-nodes-base@1.16.7(eslint@8.57.1)(typescript@5.9.3):
dependencies:
'@typescript-eslint/utils': 8.62.1(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils': 8.67.0(eslint@8.57.1)(typescript@5.9.3)
camel-case: 4.1.2
indefinite: 2.5.2
pascal-case: 3.1.2
@@ -3322,14 +3324,14 @@ snapshots:
eslint@8.57.1:
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
'@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1)
'@eslint-community/regexpp': 4.12.2
'@eslint/eslintrc': 2.1.4
'@eslint/js': 8.57.1
'@humanwhocodes/config-array': 0.13.0
'@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8
'@ungap/structured-clone': 1.3.2
'@ungap/structured-clone': 1.3.3
ajv: 6.15.0
chalk: 4.1.2
cross-spawn: 7.0.6
@@ -3351,7 +3353,7 @@ snapshots:
imurmurhash: 0.1.4
is-glob: 4.0.3
is-path-inside: 3.0.3
js-yaml: 4.3.0
js-yaml: 4.3.1
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
lodash.merge: 4.6.2
@@ -3365,8 +3367,8 @@ snapshots:
espree@9.6.1:
dependencies:
acorn: 8.17.0
acorn-jsx: 5.3.2(acorn@8.17.0)
acorn: 8.18.0
acorn-jsx: 5.3.2(acorn@8.18.0)
eslint-visitor-keys: 3.4.3
esprima-next@5.8.4: {}
@@ -3482,11 +3484,11 @@ snapshots:
flat-cache@3.2.0:
dependencies:
flatted: 3.4.2
flatted: 3.4.4
keyv: 4.5.4
rimraf: 3.0.2
flatted@3.4.2: {}
flatted@3.4.4: {}
for-each@0.3.5:
dependencies:
@@ -3806,7 +3808,7 @@ snapshots:
isobject@3.0.1: {}
isolated-vm@6.1.2:
isolated-vm@6.2.0:
dependencies:
node-gyp-build: 4.8.4
@@ -3815,7 +3817,7 @@ snapshots:
istanbul-lib-instrument@5.2.1:
dependencies:
'@babel/core': 7.29.7
'@babel/parser': 7.29.7
'@babel/parser': 7.29.8
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
semver: 6.3.1
@@ -3825,7 +3827,7 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
'@babel/core': 7.29.7
'@babel/parser': 7.29.7
'@babel/parser': 7.29.8
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
semver: 7.8.5
@@ -4095,10 +4097,10 @@ snapshots:
jest-snapshot@29.7.0:
dependencies:
'@babel/core': 7.29.7
'@babel/generator': 7.29.7
'@babel/generator': 7.29.8
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@jest/expect-utils': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
@@ -4171,12 +4173,12 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.15.0:
js-yaml@3.15.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.3.0:
js-yaml@4.3.1:
dependencies:
argparse: 2.0.1
@@ -4286,17 +4288,17 @@ snapshots:
mimic-fn@2.1.0: {}
minimatch@10.2.5:
minimatch@10.2.6:
dependencies:
brace-expansion: 5.0.8
brace-expansion: 5.0.9
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.17
brace-expansion: 1.1.18
minimatch@9.0.9:
dependencies:
brace-expansion: 2.1.3
brace-expansion: 2.1.4
minimist@1.2.8: {}
@@ -4342,7 +4344,7 @@ snapshots:
node-int64@0.4.0: {}
node-releases@2.0.51: {}
node-releases@2.0.53: {}
normalize-path@3.0.0: {}
@@ -4479,7 +4481,7 @@ snapshots:
prelude-ls@1.2.1: {}
prettier@3.9.4: {}
prettier@3.9.6: {}
pretty-format@29.7.0:
dependencies:
@@ -4580,7 +4582,7 @@ snapshots:
safer-buffer@2.1.2: {}
sax@1.6.0: {}
sax@1.6.1: {}
semver-greatest-satisfied-range@2.0.0:
dependencies:
@@ -4679,7 +4681,7 @@ snapshots:
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.2.2
ansi-regex: 6.3.0
strip-bom@4.0.0: {}
@@ -4804,9 +4806,9 @@ snapshots:
undici-types@6.21.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.7):
update-browserslist-db@1.3.1(browserslist@4.28.8):
dependencies:
browserslist: 4.28.7
browserslist: 4.28.8
escalade: 3.2.0
picocolors: 1.1.1
@@ -4937,7 +4939,7 @@ snapshots:
xml2js@0.6.2:
dependencies:
sax: 1.6.0
sax: 1.6.1
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "1.0.15",
"version": "1.0.16",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/pi-agent-plugin",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects",
"license": "Apache-2.0",
+15
View File
@@ -0,0 +1,15 @@
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
*.whl
.venv/
venv/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.hatch/
.DS_Store
.idea/
.vscode/
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [Taranjeet Singh]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+10
View File
@@ -0,0 +1,10 @@
strands-mem0
Copyright 2026 Mem0
This product includes software developed at Mem0 (https://mem0.ai).
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
+94
View File
@@ -0,0 +1,94 @@
<div align="center">
<h1>strands-mem0</h1>
<h3>Persistent long-term memory for Strands Agents, backed by Mem0</h3>
<p>
A community <a href="https://strandsagents.com/">Strands Agents</a> integration that plugs
<a href="https://mem0.ai">Mem0</a> in as a first-class <code>MemoryStore</code>.
</p>
</div>
---
`strands-mem0` gives [Strands](https://github.com/strands-agents/sdk-python) agents durable memory
that survives across sessions, backed by [Mem0](https://mem0.ai). Where the
`mem0_memory` tool is called explicitly by the model, `Mem0MemoryStore` plugs into the **agent loop**
directly: the manager recalls context and injects it automatically, and writes new memories, either
verbatim or by extracting facts from the conversation.
- **Automatic recall + injection** — relevant memories are searched and prepended to the prompt every turn, no tool call required.
- **Server-side extraction** — raw conversation turns are handed to Mem0, which distills and de-duplicates facts on its own pipeline (no extra client-side model call).
- **Hosted or self-hosted** — the managed [Mem0 Platform](https://app.mem0.ai) by default, or your own Mem0 OSS backend via a config dict.
## Install
```bash
pip install strands-mem0
```
## Usage
```python
from strands import Agent
from strands.memory import MemoryManager
from strands_mem0 import Mem0MemoryStore
# Recall + write, distilling facts from the conversation via Mem0's server-side extraction.
store = Mem0MemoryStore(user_id="alex", writable=True, extraction=True)
agent = Agent(memory_manager=MemoryManager(stores=[store]))
# The agent now recalls from and writes to Mem0 without any explicit tool call.
agent("Remember that I prefer dark-mode dashboards and only drink oat milk.")
agent("How do I like my dashboards?") # recalls the stored preference
```
Set `MEM0_API_KEY` for the hosted platform (get one at [app.mem0.ai](https://app.mem0.ai)), or pass
`api_key=...`. For a self-hosted Mem0 OSS backend, pass a `config=...` dict instead.
## How it works
`Mem0MemoryStore` implements all three `MemoryStore` hooks:
| Method | Maps to | When it runs |
|---|---|---|
| `search(query)` | `mem0.search(query, filters={...})` | Every turn, to recall and inject context |
| `add(content)` | `mem0.add(content, infer=False)` | The `add_memory` tool / a client-side extractor — stores a fact verbatim |
| `add_messages(messages)` | `mem0.add(rendered_turns, infer=True)` | Extraction — renders conversation turns to text, then hands them to Mem0's **server-side** extraction |
Because `add_messages` is implemented, enabling `extraction` routes conversation turns straight to Mem0's own
extraction pipeline. A store that only implemented `add` would instead need a client-side `ModelExtractor`
(an extra model call) to distill facts first.
### Configuration
| Argument | Default | Description |
|---|---|---|
| `user_id` / `agent_id` / `run_id` / `app_id` | _(at least one required)_ | Mem0 entity scope that owns the memories |
| `name` | `"mem0"` | Store identifier, used to target it from memory tools |
| `writable` | `True` | Whether the manager may write to the store |
| `extraction` | `None` | Automatic extraction (`bool` or `ExtractionConfig`) |
| `max_search_results` | `None` | Default result cap per search (falls back to 5) |
| `metadata` | `None` | Default metadata merged into every write |
| `api_key` / `host` | env | Mem0 platform key / base URL (`api_key` defaults to `$MEM0_API_KEY`) |
| `config` | `None` | Mem0 OSS config dict for a self-hosted backend |
## The explicit tool
For the model-called tool (`store` / `retrieve` / `get` / `delete`), use the
[`mem0_memory`](https://github.com/strands-agents/tools) tool from `strands-agents-tools`. The store and
the tool share one Mem0 backend and namespace.
## Development
The package lives under [`python/`](python/) (monorepo-style layout matching the
[Strands extension-template](https://github.com/strands-agents/extension-template)).
```bash
cd python
pip install hatch
hatch run test # pytest (no live server required — mocked client)
hatch run prepare # format + lint + typecheck + test
```
## License
[Apache-2.0](LICENSE). Mem0 is a trademark of its respective owner. Strands Agents is a project of its respective authors.
@@ -0,0 +1,37 @@
# strands-mem0 (Python)
Persistent long-term memory for [Strands Agents](https://github.com/strands-agents/sdk-python),
backed by [Mem0](https://mem0.ai).
See the [repository README](../README.md) for full usage. Quick start:
```bash
pip install strands-mem0
```
As a `MemoryStore` that plugs into the agent loop (Strands >= 1.45):
```python
from strands import Agent
from strands.memory import MemoryManager
from strands_mem0 import Mem0MemoryStore
store = Mem0MemoryStore(user_id="alex", writable=True, extraction=True)
agent = Agent(memory_manager=MemoryManager(stores=[store]))
```
Set `MEM0_API_KEY` for the hosted platform, or pass `config=...` for a self-hosted Mem0 OSS backend.
## Local development
```bash
pip install hatch
hatch run test # pytest (mocked client, no live server)
hatch run prepare # format + lint + typecheck + test
```
## Release
Publish a GitHub release tagged `strands-mem0-v*` (e.g. `strands-mem0-v0.1.0`). The
release router (`.github/workflows/release.yml`) dispatches `strands-mem0-cd.yml`,
which builds the wheel and publishes it to PyPI via trusted publishing (OIDC).
@@ -0,0 +1,88 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "strands-mem0"
version = "0.1.0"
description = "Persistent long-term memory for Strands agents, backed by Mem0."
readme = "README.md"
requires-python = ">=3.10"
license = "Apache-2.0"
authors = [
{name = "Mem0", email = "founders@mem0.ai"}
]
keywords = ["strands", "strands-agents", "agents", "ai", "memory", "mem0", "vector-search", "personalization"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
# strands-agents>=1.45.0: first release shipping the `strands.memory` module
# (MemoryStore / MemoryManager) that Mem0MemoryStore implements.
dependencies = [
"strands-agents>=1.45.0",
"mem0ai>=2.0.11",
]
[project.urls]
Homepage = "https://mem0.ai"
Documentation = "https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0#readme"
Repository = "https://github.com/mem0ai/mem0"
Issues = "https://github.com/mem0ai/mem0/issues"
[project.optional-dependencies]
dev = [
"pytest>=8.0.0,<9.0.0",
"pytest-asyncio>=0.25.0,<1.0.0",
"ruff>=0.11.0,<1.0.0",
"mypy>=1.15.0,<2.0.0",
"hatch",
]
[tool.hatch.build.targets.wheel]
packages = ["src/strands_mem0"]
[tool.hatch.envs.default]
dependencies = [
"pytest>=8.0.0,<9.0.0",
"pytest-asyncio>=0.25.0,<1.0.0",
"ruff>=0.11.0,<1.0.0",
"mypy>=1.15.0,<2.0.0",
]
[tool.hatch.envs.default.scripts]
test = "pytest {args}"
lint = "ruff check src tests"
format = "ruff format src tests"
typecheck = "mypy src"
prepare = ["format", "lint", "typecheck", "test"]
[tool.ruff]
line-length = 120
include = ["src/**/*.py", "tests/**/*.py"]
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
]
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
asyncio_mode = "auto"
@@ -0,0 +1,26 @@
"""Strands Mem0 -- persistent long-term memory for Strands agents, backed by Mem0.
:class:`Mem0MemoryStore` is a Strands ``MemoryStore`` that plugs into the agent
loop via a :class:`~strands.memory.MemoryManager`, with automatic memory injection
and extraction. It implements both write sinks, so ``extraction`` uses Mem0's
server-side extraction (no extra model call).
For the explicit, model-called tool (``store`` / ``retrieve`` / ``get`` / ``delete``),
use the ``mem0_memory`` tool from ``strands-agents-tools``; a store and the tool can
share one Mem0 backend and namespace.
"""
from importlib.metadata import PackageNotFoundError, version
from strands_mem0.client import Mem0ServiceClient
from strands_mem0.store import Mem0MemoryStore
__all__ = [
"Mem0MemoryStore",
"Mem0ServiceClient",
]
try:
__version__ = version("strands-mem0")
except PackageNotFoundError: # pragma: no cover - only when running from a source tree
__version__ = "0.0.0+unknown"
@@ -0,0 +1,176 @@
"""A thin wrapper around the Mem0 SDK used by :class:`~strands_mem0.store.Mem0MemoryStore`.
Both Mem0 backends -- the hosted platform (:class:`mem0.MemoryClient`) and
self-hosted OSS (:class:`mem0.Memory`) -- expose the same call shape to the store:
- **search** takes the entity scope inside a ``filters`` dict plus ``top_k``.
- **add** takes the entity scope as top-level keyword arguments.
The wrapper hides the two remaining differences:
- ``app_id`` is a platform-only scope; OSS ``Memory.add`` has no ``app_id``
parameter, so it is rejected up front for the OSS backend rather than surfacing
as a ``TypeError`` mid-call.
- the telemetry ``source`` tag is attached to platform writes only (OSS
``Memory.add`` has a fixed signature and would reject an unknown kwarg).
"""
from __future__ import annotations
import inspect
import os
from typing import Any
# Only the synchronous platform client is supported. ``AsyncMemoryClient``'s
# ``add`` / ``search`` are coroutine functions, so ``asyncio.to_thread`` would hand
# back an un-awaited coroutine and every write would silently no-op; it is rejected
# in ``__init__`` rather than listed here.
_PLATFORM_CLIENTS = {"MemoryClient"}
# Tags platform writes so Mem0's backend attributes the memory to this integration
# in telemetry (recognized values live in the backend's KNOWN_EVENT_SOURCES
# allowlist; unknown ones bucket into "OTHERS"). Platform only.
_SOURCE = "STRANDS"
def _is_platform_client(client: Any) -> bool:
"""Whether ``client`` is a hosted Mem0 platform client (vs an OSS ``Memory``)."""
return type(client).__name__ in _PLATFORM_CLIENTS
def _is_async_client(client: Any) -> bool:
"""Whether ``client``'s ``add`` / ``search`` are coroutine functions."""
return inspect.iscoroutinefunction(getattr(client, "add", None)) or inspect.iscoroutinefunction(
getattr(client, "search", None)
)
class Mem0ServiceClient:
"""Thin wrapper around the Mem0 SDK for the memory store.
Exactly one backend is selected at construction time:
- ``client`` given: use it as-is (a :class:`mem0.MemoryClient` or
:class:`mem0.Memory`); mainly for testing and advanced/OSS setups.
- ``config`` given: build a self-hosted :class:`mem0.Memory` from it.
- otherwise: build a hosted :class:`mem0.MemoryClient` from ``api_key`` /
``$MEM0_API_KEY`` (and optional ``host``).
"""
def __init__(
self,
api_key: str | None = None,
host: str | None = None,
config: dict[str, Any] | None = None,
client: Any | None = None,
) -> None:
"""Initialize the Mem0 client.
Args:
api_key: Mem0 platform API key. Falls back to ``$MEM0_API_KEY``.
host: Mem0 platform base URL. Defaults to the SDK default
(``https://api.mem0.ai``).
config: A Mem0 OSS config dict; when given, a self-hosted
:class:`mem0.Memory` is built instead of the platform client.
client: A pre-built Mem0 client to use directly (platform or OSS).
Raises:
ValueError: If ``client`` is an async Mem0 client (its coroutines
would never be awaited off the worker thread).
"""
if client is not None:
if _is_async_client(client):
raise ValueError(
"Async Mem0 clients are not supported. Pass a synchronous "
"mem0.MemoryClient (or a mem0.Memory / config): the store runs the "
"SDK in a worker thread, so an async client's coroutines would "
"never be awaited and every write would silently no-op."
)
self.mem0 = client
self.is_platform = _is_platform_client(client)
return
if config is not None:
try:
from mem0 import Memory
except ImportError as err: # pragma: no cover - exercised via install docs
raise ImportError(
"The mem0ai package is required. Install it with: pip install 'strands-mem0'"
) from err
self.mem0 = Memory.from_config(config)
self.is_platform = False
return
try:
from mem0 import MemoryClient
except ImportError as err: # pragma: no cover - exercised via install docs
raise ImportError("The mem0ai package is required. Install it with: pip install 'strands-mem0'") from err
api_key = api_key or os.environ.get("MEM0_API_KEY")
# MemoryClient(host=None) would override the SDK default with None, so only
# pass host when the caller actually set one.
self.mem0 = MemoryClient(api_key=api_key, host=host) if host else MemoryClient(api_key=api_key)
self.is_platform = True
def _check_scope(self, scope: dict[str, str]) -> None:
"""Reject scope the selected backend cannot honor.
``app_id`` exists only on the platform; the OSS ``Memory`` API has no
``app_id`` parameter, so we fail loudly here rather than let it surface as
a ``TypeError`` on ``add`` or silently miss on ``search``.
"""
if not self.is_platform and "app_id" in scope:
raise ValueError(
"app_id is a Mem0 platform-only scope. The OSS backend supports "
"user_id, agent_id, and run_id; drop app_id or use the platform client."
)
def _write_extras(self) -> dict[str, str]:
"""Extra kwargs attached to platform writes: the telemetry ``source`` tag."""
return {"source": _SOURCE} if self.is_platform else {}
def store_memory(
self,
content: str,
scope: dict[str, str],
metadata: dict[str, Any] | None = None,
) -> Any:
"""Store one discrete fact verbatim (``infer=False``).
Used by the store's ``add`` sink -- the content is already a distilled fact
(from the ``add_memory`` tool or a client-side extractor), so Mem0's own
extraction is skipped to preserve it exactly.
"""
self._check_scope(scope)
return self.mem0.add(content, metadata=metadata, infer=False, **self._write_extras(), **scope)
def store_messages(self, messages: list[dict[str, Any]], scope: dict[str, str]) -> Any:
"""Hand rendered conversation turns to Mem0 for server-side extraction (``infer=True``).
Used by the store's ``add_messages`` sink. Mem0 extracts and de-duplicates
facts on the server, so no client-side model call is needed.
"""
self._check_scope(scope)
return self.mem0.add(messages, infer=True, **self._write_extras(), **scope)
def search_memories(self, query: str, scope: dict[str, str], top_k: int) -> list[dict[str, Any]]:
"""Semantic recall scoped to the store's entity.
Both backends take the scope inside ``filters`` and honor ``top_k``; the
response is normalized to a plain list of memory dicts.
"""
self._check_scope(scope)
response = self.mem0.search(query, filters=dict(scope), top_k=top_k)
return _extract_results(response)
def _extract_results(response: Any) -> list[dict[str, Any]]:
"""Normalize a Mem0 search response to a list of memory dicts.
Mem0 returns ``{"results": [...]}`` (v1.1) or, on older paths, a bare list.
"""
if isinstance(response, dict):
results = response.get("results", [])
return list(results) if isinstance(results, list) else []
if isinstance(response, list):
return response
return []
@@ -0,0 +1,222 @@
"""A Strands ``MemoryStore`` backed by Mem0.
A memory store gives a Strands agent cross-session recall: a
:class:`~strands.memory.MemoryManager` searches it to recall facts and, when
writable, writes new ones -- either directly or via automatic extraction from the
conversation. Unlike the ``mem0_memory`` tool (which the model calls explicitly),
a store plugs into the agent loop out of the box, with memory injection and
extraction triggers handled by the manager.
``Mem0MemoryStore`` implements both write sinks, which is what sets it apart from a
vector-DB-style store:
- :meth:`add` writes a single distilled fact verbatim (``infer=False``). This is
the sink for the ``add_memory`` tool and for a client-side extractor.
- :meth:`add_messages` renders raw conversation turns to text and hands them to
Mem0 for **server-side extraction** (``infer=True``). Because this sink exists,
enabling ``extraction`` routes messages straight to Mem0's own extraction
pipeline -- no extra client-side model call, and Mem0's de-duplication applies.
Example:
```python
from strands import Agent
from strands.memory import MemoryManager
from strands_mem0 import Mem0MemoryStore
# Recall + write, with Mem0 extracting facts from the conversation server-side.
store = Mem0MemoryStore(user_id="alex", writable=True, extraction=True)
agent = Agent(memory_manager=MemoryManager(stores=[store]))
```
Configure the hosted platform via the ``api_key`` argument or the ``MEM0_API_KEY``
environment variable, or pass a Mem0 OSS ``config`` dict for a self-hosted backend.
``app_id`` scope is platform-only.
"""
from __future__ import annotations
import asyncio
from typing import Any
from strands.memory import AddMessagesContext, MemoryEntry, MemoryStore, SearchOptions
from strands.types.content import Message
from strands_mem0.client import Mem0ServiceClient
DEFAULT_MAX_SEARCH_RESULTS = 5
# Entity fields that scope a memory in Mem0. At least one must be set.
_SCOPE_FIELDS = ("user_id", "agent_id", "run_id", "app_id")
class Mem0MemoryStore(MemoryStore):
"""A Strands :class:`~strands.memory.MemoryStore` backed by Mem0.
Implements :meth:`search` (semantic recall), :meth:`add` (a verbatim
single-fact write sink) and :meth:`add_messages` (raw-message ingestion with
Mem0 server-side extraction). Because ``add_messages`` is implemented, enabling
``extraction`` uses Mem0's server-side extraction rather than a client-side
model call.
"""
def __init__(
self,
*,
user_id: str | None = None,
agent_id: str | None = None,
run_id: str | None = None,
app_id: str | None = None,
name: str = "mem0",
description: str | None = "Persistent long-term memory backed by Mem0.",
max_search_results: int | None = None,
writable: bool = True,
extraction: Any = None,
metadata: dict[str, Any] | None = None,
api_key: str | None = None,
host: str | None = None,
config: dict[str, Any] | None = None,
client: Mem0ServiceClient | None = None,
) -> None:
"""Initialize the store.
Args:
user_id: Mem0 user namespace that owns the memories.
agent_id: Mem0 agent namespace.
run_id: Mem0 run/session namespace.
app_id: Mem0 app namespace (platform only).
name: Unique store identifier, used to target it in tools.
description: Human-readable description, included in tool descriptions.
max_search_results: Default maximum results per search.
writable: Whether the store accepts writes.
extraction: Automatic-extraction config (``bool | ExtractionConfig``).
metadata: Default metadata merged into every write.
api_key: Mem0 platform API key (defaults to ``$MEM0_API_KEY``).
host: Mem0 platform base URL.
config: Mem0 OSS config dict for a self-hosted backend.
client: A pre-built :class:`~strands_mem0.client.Mem0ServiceClient`
(for testing, or to wrap your own raw Mem0 client via
``Mem0ServiceClient(client=...)``); when omitted, one is
constructed lazily on first use from ``api_key`` / ``config``.
Raises:
ValueError: If no entity scope (``user_id`` / ``agent_id`` / ``run_id``
/ ``app_id``) is provided.
"""
scope = {
"user_id": user_id,
"agent_id": agent_id,
"run_id": run_id,
"app_id": app_id,
}
self.scope = {key: value for key, value in scope.items() if value}
if not self.scope:
raise ValueError("Mem0MemoryStore requires at least one of user_id, agent_id, run_id, or app_id")
# app_id is platform-only. When a self-hosted OSS backend is requested via
# `config`, fail at construction rather than as a TypeError on the first
# write (OSS Memory.add has no app_id). The injected-client OSS case is
# caught in Mem0ServiceClient, which is the only place that knows the backend.
if "app_id" in self.scope and config is not None:
raise ValueError(
"app_id is a Mem0 platform-only scope and cannot be used with a self-hosted "
"config (OSS Memory has no app_id). Drop app_id or use the platform backend."
)
# MemoryStore Protocol attributes.
self.name = name
self.description = description
self.max_search_results = max_search_results
self.writable = writable
self.extraction = extraction
# Mem0-specific configuration.
self.metadata = metadata
self._api_key = api_key
self._host = host
self._config = config
self._client = client
@property
def client(self) -> Mem0ServiceClient:
"""The Mem0 service client, constructed lazily on first use.
Note: constructing the underlying SDK client can block (the platform client
validates the API key over HTTP; the OSS client builds embedders / vector
stores), so first use is deferred and always happens inside a worker thread
via :func:`asyncio.to_thread`, never on the event loop.
"""
if self._client is None:
self._client = Mem0ServiceClient(api_key=self._api_key, host=self._host, config=self._config)
return self._client
async def search(self, query: str, options: SearchOptions | None = None) -> list[MemoryEntry]:
"""Search Mem0 for entries matching ``query``, ordered by relevance."""
top_k = options.get("max_search_results") if options is not None else None
if top_k is None:
top_k = self.max_search_results
if top_k is None:
top_k = DEFAULT_MAX_SEARCH_RESULTS
# ``self.client`` is resolved inside the thread so lazy construction (a
# blocking call) does not run on the event loop.
memories = await asyncio.to_thread(lambda: self.client.search_memories(query, self.scope, top_k))
return [self._to_entry(memory) for memory in memories]
async def add(self, content: str, metadata: dict[str, Any] | None = None) -> Any:
"""Write a single distilled fact to Mem0 verbatim (``infer=False``).
Extraction writes are at-least-once, so this tolerates duplicate content;
Mem0 de-duplicates on the server.
"""
merged = self._merge_metadata(metadata)
return await asyncio.to_thread(lambda: self.client.store_memory(content, self.scope, merged))
async def add_messages(self, messages: list[Message], context: AddMessagesContext | None = None) -> Any:
"""Ingest raw conversation turns for Mem0 server-side extraction (``infer=True``).
A Strands ``Message.content`` is a list of content blocks (a text block is
``{"text": "..."}``); Mem0 keeps only ``{"type": "text"}`` parts, so the raw
blocks would be dropped. We render each turn's text blocks to a string and
skip turns that render empty (a pure tool-use / tool-result turn), so nothing
silently no-ops.
"""
payload: list[dict[str, str]] = []
for message in messages:
text = self._render_content(message.get("content"))
if text:
payload.append({"role": message["role"], "content": text})
if not payload:
return None
return await asyncio.to_thread(lambda: self.client.store_messages(payload, self.scope))
@staticmethod
def _render_content(content: Any) -> str:
"""Flatten a Strands message ``content`` to plain text.
Accepts either a string or a list of content blocks; joins the text of
every ``{"text": ...}`` block and ignores tool-use / image / other blocks.
"""
if isinstance(content, str):
return content
if isinstance(content, list):
return "\n".join(part["text"] for part in content if isinstance(part, dict) and part.get("text"))
return ""
def _merge_metadata(self, metadata: dict[str, Any] | None) -> dict[str, Any] | None:
"""Merge per-call metadata over the store's default metadata."""
if self.metadata and metadata:
return {**self.metadata, **metadata}
return metadata or self.metadata
@staticmethod
def _to_entry(memory: dict[str, Any]) -> MemoryEntry:
"""Map a Mem0 memory dict to a Strands :class:`~strands.memory.MemoryEntry`."""
content = memory.get("memory") or memory.get("content") or ""
metadata: dict[str, Any] = {}
for key in ("id", "score", "categories", "created_at", "updated_at", *_SCOPE_FIELDS):
value = memory.get(key)
if value is not None:
metadata[key] = value
extra = memory.get("metadata")
if isinstance(extra, dict):
metadata.update(extra)
return MemoryEntry(content=content, metadata=metadata or None)
@@ -0,0 +1,214 @@
"""Tests for Mem0ServiceClient: backend routing, call shapes, and response shaping.
The fakes mirror the *real* mem0ai signatures: a keyword-only ``search`` that
rejects top-level entity params, and a fixed-signature OSS ``add`` with no
``**kwargs``. So a call shape the real SDK would reject fails here too, which is
what the earlier permissive fakes did not do.
"""
import pytest
from strands_mem0.client import Mem0ServiceClient, _extract_results, _is_platform_client
class FakeMemoryClient:
"""Stand-in for mem0.MemoryClient (platform: add/search take **kwargs)."""
def __init__(self):
self.add_calls = []
self.search_calls = []
def add(self, messages, **kwargs):
self.add_calls.append((messages, kwargs))
return {"results": [{"id": "m1"}]}
def search(self, query, **kwargs):
self.search_calls.append((query, kwargs))
return {"results": [{"id": "m1", "memory": "hi"}]}
class FakeMemory:
"""Stand-in for mem0.Memory (OSS) with the real, strict signatures.
``search`` is keyword-only and rejects top-level entity params; ``add`` has a
fixed signature with no ``**kwargs`` (so ``source`` or ``app_id`` is a
``TypeError``), exactly like the shipped SDK.
"""
def __init__(self):
self.add_calls = []
self.search_calls = []
def add(
self,
messages,
*,
user_id=None,
agent_id=None,
run_id=None,
metadata=None,
infer=True,
timestamp=None,
expiration_date=None,
memory_type=None,
prompt=None,
):
self.add_calls.append(
(
messages,
{"user_id": user_id, "agent_id": agent_id, "run_id": run_id, "metadata": metadata, "infer": infer},
)
)
return {"results": []}
def search(self, query, *, top_k=20, filters=None, threshold=0.1, **kwargs):
rejected = kwargs.keys() & {"user_id", "agent_id", "run_id", "app_id"}
if rejected:
raise ValueError(f"Top-level entity parameters {set(rejected)} are not supported in search().")
self.search_calls.append((query, {"top_k": top_k, "filters": filters}))
return {"results": []}
class FakeAsyncMemoryClient:
"""Stand-in for mem0.AsyncMemoryClient: coroutine add/search."""
async def add(self, messages, **kwargs): # pragma: no cover - never called
return {}
async def search(self, query, **kwargs): # pragma: no cover - never called
return {}
def platform_client():
"""A Mem0ServiceClient wrapping a fake platform client."""
fake = FakeMemoryClient()
fake.__class__.__name__ = "MemoryClient"
return Mem0ServiceClient(client=fake), fake
# ---------------------------------------------------------------------------
# backend detection
# ---------------------------------------------------------------------------
def test_detects_platform_by_class_name():
assert _is_platform_client(FakeMemory()) is False
fake = FakeMemoryClient()
fake.__class__.__name__ = "MemoryClient"
assert _is_platform_client(fake) is True
def test_injected_client_sets_platform_flag():
fake = FakeMemoryClient()
fake.__class__.__name__ = "MemoryClient"
assert Mem0ServiceClient(client=fake).is_platform is True
assert Mem0ServiceClient(client=FakeMemory()).is_platform is False
def test_async_client_is_rejected():
"""An async Mem0 client cannot be driven from a worker thread; reject it loudly."""
with pytest.raises(ValueError, match="Async Mem0 clients are not supported"):
Mem0ServiceClient(client=FakeAsyncMemoryClient())
# ---------------------------------------------------------------------------
# write routing
# ---------------------------------------------------------------------------
def test_store_memory_is_verbatim_and_tagged():
"""Platform store_memory writes infer=False, scope top-level, and a source tag."""
client, fake = platform_client()
client.store_memory("a fact", {"user_id": "alex"}, {"k": "v"})
messages, kwargs = fake.add_calls[0]
assert messages == "a fact"
assert kwargs["infer"] is False
assert kwargs["user_id"] == "alex"
assert kwargs["metadata"] == {"k": "v"}
assert kwargs["source"] == "STRANDS"
def test_store_messages_infers_and_tags():
"""Platform store_messages hands turns to Mem0 with infer=True and a source tag."""
client, fake = platform_client()
turns = [{"role": "user", "content": "hi"}]
client.store_messages(turns, {"user_id": "alex"})
messages, kwargs = fake.add_calls[0]
assert messages == turns
assert kwargs["infer"] is True
assert kwargs["source"] == "STRANDS"
def test_oss_writes_omit_source():
"""OSS Memory.add has no source parameter, so the tag must be platform-only.
(If the code passed source here, FakeMemory.add would raise TypeError.)
"""
fake = FakeMemory()
client = Mem0ServiceClient(client=fake)
client.store_memory("a fact", {"user_id": "alex"}, None)
client.store_messages([{"role": "user", "content": "hi"}], {"user_id": "alex"})
assert len(fake.add_calls) == 2
for _, kwargs in fake.add_calls:
assert "source" not in kwargs
def test_oss_add_app_id_is_rejected():
"""app_id is platform-only; the OSS path fails loudly rather than TypeError-ing."""
client = Mem0ServiceClient(client=FakeMemory())
with pytest.raises(ValueError, match="platform-only"):
client.store_memory("f", {"user_id": "alex", "app_id": "app1"}, None)
# ---------------------------------------------------------------------------
# search routing (filters + top_k on both backends)
# ---------------------------------------------------------------------------
def test_platform_search_uses_filters():
"""Platform search passes scope inside filters with top_k, never top-level."""
client, fake = platform_client()
client.search_memories("q", {"user_id": "alex"}, 5)
_, kwargs = fake.search_calls[0]
assert kwargs["filters"] == {"user_id": "alex"}
assert kwargs["top_k"] == 5
assert "user_id" not in kwargs
def test_oss_search_uses_filters():
"""OSS search also takes filters + top_k. The strict fake would raise on the
old top-level/limit call shape, so this is the regression test for blocker 1."""
fake = FakeMemory()
client = Mem0ServiceClient(client=fake)
client.search_memories("q", {"user_id": "alex"}, 5)
_, recorded = fake.search_calls[0]
assert recorded["filters"] == {"user_id": "alex"}
assert recorded["top_k"] == 5
def test_oss_search_app_id_is_rejected():
client = Mem0ServiceClient(client=FakeMemory())
with pytest.raises(ValueError, match="platform-only"):
client.search_memories("q", {"user_id": "alex", "app_id": "app1"}, 5)
# ---------------------------------------------------------------------------
# response normalization
# ---------------------------------------------------------------------------
def test_extract_results_shapes():
assert _extract_results({"results": [{"id": 1}]}) == [{"id": 1}]
assert _extract_results([{"id": 1}]) == [{"id": 1}]
assert _extract_results({"nope": 1}) == []
assert _extract_results(None) == []
@@ -0,0 +1,255 @@
"""Tests for the Mem0MemoryStore (Strands MemoryStore integration).
The store is exercised with a mocked Mem0ServiceClient, so no live Mem0 server
(or the ``mem0ai`` SDK) is required.
"""
from unittest.mock import MagicMock
import pytest
from strands.memory import MemoryEntry, MemoryStore
from strands.memory.types import _has_method, _has_write_sink
from strands_mem0 import Mem0MemoryStore
@pytest.fixture
def mock_client():
"""A mocked Mem0ServiceClient."""
return MagicMock()
def make_store(mock_client, **kwargs):
"""Build a store wired to the mocked client (default scope: user_id=alex)."""
kwargs.setdefault("user_id", "alex")
return Mem0MemoryStore(client=mock_client, **kwargs)
# ---------------------------------------------------------------------------
# Construction / protocol conformance
# ---------------------------------------------------------------------------
def test_requires_a_scope():
"""At least one of user_id / agent_id / run_id / app_id is mandatory."""
with pytest.raises(ValueError, match="at least one of"):
Mem0MemoryStore()
def test_app_id_with_oss_config_rejected_at_construction():
"""app_id is platform-only; pairing it with an OSS config fails at construction."""
with pytest.raises(ValueError, match="platform-only"):
Mem0MemoryStore(app_id="app1", config={"vector_store": {"provider": "qdrant"}})
def test_scope_collects_only_set_fields(mock_client):
"""Only the provided entity fields end up in the scope."""
store = Mem0MemoryStore(client=mock_client, user_id="alex", agent_id="assistant")
assert store.scope == {"user_id": "alex", "agent_id": "assistant"}
def test_is_a_memory_store(mock_client):
"""The store is a genuine MemoryStore subclass (MemoryStore is a
non-runtime-checkable Protocol, so check the MRO rather than isinstance)."""
store = make_store(mock_client)
assert MemoryStore in type(store).__mro__
def test_protocol_attributes_default(mock_client):
"""Protocol attributes take sensible, writable-by-default values."""
store = make_store(mock_client)
assert store.name == "mem0"
assert store.description is not None
assert store.max_search_results is None
assert store.writable is True
assert store.extraction is None
assert store.scope == {"user_id": "alex"}
def test_protocol_attributes_override(mock_client):
"""Config fields are honored."""
store = make_store(
mock_client,
name="notes",
description="d",
max_search_results=3,
writable=False,
extraction=True,
metadata={"team": "growth"},
)
assert store.name == "notes"
assert store.max_search_results == 3
assert store.writable is False
assert store.extraction is True
assert store.metadata == {"team": "growth"}
def test_write_sink_detection(mock_client):
"""Both `add` and `add_messages` are real sinks -- extraction defaults to
Mem0's server-side path (add_messages), not a client-side ModelExtractor."""
store = make_store(mock_client)
assert _has_method(store, "search") is True
assert _has_method(store, "add") is True
assert _has_method(store, "add_messages") is True
assert _has_method(store, "initialize") is False
assert _has_method(store, "get_tools") is False
assert _has_write_sink(store) is True
# ---------------------------------------------------------------------------
# search
# ---------------------------------------------------------------------------
async def test_search_maps_to_memory_entries(mock_client):
"""Mem0 hits are mapped to MemoryEntry with metadata preserved."""
mock_client.search_memories.return_value = [
{
"id": "mem-1",
"memory": "Alex prefers dark roast",
"score": 0.91,
"categories": ["preferences"],
"created_at": "2026-07-02T00:00:00Z",
"user_id": "alex",
"metadata": {"category": "prefs"},
}
]
store = make_store(mock_client)
results = await store.search("coffee")
assert len(results) == 1
entry = results[0]
assert isinstance(entry, MemoryEntry)
assert entry.content == "Alex prefers dark roast"
assert entry.metadata["id"] == "mem-1"
assert entry.metadata["score"] == 0.91
assert entry.metadata["categories"] == ["preferences"]
assert entry.metadata["category"] == "prefs"
async def test_search_default_top_k(mock_client):
"""With no options and no configured max, the default top_k is used."""
mock_client.search_memories.return_value = []
store = make_store(mock_client)
await store.search("q")
mock_client.search_memories.assert_called_once_with("q", {"user_id": "alex"}, 5)
async def test_search_options_override_top_k(mock_client):
"""SearchOptions.max_search_results wins over the configured default."""
mock_client.search_memories.return_value = []
store = make_store(mock_client, max_search_results=3)
await store.search("q", {"max_search_results": 10})
mock_client.search_memories.assert_called_once_with("q", {"user_id": "alex"}, 10)
async def test_search_config_top_k(mock_client):
"""The configured max is used when options omit it."""
mock_client.search_memories.return_value = []
store = make_store(mock_client, max_search_results=7)
await store.search("q")
mock_client.search_memories.assert_called_once_with("q", {"user_id": "alex"}, 7)
async def test_search_handles_missing_content(mock_client):
"""A hit without memory text maps to an empty string, not None."""
mock_client.search_memories.return_value = [{"id": "mem-2"}]
store = make_store(mock_client)
results = await store.search("q")
assert results[0].content == ""
assert results[0].metadata == {"id": "mem-2"}
# ---------------------------------------------------------------------------
# add / add_messages
# ---------------------------------------------------------------------------
async def test_add_writes_a_verbatim_fact(mock_client):
"""add() forwards content, scope and merged metadata to store_memory."""
stored = {"id": "mem-9"}
mock_client.store_memory.return_value = stored
store = make_store(mock_client, metadata={"team": "growth"})
result = await store.add("new fact", {"source": "chat"})
assert result == stored
mock_client.store_memory.assert_called_once_with(
"new fact", {"user_id": "alex"}, {"team": "growth", "source": "chat"}
)
async def test_add_without_metadata_uses_store_default(mock_client):
"""With no per-call metadata, the store's default metadata is used."""
store = make_store(mock_client, metadata={"team": "growth"})
await store.add("fact")
mock_client.store_memory.assert_called_once_with("fact", {"user_id": "alex"}, {"team": "growth"})
async def test_add_messages_renders_content_blocks(mock_client):
"""add_messages renders Strands content blocks to text before sending.
Strands hands content as list[ContentBlock] (a text block is ``{"text": ...}``);
mem0 keeps only text parts, so the store must flatten each turn to a string.
"""
messages = [
{"role": "user", "content": [{"text": "I love hiking"}]},
{"role": "assistant", "content": [{"text": "Noted!"}]},
]
store = make_store(mock_client)
await store.add_messages(messages)
mock_client.store_messages.assert_called_once_with(
[{"role": "user", "content": "I love hiking"}, {"role": "assistant", "content": "Noted!"}],
{"user_id": "alex"},
)
async def test_add_messages_skips_empty_turns(mock_client):
"""A turn with no text (a pure tool-use turn) renders to nothing and is not sent."""
store = make_store(mock_client)
result = await store.add_messages([{"role": "assistant", "content": [{"toolUse": {"name": "x"}}]}])
assert result is None
mock_client.store_messages.assert_not_called()
# ---------------------------------------------------------------------------
# lazy client construction
# ---------------------------------------------------------------------------
def test_client_constructed_lazily(monkeypatch):
"""No Mem0ServiceClient is built until the client property is accessed."""
calls = {"n": 0}
class FakeClient:
def __init__(self, api_key=None, host=None, config=None, client=None):
calls["n"] += 1
self.api_key = api_key
monkeypatch.setattr("strands_mem0.store.Mem0ServiceClient", FakeClient)
store = Mem0MemoryStore(user_id="alex", api_key="m0-x")
assert calls["n"] == 0 # not built yet
client = store.client
assert calls["n"] == 1
assert client.api_key == "m0-x"
# Second access reuses the same instance.
assert store.client is client
assert calls["n"] == 1
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/vercel-ai-provider",
"version": "3.0.1",
"version": "3.0.2",
"description": "Vercel AI Provider for providing memory to LLMs",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -88,8 +88,8 @@
"esbuild": ">=0.28.1",
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0",
"brace-expansion@>=3.0.0 <5.0.8": ">=5.0.8 <6.0.0",
"js-yaml@<3.15.0": ">=3.15.0 <4.0.0",
"js-yaml@>=4.0.0 <4.3.0": ">=4.3.0 <5.0.0"
"js-yaml@<3.15.1": ">=3.15.1 <4.0.0",
"js-yaml@>=4.0.0 <4.3.1": ">=4.3.1 <5.0.0"
}
}
}
+6 -6
View File
@@ -14,8 +14,8 @@ overrides:
esbuild: '>=0.28.1'
brace-expansion@<1.1.16: '>=1.1.16 <2.0.0'
brace-expansion@>=3.0.0 <5.0.8: '>=5.0.8 <6.0.0'
js-yaml@<3.15.0: '>=3.15.0 <4.0.0'
js-yaml@>=4.0.0 <4.3.0: '>=4.3.0 <5.0.0'
js-yaml@<3.15.1: '>=3.15.1 <4.0.0'
js-yaml@>=4.0.0 <4.3.1: '>=4.3.1 <5.0.0'
importers:
@@ -1355,8 +1355,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
js-yaml@3.15.1:
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
hasBin: true
jsesc@3.1.0:
@@ -2230,7 +2230,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.15.0
js-yaml: 3.15.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.6': {}
@@ -3359,7 +3359,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.15.0:
js-yaml@3.15.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
@@ -15,5 +15,5 @@ overrides:
"esbuild": ">=0.28.1"
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0"
"brace-expansion@>=3.0.0 <5.0.8": ">=5.0.8 <6.0.0"
"js-yaml@<3.15.0": ">=3.15.0 <4.0.0"
"js-yaml@>=4.0.0 <4.3.0": ">=4.3.0 <5.0.0"
"js-yaml@<3.15.1": ">=3.15.1 <4.0.0"
"js-yaml@>=4.0.0 <4.3.1": ">=4.3.1 <5.0.0"
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/zapier",
"version": "0.1.1",
"version": "0.1.2",
"description": "Zapier integration for Mem0 — the memory layer for AI agents.",
"keywords": [
"zapier",
@@ -71,7 +71,8 @@
"esbuild": ">=0.28.1",
"undici@<7.29.0": ">=7.29.0 <8.0.0",
"undici@>=8.0.0 <8.9.0": ">=8.9.0 <9.0.0",
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0"
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0",
"js-yaml@<3.15.1": ">=3.15.1 <4.0.0"
}
}
}
+73 -72
View File
@@ -11,6 +11,7 @@ overrides:
undici@<7.29.0: '>=7.29.0 <8.0.0'
undici@>=8.0.0 <8.9.0: '>=8.9.0 <9.0.0'
brace-expansion@<1.1.16: '>=1.1.16 <2.0.0'
js-yaml@<3.15.1: '>=3.15.1 <4.0.0'
importers:
@@ -50,8 +51,8 @@ packages:
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.29.7':
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
'@babel/generator@7.29.8':
resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.29.7':
@@ -92,8 +93,8 @@ packages:
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.7':
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
'@babel/parser@7.29.8':
resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -196,12 +197,12 @@ packages:
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.7':
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
'@babel/traverse@7.29.8':
resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
'@babel/types@7.29.8':
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@0.2.3':
@@ -297,8 +298,8 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@sinclair/typebox@0.27.10':
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
'@sinclair/typebox@0.27.12':
resolution: {integrity: sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==}
'@sinonjs/commons@3.0.1':
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
@@ -409,8 +410,8 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
baseline-browser-mapping@2.10.41:
resolution: {integrity: sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==}
baseline-browser-mapping@2.11.15:
resolution: {integrity: sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -421,8 +422,8 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browserslist@4.28.4:
resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==}
browserslist@4.28.8:
resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
@@ -460,8 +461,8 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
caniuse-lite@1.0.30001800:
resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==}
caniuse-lite@1.0.30001809:
resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -578,8 +579,8 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
electron-to-chromium@1.5.384:
resolution: {integrity: sha512-g6KAKY1vkYsADvSPWvdJsuYT0ixdcu6lUtD9P/wJKGBEDlZVXh2AX42j1mPqqaQPDluWjara9ziQ7xqAeXCt5A==}
electron-to-chromium@1.5.409:
resolution: {integrity: sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ==}
emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
@@ -950,8 +951,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
js-yaml@3.15.1:
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
hasBin: true
jsesc@3.1.0:
@@ -1078,8 +1079,8 @@ packages:
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
node-releases@2.0.50:
resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==}
node-releases@2.0.53:
resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==}
engines: {node: '>=18'}
normalize-path@3.0.0:
@@ -1376,8 +1377,8 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
update-browserslist-db@1.3.1:
resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -1468,14 +1469,14 @@ snapshots:
'@babel/core@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse': 7.29.8
'@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3
@@ -1485,10 +1486,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/generator@7.29.7':
'@babel/generator@7.29.8':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
@@ -1497,7 +1498,7 @@ snapshots:
dependencies:
'@babel/compat-data': 7.29.7
'@babel/helper-validator-option': 7.29.7
browserslist: 4.28.4
browserslist: 4.28.8
lru-cache: 5.1.1
semver: 6.3.1
@@ -1505,8 +1506,8 @@ snapshots:
'@babel/helper-module-imports@7.29.7':
dependencies:
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse': 7.29.8
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -1515,7 +1516,7 @@ snapshots:
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.7
'@babel/traverse': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -1530,11 +1531,11 @@ snapshots:
'@babel/helpers@7.29.7':
dependencies:
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@babel/parser@7.29.7':
'@babel/parser@7.29.8':
dependencies:
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)':
dependencies:
@@ -1626,22 +1627,22 @@ snapshots:
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@babel/traverse@7.29.7':
'@babel/traverse@7.29.8':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/types': 7.29.8
debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@babel/types@7.29.7':
'@babel/types@7.29.8':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
@@ -1653,7 +1654,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.15.0
js-yaml: 3.15.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.6': {}
@@ -1769,7 +1770,7 @@ snapshots:
'@jest/schemas@29.6.3':
dependencies:
'@sinclair/typebox': 0.27.10
'@sinclair/typebox': 0.27.12
'@jest/source-map@29.6.3':
dependencies:
@@ -1839,7 +1840,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@sinclair/typebox@0.27.10': {}
'@sinclair/typebox@0.27.12': {}
'@sinonjs/commons@3.0.1':
dependencies:
@@ -1851,24 +1852,24 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.28.0
'@types/babel__generator@7.27.0':
dependencies:
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@types/babel__traverse@7.28.0':
dependencies:
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@types/graceful-fs@4.1.9':
dependencies:
@@ -1964,7 +1965,7 @@ snapshots:
babel-plugin-jest-hoist@29.6.3:
dependencies:
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.28.0
@@ -1995,7 +1996,7 @@ snapshots:
balanced-match@1.0.2: {}
baseline-browser-mapping@2.10.41: {}
baseline-browser-mapping@2.11.15: {}
brace-expansion@1.1.18:
dependencies:
@@ -2006,13 +2007,13 @@ snapshots:
dependencies:
fill-range: 7.1.1
browserslist@4.28.4:
browserslist@4.28.8:
dependencies:
baseline-browser-mapping: 2.10.41
caniuse-lite: 1.0.30001800
electron-to-chromium: 1.5.384
node-releases: 2.0.50
update-browserslist-db: 1.2.3(browserslist@4.28.4)
baseline-browser-mapping: 2.11.15
caniuse-lite: 1.0.30001809
electron-to-chromium: 1.5.409
node-releases: 2.0.53
update-browserslist-db: 1.3.1(browserslist@4.28.8)
bs-logger@0.2.6:
dependencies:
@@ -2047,7 +2048,7 @@ snapshots:
camelcase@6.3.0: {}
caniuse-lite@1.0.30001800: {}
caniuse-lite@1.0.30001809: {}
chalk@4.1.2:
dependencies:
@@ -2155,7 +2156,7 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
electron-to-chromium@1.5.384: {}
electron-to-chromium@1.5.409: {}
emittery@0.13.1: {}
@@ -2367,7 +2368,7 @@ snapshots:
istanbul-lib-instrument@5.2.1:
dependencies:
'@babel/core': 7.29.7
'@babel/parser': 7.29.7
'@babel/parser': 7.29.8
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
semver: 6.3.1
@@ -2377,7 +2378,7 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
'@babel/core': 7.29.7
'@babel/parser': 7.29.7
'@babel/parser': 7.29.8
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
semver: 7.8.5
@@ -2641,10 +2642,10 @@ snapshots:
jest-snapshot@29.7.0:
dependencies:
'@babel/core': 7.29.7
'@babel/generator': 7.29.7
'@babel/generator': 7.29.8
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
'@babel/types': 7.29.7
'@babel/types': 7.29.8
'@jest/expect-utils': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
@@ -2713,7 +2714,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.15.0:
js-yaml@3.15.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
@@ -2812,7 +2813,7 @@ snapshots:
node-int64@0.4.0: {}
node-releases@2.0.50: {}
node-releases@2.0.53: {}
normalize-path@3.0.0: {}
@@ -3066,9 +3067,9 @@ snapshots:
undici-types@6.21.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.4):
update-browserslist-db@1.3.1(browserslist@4.28.8):
dependencies:
browserslist: 4.28.4
browserslist: 4.28.8
escalade: 3.2.0
picocolors: 1.1.1
@@ -8,3 +8,4 @@ overrides:
"undici@<7.29.0": ">=7.29.0 <8.0.0"
"undici@>=8.0.0 <8.9.0": ">=8.9.0 <9.0.0"
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0"
"js-yaml@<3.15.1": ">=3.15.1 <4.0.0"
@@ -26,6 +26,8 @@ export default {
helpText: 'Override only for self-hosted or non-default deployments.',
},
],
// Shown on the connection label in the Zap editor.
connectionLabel: 'Mem0',
// Shown on the connection label in the Zap editor. Uses the account email
// from the /v1/ping/ test response so users with multiple Mem0 connections
// can tell them apart.
connectionLabel: '{{user_email}}',
};
@@ -1,5 +1,4 @@
import type { ZObject, Bundle, AddResponse, EventResponse } from '../types';
import { captureEvent } from '../telemetry';
const POLL_INTERVAL_MS = 1500;
// Bounded to a 60s poll budget, under Zapier's per-step execution timeout.
@@ -38,6 +37,8 @@ const perform = async (z: ZObject, bundle: Bundle): Promise<AddResponse | EventR
const body: Record<string, unknown> = {
messages: [{ role: bundle.inputData.role || 'user', content: bundle.inputData.content }],
infer,
// Attribution is recorded server-side by Mem0 from this source tag.
source: 'ZAPIER',
};
if (bundle.inputData.user_id) body.user_id = bundle.inputData.user_id;
if (bundle.inputData.agent_id) body.agent_id = bundle.inputData.agent_id;
@@ -69,8 +70,6 @@ const perform = async (z: ZObject, bundle: Bundle): Promise<AddResponse | EventR
if (bundle.inputData.includes) body.includes = bundle.inputData.includes;
if (bundle.inputData.excludes) body.excludes = bundle.inputData.excludes;
captureEvent('zapier.add_memory', bundle.authData?.apiKey, { infer, wait });
const response = await z.request({
url: '/v3/memories/add/',
method: 'POST',
@@ -97,7 +96,7 @@ export default {
noun: 'Memory',
display: {
label: 'Add Memory',
description: 'Extract and store memories from a message.',
description: 'Extracts and stores memories from a message.',
},
operation: {
perform,
@@ -14,7 +14,7 @@ export default {
noun: 'Memory',
display: {
label: 'Delete Memory',
description: 'Delete a single memory by its ID.',
description: 'Deletes a memory by its ID.',
},
operation: {
perform,
@@ -1,12 +1,9 @@
import type { ZObject, Bundle, Memory } from '../types';
import { captureEvent } from '../telemetry';
const perform = async (z: ZObject, bundle: Bundle): Promise<Memory[]> => {
const body: Record<string, unknown> = {};
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
captureEvent('zapier.get_memories', bundle.authData?.apiKey);
const response = await z.request({
url: '/v3/memories/',
method: 'POST',
@@ -25,8 +22,8 @@ export default {
key: 'get_memories',
noun: 'Memory',
display: {
label: 'Get Memories',
description: 'List stored memories for a user.',
label: 'Find Memories by User',
description: 'Finds all stored memories for a user.',
},
operation: {
perform,
@@ -1,5 +1,4 @@
import type { ZObject, Bundle, Memory } from '../types';
import { captureEvent } from '../telemetry';
const perform = async (z: ZObject, bundle: Bundle): Promise<Memory[]> => {
const body: Record<string, unknown> = {
@@ -9,8 +8,6 @@ const perform = async (z: ZObject, bundle: Bundle): Promise<Memory[]> => {
};
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
captureEvent('zapier.search_memories', bundle.authData?.apiKey);
const response = await z.request({
url: '/v3/memories/search/',
method: 'POST',
@@ -25,8 +22,8 @@ export default {
key: 'search_memories',
noun: 'Memory',
display: {
label: 'Search Memories',
description: 'Semantic search over stored memories.',
label: 'Find Memories',
description: 'Finds memories matching a semantic query.',
},
operation: {
perform,
-39
View File
@@ -1,39 +0,0 @@
import { createHash } from 'crypto';
// Anonymous usage telemetry via PostHog, mirroring the Mem0 CLI / plugins.
// Fire-and-forget: never awaited, never throws, so it can neither slow down
// nor break an action. Opt out with MEM0_TELEMETRY=false.
const POSTHOG_API_KEY = 'phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX';
const POSTHOG_HOST = 'https://us.i.posthog.com/i/v0/e/';
export function captureEvent(
event: string,
apiKey: string | undefined,
properties: Record<string, unknown> = {},
): void {
if (process.env.MEM0_TELEMETRY === 'false') return;
try {
// Hash the API key so events are attributable to one account without
// ever transmitting the key itself.
const distinctId = apiKey ? createHash('md5').update(apiKey).digest('hex') : 'zapier-anon';
const payload = {
api_key: POSTHOG_API_KEY,
event,
distinct_id: distinctId,
properties: {
source: 'ZAPIER',
$process_person_profile: false,
...properties,
},
};
void fetch(POSTHOG_HOST, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).catch(() => {
/* swallow — telemetry must never surface to the user */
});
} catch {
/* swallow — telemetry must never break the action */
}
}

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