Compare commits

..

15 Commits

Author SHA1 Message Date
Pratik 1f68430471 test(profiles): use placeholder ids in the job response fixture
The fixture carried a real job_id and event_id from a live generation. The
shape is what the test needs, not the values, and this repo is public.
2026-09-23 10:49:16 -07:00
Pratik 1121efe365 docs(profiles): match the job create response in openapi
The 202 body documented usage_units and a results array. The API returns
entity_count_reserved, event_id and, for sample jobs, entity_ids; results
only appears on GET /v2/profiles/jobs/{id}/. The SDK types already use
entityCountReserved and entityIds, so the spec was the one out of step.

The docs sample example indexes entity_ids with .get(), matching the
notebook and the TS example's ?? [].
2026-09-23 10:29:15 -07:00
karthik be1ded5793 fix(profiles): address SDK/docs review for user profiles v1
Addresses @kartik-mem0's review on mem0#7340, verified against the live
staging profiles API on a neuron:

- generate_profile / sample_profiles accept a caller-supplied idempotency_key,
  so retrying a lost request reuses the job instead of creating a second
  billable one (Python sync+async and TS)
- TS uses the uuid dependency instead of the global crypto.randomUUID(), which
  throws on the supported Node 18 target
- update_profile_settings distinguishes an omitted argument from an explicit
  None, so schema / custom_instructions can be cleared (Python sentinel)
- export ProfileJobResponse and ProfileJobStatus; drop the deprecated
  ProfileTriggerResponse / ProfileSamplesResponse aliases and the unpopulated
  results field; correct usageUnits -> entityCountReserved; add error to
  ProfileResponse
- openapi: nest schema / custom_instructions under entities in the settings
  request and response, add capabilities, and mark entity_type required on the
  job body
- docs sample example polls to a terminal job status with a timeout, then reads
  the create response's entity_ids (status.results raised KeyError)
- notebook: include PARTIALLY_SUCCEEDED in terminal states, raise on timeout,
  and snapshot/restore project settings so a shared env is left as found
- tests: real job_id create shape, idempotency-key reuse, and clear-with-None

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 22:56:48 +05:30
karthik f29820886a docs(profiles): make user-profiles guide and notebook user-only
Remove leftover agent and regenerate references so the docs match the shipped
v1 SDK: drop the entity_type settings argument and the agent entities example
from the guide, and remove the regenerate demo section and cheat-sheet row from
the notebook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 19:53:41 +05:30
karthik d91d793c52 chore(profiles): bump SDKs and add changelog for user profiles v1
Python mem0ai 2.0.20 -> 2.1.0, TypeScript mem0ai 3.1.8 -> 3.2.0 (minor: new
user-profiles methods). Add matching sdk.mdx entries under the Python and
TypeScript tabs to satisfy the changelog CI gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 19:53:34 +05:30
karthik 48ae36ad94 feat(profiles): user-only SDK for v1 (drop regenerate + agent)
Reduce the User Profiles SDK to the shipped v1 scope: user profiles only,
no full rebuild.

- Remove regenerate_profiles / regenerateProfiles (sync + async, Python + TS)
  and the ProfileRegenerateResponse type. Full rebuild is gated off server-side
  (409 not_yet_available).
- Drop the entity_type / entityType parameter from get_profile,
  generate_profile, sample_profiles, and update_profile_settings; every path
  and payload is scoped to "user". Narrow ProfileEntityType to "user".
- Update tests to the user-only surface.

Endpoint paths (/v2/profiles/jobs/, /v2/profiles/settings/,
/v2/entities/user/{id}/profile/) and the per-job Idempotency-Key are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 19:50:48 +05:30
Pratik 949b026991 fix(profiles): correct the settings shape, job entity_type and status type
Four bugs that made the profile SDK unusable against the live API, each
found by running the demo notebook end to end rather than by reading.

1. `update_profile_settings()` sent a flat body:

       {"enabled": ..., "schema": ..., "custom_instructions": ...}

   The API takes only `enabled` and `entities` at the top level and
   answers 400 "Unsupported settings: custom_instructions, schema." to
   anything else, so every call passing a schema failed.
   `get_profile_settings()` already returned the nested shape, so the read
   and the write disagreed and the method could not round-trip its own
   settings. Both SDKs now nest `schema` and `custom_instructions` under
   `entities.<entity_type>` while keeping the flat call signature;
   `entity_type` is a new optional argument defaulting to "user".

2. `sample_profiles()` and `regenerate_profiles()` never sent
   `entity_type`. Every profile job must name an entity kind, so both
   failed with "entity_type must be one of: user, agent."

3. The TypeScript read path rewrote the customer's schema property names.
   Once the schema moved under `entities`, `snakeToCamelKeys` camel-cased
   the keys inside it, because only a top-level schema was restored
   verbatim. A field named `favorite_topics` came back as `favoriteTopics`.

4. `ProfileStatus` declared `notEnabled` and `insufficientData`, but a
   status is a value, not a key, so it is never camel-cased. tsc rejected
   `status === "insufficient_data"`, which is true at runtime, and accepted
   `status === "insufficientData"`, which can never fire. Branching on
   status is the documented way to use a profile, so the type steered
   every TypeScript caller into a dead branch.

Response-shape corrections found alongside them: sample returns
`entity_ids` on create and a richer `results` array on the job, so the TS
`results` field on the create response is marked deprecated and never set;
regenerate answers 409, not the 501 the docstrings claimed.

Verified against a live environment, from both SDKs: a settings write
followed by a read returns the schema property for property, a partial
update no longer blanks it, sample returns 202 with the entities it
picked, regenerate reaches the server and answers its real
not_yet_available, and a user with no profile returns "insufficient_data".

Adds a user-profiles demo notebook covering the whole loop: schema,
ingestion, generation, a before/after diff of a profile rewriting itself,
schema sampling, and a failure-scenario section for each way the API says
no. It polls the add event to a terminal status instead of sleeping, waits
for the extracted memory count to settle rather than trusting the first
page, and reports plainly when generation cannot finish instead of
presenting an empty profile as a result. Executed end to end: 0 failing
cells.

- python: 20 passed (tests/test_client_profiles.py)
- typescript: 203 passed (src/client/tests/)
2026-09-18 17:10:31 -07:00
Pratik 414aef6f1a docs(profiles): drop the sidebar icon and the plan table
Review feedback from Rudraj on the docs preview.

The sidebar icon made Profiles the only entry in platform/features with
one; every sibling page has no icon, so it read as a rendering accident.

The plan table stated Pro/Enterprise availability, which is not how the
feature is reaching customers: it is enabled per organization on request
while in beta. A table naming tiers invites a self-serve upgrade that
does not turn it on.

Keeps the two conditions that are not about pricing — schema configured
and an entity-scoped memory — and the note that a disabled project reads
back not_enabled rather than erroring.
2026-09-18 17:10:03 -07:00
karthik 9192b9b565 docs(profiles): make profiles docs user-only and match the shipped API
Hide agent profiles for this release (backend exists but is gated off) and
sync the guide, api-reference, openapi, and llms.txt to the User Profiles
feature head.

Agent removal:
- Drop "users and agents" framing, the entity_type="agent" get_profile
  example, and the "profiles for agents" FAQ from the guide.
- Reduce entity_type to user-only in the read path/param/response and the
  jobs request body in openapi.json; drop "or agent" from the api-ref and
  llms.txt descriptions.

Code sync (verified against the feature head):
- Read example + prose now include generation_count alongside profile,
  status, entity_type, entity_id, updated_at.
- Sample flow corrected: sample_profiles returns a job; poll get_profile_job
  and read each result's entity, instead of iterating a non-existent
  results field on the create response.
- Bulk regenerate is gated off for v1 (FULL_REBUILD_ENABLED=False): remove
  it from the guide, openapi operation enum, and the 501 note; keep only
  sample and trigger. Idempotency-Key marked required to match the backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-18 03:49:32 +05:30
karthik 4b9a27aac1 fix(profiles): format TS SDK profile client with prettier
The ts-sdk CI Lint step (npx prettier --check .) failed on the three
profile client files, which also failed the aggregate CI Gate. Reformat
them with prettier 3.8.4 (the pinned devDependency); whitespace only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-18 03:18:19 +05:30
Pratik 34894e2cea fix(profiles): follow the jobs API
Four POST routes became one collection with the operation in the body, so the
three route pages collapse into one. samples became sample. Creates send an
Idempotency-Key and return a status_url to poll, which the client follows
rather than building the path.

Full rebuild is closed: regenerate_profiles answers 501 not_yet_available and
the docs say so instead of teaching a daily cadence that cannot run.
2026-09-16 17:02:10 -07:00
karthik c4eea87526 Merge branch 'main' into user/karthik/user-profiles-docs 2026-09-15 22:12:30 +05:30
karthik b852f9240c docs(profiles): correct response example and regenerate cadence from live API
Live e2e against the feature neuron: the profile envelope has no generation_count, and the regenerate cooldown is once per day (86400s), not hourly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-15 21:46:53 +05:30
karthik ac2c5cbfbf docs(profiles): add plan availability, cadence, and FAQ to the profiles guide
Model the profiles feature guide on the Dream guide: add a Plan availability table with eligibility criteria, a 'When profiles update' cadence section, a schema size-budget note, and an FAQ.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-15 21:38:00 +05:30
Pratik 623e33f2db feat(profiles): SDK methods and docs for entity profiles
Adds the profile surface to both SDKs and documents it.

Python (MemoryClient + AsyncMemoryClient) and TypeScript gain:
get/generate profile, get/update settings, sample, regenerate.

A profile's keys come from the customer's own JSON Schema, so the TS
client keeps `profile` opaque and passes `schema` through untouched in
both directions. Camel-casing them would return field names that do not
match the schema the customer wrote.

Reads use the v2 envelope, where a known entity with no profile yet is a
200 carrying a status rather than a 404, so an empty state is
distinguishable from an error.

Verified against a live environment running this API: settings
round-trip, a partial update leaves the schema intact, generation
produces a profile under the configured schema, and sample, regenerate,
cooldown and the 400/404 paths all answer as documented.
2026-09-10 16:04:09 -07:00
289 changed files with 4132 additions and 13618 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0",
"source": "./integrations/claude-code-plugin",
"description": "Cross-session memory and token savings for coding agents.",
"version": "0.3.3"
"version": "0.3.1"
}
]
}
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0",
"source": "./integrations/cursor-plugin",
"description": "Cross-session memory and token savings for coding agents.",
"version": "0.3.3"
"version": "0.3.1"
}
]
}
@@ -32,9 +32,6 @@ jobs:
- name: Type check
run: bun run type-check
- name: Test
run: bun test
- name: Build
run: bun run build
+1 -1
View File
@@ -5,7 +5,7 @@
{
"id": "mem0",
"displayName": "Mem0",
"version": "0.3.3",
"version": "0.3.1",
"description": "Cross-session memory and token savings for coding agents.",
"homepage": "https://mem0.ai",
"keywords": ["memory", "personalization", "mcp", "semantic-search"],
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/cli",
"version": "0.2.14",
"version": "0.2.13",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
+1 -2
View File
@@ -31,8 +31,7 @@ export class PlatformBackend implements Backend {
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
"X-Mem0-Source": "CLI",
"X-Mem0-Client": `mem0-cli-node/${CLI_VERSION}`,
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
"X-Mem0-Client-Version": CLI_VERSION,
};
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.2.13"
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.13"
__version__ = "0.2.12"
+1 -2
View File
@@ -27,8 +27,7 @@ class PlatformBackend(Backend):
headers={
"Authorization": f"Token {config.api_key}",
"Content-Type": "application/json",
"X-Mem0-Source": "CLI",
"X-Mem0-Client": f"mem0-cli-python/{__version__}",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
"X-Mem0-Client-Version": __version__,
},
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "API Reference Overview"
sidebarTitle: "Overview"
title: "Overview"
seo:
title: "API Reference Overview - Mem0"
icon: "terminal"
iconType: "solid"
description: "REST APIs for memory management, search, and entity operations"
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Delete Memory API Endpoint"
sidebarTitle: "Delete Memory"
title: 'Delete Memory'
seo:
title: "Delete Memory API Endpoint - Mem0"
description: "Delete a single memory by its unique memory ID from the Mem0 platform using the DELETE endpoint."
openapi: delete /v1/memories/{memory_id}/
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Update Memory API Endpoint"
sidebarTitle: "Update Memory"
title: 'Update Memory'
seo:
title: "Update Memory API Endpoint - Mem0"
description: "Update the content, metadata, timestamp, or expiration date of a single memory by its unique ID using the PUT endpoint."
openapi: put /v1/memories/{memory_id}/
---
@@ -1,6 +1,7 @@
---
title: "Add Organization Member API Endpoint"
sidebarTitle: "Add Member"
title: 'Add Member'
seo:
title: "Add Organization Member API Endpoint - Mem0"
description: "Add a new member to an organization with a specified role such as READER or OWNER access level."
openapi: post /api/v1/orgs/organizations/{org_id}/members/
---
@@ -1,6 +1,7 @@
---
title: "Get Organization Members API Endpoint"
sidebarTitle: "Get Members"
title: 'Get Members'
seo:
title: "Get Organization Members API Endpoint - Mem0"
description: "Retrieve a list of all members belonging to a specific organization on the Mem0 platform."
openapi: get /api/v1/orgs/organizations/{org_id}/members/
---
@@ -1,6 +1,7 @@
---
title: "Add Project Member API Endpoint"
sidebarTitle: "Add Member"
title: 'Add Member'
seo:
title: "Add Project Member API Endpoint - Mem0"
description: "Add a new member to a project with a specified role such as READER or OWNER access level."
openapi: post /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/
---
@@ -1,6 +1,7 @@
---
title: "Get Project Members API Endpoint"
sidebarTitle: "Get Members"
title: 'Get Members'
seo:
title: "Get Project Members API Endpoint - Mem0"
description: "Retrieve a list of all members belonging to a specific project on the Mem0 platform."
openapi: get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/
---
+41 -211
View File
@@ -7,33 +7,10 @@ mode: "wide"
<Tabs>
<Tab title="Python">
<Update label="2026-09-25" description="v2.2.1">
**Bug Fixes:**
- **Core:** `add()` (`Memory` and `AsyncMemory`) no longer reports records the vector store rejected as successful `ADD` events. Only records that were actually inserted are written to history, entity-linked, and returned. If none of the extracted memories could be inserted, `add()` now raises `VectorStoreError` instead of returning memories that were never stored ([#7066](https://github.com/mem0ai/mem0/pull/7066))
- **Core:** Restore the context-manager protocol on `Memory` (`with Memory() as m:`) and `AsyncMemory` (`async with AsyncMemory() as m:`), which closes the instance on exit ([#7354](https://github.com/mem0ai/mem0/pull/7354))
- **LLMs:** The AWS Bedrock Anthropic path now returns the first Converse content block that carries text instead of always reading `content[0]`. Claude reasoning models can emit a `reasoningContent` block before the text block, which previously made the call fail with a `KeyError` ([#6369](https://github.com/mem0ai/mem0/pull/6369))
- **Vector Stores:** Turbopuffer filters now apply every operator (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`). Only `gte` and `lte` were read before, so any other operator was silently dropped and the query returned unfiltered results. An unsupported operator now raises `ValueError` ([#6564](https://github.com/mem0ai/mem0/pull/6564))
- **Vector Stores:** Turbopuffer `search()` scores now respect `distance_metric`. With `euclidean_squared`, the unbounded squared distance maps to `1 / (1 + distance)` instead of `1 - distance`, which went negative and inverted ranking for any distance above 1 ([#6559](https://github.com/mem0ai/mem0/pull/6559))
- **Vector Stores:** S3 Vectors `search()` scores are now metric-aware. With `euclidean`, the distance maps to `1 / (1 + distance)` instead of `max(0, 1 - distance)`, which collapsed most scores to 0 ([#6547](https://github.com/mem0ai/mem0/pull/6547))
</Update>
<Update label="2026-09-23" description="v2.2.0">
<Update label="2026-09-23" description="v2.1.0">
**New Features:**
- **Client:** Add User Profiles to `MemoryClient` and `AsyncMemoryClient`: `get_profile()`, `generate_profile()`, `get_profile_settings()`, `update_profile_settings()`, `sample_profiles()`, and `get_profile_job()`. A profile is a structured, always-current JSON summary of one user, shaped by a JSON Schema you configure per project and filled by an LLM from that user's memories. Generation is asynchronous. Every job POST carries an `Idempotency-Key`; to retry a lost request without starting a second job, pass the same `idempotency_key` on each attempt ([#7340](https://github.com/mem0ai/mem0/pull/7340))
**Bug Fixes:**
- **Vector Stores:** Guard against `None` timestamps in the Valkey vector store's `insert()` and `update()` paths. `created_at` and `updated_at` fields that were present in the payload but set to `None` previously passed the `"created_at" not in payload` / `"updated_at" in payload` checks and raised `TypeError` when `datetime.fromisoformat()` received `None`. Both paths now use `.get()` with a truthiness check so `None` values fall through to the default, matching the Redis provider's behavior ([#6993](https://github.com/mem0ai/mem0/pull/6993))
</Update>
<Update label="2026-09-18" description="v2.1.0">
**Improvements:**
- **Client:** Requests now carry three surface-identity headers so the platform can tell which product made a call. `X-Mem0-Source` names the surface and `X-Application` the host app it runs inside, both set-once so a wrapper that already declared its identity keeps it. `X-Mem0-Client` is append-only and carries `name/version` per layer, outermost first, so a plugin calling this SDK reports the whole chain rather than only the last speaker. `MEM0_SOURCE`, `MEM0_APPLICATION` and `MEM0_CLIENT_STACK` set them from the environment for wrappers that cannot pass options ([#7326](https://github.com/mem0ai/mem0/pull/7326))
- **Client:** The client stack is bounded by dropping whole entries rather than slicing characters, and this SDK's own entry is the reserved one. Truncating the joined string could sever an identifier mid-name and the platform parsed the fragment as a real client ([#7326](https://github.com/mem0ai/mem0/pull/7326))
- **Client:** Add User Profiles to `MemoryClient` and `AsyncMemoryClient`: `get_profile()`, `generate_profile()`, `get_profile_settings()`, `update_profile_settings()`, `sample_profiles()`, and `get_profile_job()`. A profile is a structured, always-current JSON summary of one user, shaped by a JSON Schema you configure per project and filled by an LLM from that user's memories. Generation is asynchronous, and every job POST carries an `Idempotency-Key` so a retry reuses the job instead of paying twice ([#7340](https://github.com/mem0ai/mem0/pull/7340))
</Update>
@@ -204,7 +181,7 @@ mode: "wide"
<Update label="2026-06-24" description="v2.0.8">
**New Features:**
- **Embeddings:** Add native `embed_batch` to five embedders for batched embedding requests: LM Studio, Together, HuggingFace, Vertex AI, and Google GenAI ([#5609](https://github.com/mem0ai/mem0/pull/5609))
- **Embeddings:** Add native `embed_batch` to five embedders: LM Studio, Together, HuggingFace, Vertex AI, and Google GenAI: for batched embedding requests ([#5609](https://github.com/mem0ai/mem0/pull/5609))
**Bug Fixes:**
- **Core:** Guard against malformed `image_url` entries in `parse_vision_messages` to prevent crashes ([#5631](https://github.com/mem0ai/mem0/pull/5631))
@@ -994,7 +971,7 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
**New Features:**
- **OpenMemory:** Added OpenMemory support
- **Neo4j:** Added weights to Neo4j model
- **AWS:** Added support for OpenSearch Serverless
- **AWS:** Added support for Opsearch Serverless
- **Examples:** Added ElizaOS Example
**Improvements:**
@@ -1257,28 +1234,10 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
<Tab title="TypeScript">
<Update label="2026-09-25" description="v3.3.1">
**Bug Fixes:**
- **Config (OSS):** `ConfigManager` no longer injects OpenAI's default `baseURL` and `model` into non-OpenAI LLM providers. The defaults now apply only to `openai` and `openai_structured`, so a DeepSeek, xAI, or other provider config without an explicit `baseURL` or `model` falls back to that provider's own defaults and env vars (`DEEPSEEK_API_BASE`, `XAI_API_BASE`, ...) instead of pointing at OpenAI with an OpenAI model name ([#7350](https://github.com/mem0ai/mem0/pull/7350))
- **Vector Stores:** Turbopuffer filters now apply every operator (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`). Only the range operators were read before, so `eq`, `ne`, `in`, and `nin` were silently dropped. A `"*"` value now matches anything instead of nothing, an array value is treated as `in`, and an unsupported operator throws ([#6578](https://github.com/mem0ai/mem0/pull/6578))
- **Vector Stores:** Turbopuffer `search()` with `euclidean_squared` now maps the unbounded squared distance to `1 / (1 + distance)` instead of `1 - distance`, which went negative and inverted ranking for any distance above 1 ([#6580](https://github.com/mem0ai/mem0/pull/6580))
- **Packaging:** `pg`, `@types/pg`, and `natural` are now optional peer dependencies, and `pg` / `@types/pg` accept caret ranges instead of the exact `8.11.3` / `8.11.0` pins, so installing `mem0ai` no longer pulls in `pg` or conflicts with an app's own `pg` version. The PGVector store now imports `pg` only when it is used, so install it alongside `mem0ai` (`npm install pg`) if you use that store. `@types/jest` moved from peer dependencies to dev dependencies ([#7450](https://github.com/mem0ai/mem0/pull/7450))
</Update>
<Update label="2026-09-23" description="v3.3.0">
<Update label="2026-09-23" description="v3.2.0">
**New Features:**
- **Client:** Add User Profiles to `MemoryClient`: `getProfile()`, `generateProfile()`, `getProfileSettings()`, `updateProfileSettings()`, `sampleProfiles()`, and `getProfileJob()`. A profile is a structured, always-current JSON summary of one user, shaped by a JSON Schema you configure per project and filled by an LLM from that user's memories. Generation is asynchronous. Every job POST carries an `Idempotency-Key`; to retry a lost request without starting a second job, pass the same `idempotencyKey` on each attempt ([#7340](https://github.com/mem0ai/mem0/pull/7340))
</Update>
<Update label="2026-09-18" description="v3.2.0">
**Improvements:**
- **Client:** Requests now carry `X-Mem0-Source`, `X-Application` and `X-Mem0-Client`, matching the Python SDK. The first two are set-once so an outer wrapper keeps its identity; the third is append-only and reports the whole layer chain. Read from `MEM0_SOURCE`, `MEM0_APPLICATION` and `MEM0_CLIENT_STACK` when set ([#7326](https://github.com/mem0ai/mem0/pull/7326))
- **Client:** The SDK version in `X-Mem0-Client` is injected at build time rather than hardcoded, so it cannot go stale at the next release ([#7326](https://github.com/mem0ai/mem0/pull/7326))
- **Client:** Add User Profiles to `MemoryClient`: `getProfile()`, `generateProfile()`, `getProfileSettings()`, `updateProfileSettings()`, `sampleProfiles()`, and `getProfileJob()`. A profile is a structured, always-current JSON summary of one user, shaped by a JSON Schema you configure per project and filled by an LLM from that user's memories. Generation is asynchronous, and every job POST carries an `Idempotency-Key` so a retry reuses the job instead of paying twice ([#7340](https://github.com/mem0ai/mem0/pull/7340))
</Update>
@@ -1919,13 +1878,6 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
<Tab title="CLI">
<Update label="2026-09-18" description="Python v0.2.13 / Node v0.2.14">
**Improvements:**
- **Client:** Requests now carry the three surface-identity headers (`X-Mem0-Source`, `X-Application`, `X-Mem0-Client`) introduced in the Python and TypeScript SDKs, so the platform can attribute calls made through the CLI to the correct surface and version ([#7326](https://github.com/mem0ai/mem0/pull/7326))
</Update>
<Update label="2026-08-24" description="Python v0.2.12 / Node v0.2.13">
**New Features:**
@@ -2138,7 +2090,7 @@ A full-featured command-line interface for Mem0, available in both Python and No
- New Git repository writes use a hash of the remote identity in `agent_id`. Search and explicit shared-memory deletion include both current and legacy repository IDs within the repository's `app_id`. Existing memories are not rewritten. Legacy IDs retain their original ambiguity for matching owner/repository names on different Git hosts.
**Packaging:**
- Claude Code, Cursor, Codex, Kimi, Antigravity, and the portable Python bundle are versioned at `0.3.1`. OpenCode and DeepSeek Harness are `0.3.0`; Pi Agent is `0.3.0`; OpenClaw is `1.1.0`. Each host's changes and upgrade considerations are listed in its tab.
- Claude Code, Cursor, Codex, Kimi, Antigravity, and the portable Python bundle are versioned at `0.3.1`. OpenCode, Pi Agent, and DeepSeek Harness are `0.3.0`; OpenClaw is `1.1.0`. Each host's changes and upgrade considerations are listed in its tab.
- Python and TypeScript CI run their respective runtime suites. Package checks build the installable artifacts, check generated-file consistency, and reject TypeScript output that still imports monorepo source.
[#7203](https://github.com/mem0ai/mem0/pull/7203)
@@ -2433,25 +2385,9 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
<Tab title="Claude Code">
<Update label="2026-09-23" description="Claude Code plugin v0.3.3">
<Update label="Unreleased" description="Sidekick availability">
**Improvements:**
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Sidekick:** Sidekick searches memories when earlier sessions could help, instead of before every answer ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="Claude Code plugin v0.3.2">
**Improvements:**
- **Telemetry:** `PLUGIN_VERSION` bumped to `0.3.2`. The `mem0-plugin/<version>` wire header and `plugin_version` telemetry field now reflect the fixes from #7322 through #7358 ([#7373](https://github.com/mem0ai/mem0/pull/7373))
- **Telemetry:** Events are no longer delivered twice, no longer lose parked events on flush, and now attribute each event to the plugin that produced it ([#7323](https://github.com/mem0ai/mem0/pull/7323), [#7324](https://github.com/mem0ai/mem0/pull/7324), [#7358](https://github.com/mem0ai/mem0/pull/7358))
**Changes:**
- **Sidekick:** Sidekick is now available only in Claude Code, with Sonnet, worktree isolation, and parent memories.
Sidekick is now available only in Claude Code, with Sonnet, worktree isolation, and parent memories.
</Update>
@@ -2474,24 +2410,9 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
<Tab title="Cursor">
<Update label="2026-09-23" description="Cursor plugin v0.3.3">
<Update label="Unreleased" description="Sidekick availability">
**Improvements:**
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="Cursor plugin v0.3.2">
**Improvements:**
- **Telemetry:** `PLUGIN_VERSION` bumped to `0.3.2`. The `mem0-plugin/<version>` wire header and `plugin_version` telemetry field now reflect the fixes from #7322 through #7358 ([#7373](https://github.com/mem0ai/mem0/pull/7373))
- **Telemetry:** Events are no longer delivered twice, no longer lose parked events on flush, and now attribute each event to the plugin that produced it ([#7323](https://github.com/mem0ai/mem0/pull/7323), [#7324](https://github.com/mem0ai/mem0/pull/7324), [#7358](https://github.com/mem0ai/mem0/pull/7358))
**Changes:**
- **Sidekick:** Removes Sidekick and its start/stop hooks. Memory capture, search, and six skills remain available.
Removes Sidekick and its start/stop hooks. Memory capture, search, and six skills remain available.
</Update>
@@ -2514,24 +2435,9 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
<Tab title="Codex">
<Update label="2026-09-23" description="Codex plugin v0.3.3">
<Update label="Unreleased" description="Sidekick availability">
**Improvements:**
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="Codex plugin v0.3.2">
**Improvements:**
- **Telemetry:** `PLUGIN_VERSION` bumped to `0.3.2`. The `mem0-plugin/<version>` wire header and `plugin_version` telemetry field now reflect the fixes from #7322 through #7358 ([#7373](https://github.com/mem0ai/mem0/pull/7373))
- **Telemetry:** Events are no longer delivered twice, no longer lose parked events on flush, and now attribute each event to the plugin that produced it ([#7323](https://github.com/mem0ai/mem0/pull/7323), [#7324](https://github.com/mem0ai/mem0/pull/7324), [#7358](https://github.com/mem0ai/mem0/pull/7358))
**Changes:**
- **Sidekick:** Renames shared tracking to use subagent terminology. Native subagent memory support remains available.
Renames shared tracking to use subagent terminology. Native subagent memory support remains available.
</Update>
@@ -2551,25 +2457,32 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
</Tab>
<Tab title="Agent Plugins v1">
<Update label="Unreleased" description="Sidekick availability">
Sidekick is available only in Claude Code, not in the portable package.
</Update>
<Update label="2026-09-08" description="Portable Mem0 plugin v0.3.1">
**Added:**
- One portable package at `integrations/mem0-agent-plugin/`, using the Agent Plugins 1.0.0 root `plugin.json`, `mcp.json`, and fixed `skills/` locations.
- Ships a local, read-only `search_memories` server and the six shared memory skills. Uses `PLUGIN_ROOT` for bundled files and `PLUGIN_DATA` for persistent plugin state; all package files remain inside the installable directory.
**Packaging:**
- Generated from the shared Python runtime and skill templates. Builds validate the manifest, MCP configuration, skills, and generated-file consistency.
- Host lifecycle hooks and native Sidekick declarations remain in the native plugin packages; the portable package does not provide automatic lifecycle capture or host-specific subagent isolation. Its bundled remember skill cannot persist a new memory on its own because the portable package has no capture hooks or write tool.
[#7203](https://github.com/mem0ai/mem0/pull/7203)
</Update>
</Tab>
<Tab title="OpenCode">
<Update label="2026-09-23" description="OpenCode plugin v0.4.1">
**Improvements:**
- **Search:** The `search_memories` tool description no longer asks the agent to search proactively or to run several searches for multi-part questions. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, the same wording as the other coding-agent plugins ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Session context:** Removed two system-context lines that told the agent to run 2 parallel searches before responding and 2-4 parallel searches for non-trivial tasks ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Skills:** `/mem0-search` and `/mem0-context-loader` make one `search_memories` call instead of 2 and 2-4 parallel calls. `/mem0-context-loader` now uses the search skill description from the other coding-agent plugins instead of asking to load at every new task or context switch ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="OpenCode plugin v0.4.0">
**Changes:**
- **Telemetry:** The PostHog `source` tag changed from the literal `"plugin"` to `OPENCODE_PLUGIN`, and `project_hash` is now salted. Saved PostHog insights filtering on `source = "plugin"` will stop matching new events; historical data is unaffected ([#7322](https://github.com/mem0ai/mem0/pull/7322))
- **Config:** A new `keyFingerprint` key appears in the install-count deduplication logic; installs are now counted once per key rather than on every activation ([#7325](https://github.com/mem0ai/mem0/pull/7325))
</Update>
<Update label="2026-09-08" description="OpenCode plugin v0.3.0">
**Changed:**
@@ -2668,24 +2581,9 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
<Tab title="Antigravity">
<Update label="2026-09-23" description="Antigravity plugin v0.3.3">
<Update label="Unreleased" description="Sidekick availability">
**Improvements:**
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="Antigravity plugin v0.3.2">
**Improvements:**
- **Telemetry:** `PLUGIN_VERSION` bumped to `0.3.2`. The `mem0-plugin/<version>` wire header and `plugin_version` telemetry field now reflect the fixes from #7322 through #7358 ([#7373](https://github.com/mem0ai/mem0/pull/7373))
- **Telemetry:** Events are no longer delivered twice, no longer lose parked events on flush, and now attribute each event to the plugin that produced it ([#7323](https://github.com/mem0ai/mem0/pull/7323), [#7324](https://github.com/mem0ai/mem0/pull/7324), [#7358](https://github.com/mem0ai/mem0/pull/7358))
**Changes:**
- **Sidekick:** Removes Sidekick. Memory capture, search, and six skills remain available.
Removes Sidekick. Memory capture, search, and six skills remain available.
</Update>
@@ -2781,24 +2679,9 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="Kimi">
<Update label="2026-09-23" description="Kimi Code plugin v0.3.3">
<Update label="Unreleased" description="Sidekick availability">
**Improvements:**
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="Kimi Code plugin v0.3.2">
**Improvements:**
- **Telemetry:** `PLUGIN_VERSION` bumped to `0.3.2`. The `mem0-plugin/<version>` wire header and `plugin_version` telemetry field now reflect the fixes from #7322 through #7358 ([#7373](https://github.com/mem0ai/mem0/pull/7373))
- **Telemetry:** Events are no longer delivered twice, no longer lose parked events on flush, and now attribute each event to the plugin that produced it ([#7323](https://github.com/mem0ai/mem0/pull/7323), [#7324](https://github.com/mem0ai/mem0/pull/7324), [#7358](https://github.com/mem0ai/mem0/pull/7358))
**Changes:**
- **Sidekick:** Removes Sidekick and its start/stop hooks. Memory capture, recall, and six skills remain available.
Removes Sidekick and its start/stop hooks. Memory capture, recall, and six skills remain available.
</Update>
@@ -2836,21 +2719,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="OpenClaw">
<Update label="2026-09-23" description="openclaw-mem0 v1.2.1">
**Improvements:**
- **Search:** The `memory_search` tool description no longer asks the agent to search proactively or to run several searches for multi-part questions. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help. Recall strategies (`smart`, `always`, `manual`) are unchanged ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="openclaw-mem0 v1.2.0">
**Changes:**
- **Config:** Added `keyFingerprint` to the config schema for install-count deduplication; installs are now counted once per key rather than on every activation ([#7325](https://github.com/mem0ai/mem0/pull/7325))
- **Telemetry:** Events are no longer delivered twice, and the `plugin_version` field now reflects the plugin that produced the event ([#7323](https://github.com/mem0ai/mem0/pull/7323), [#7324](https://github.com/mem0ai/mem0/pull/7324), [#7358](https://github.com/mem0ai/mem0/pull/7358))
</Update>
<Update label="2026-09-08" description="openclaw-mem0 v1.1.0">
**Changed:**
@@ -3139,22 +3007,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="Pi Agent">
<Update label="2026-09-23" description="Pi Agent plugin v0.3.2">
**Improvements:**
- **Search:** The memory policy, `mem0_memory` tool description, and prompt guidelines no longer ask the agent to search before answering anything that may depend on earlier context or to run several searches per question. They now ask for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Skills:** `context-loader` makes one search instead of 2-4 parallel searches, and uses the search skill description from the other coding-agent plugins ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Automatic recall:** Injected memories are introduced as "Mem0 found these relevant memories from earlier work in this repository:", the same heading as the Python plugins. The old heading called them a shallow first pass and told the agent to search again ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="Pi Agent plugin v0.3.1">
**Improvements:**
- **Telemetry:** Events are no longer delivered twice, no longer lose parked events, and now attribute each event to the plugin that produced it. The `plugin_version` wire field reflects the fixed release ([#7323](https://github.com/mem0ai/mem0/pull/7323), [#7324](https://github.com/mem0ai/mem0/pull/7324), [#7358](https://github.com/mem0ai/mem0/pull/7358))
</Update>
<Update label="2026-09-08" description="Pi Agent plugin v0.3.0">
**Changed:**
@@ -3268,21 +3120,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="DeepSeek Harness">
<Update label="2026-09-23" description="deepseek-plugin v0.3.2">
**Improvements:**
- **Search:** The `search_memory` tool description no longer asks the agent to search proactively before answering anything that may depend on earlier context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help ([#7420](https://github.com/mem0ai/mem0/pull/7420))
- **Automatic recall:** Injected memories are introduced as "Mem0 found these relevant memories from earlier work:". The old heading called them a shallow first pass and told the agent to search again with `mem0_memory`, a tool DeepSeek does not have ([#7420](https://github.com/mem0ai/mem0/pull/7420))
</Update>
<Update label="2026-09-18" description="deepseek-plugin v0.3.1">
**Improvements:**
- **Telemetry:** Rebuild with the fixed shared telemetry core from `agent-plugin-core`. Events are no longer delivered twice, no longer lose parked events on flush, and now attribute each event to the plugin that produced it ([#7323](https://github.com/mem0ai/mem0/pull/7323), [#7324](https://github.com/mem0ai/mem0/pull/7324), [#7358](https://github.com/mem0ai/mem0/pull/7358))
</Update>
<Update label="2026-09-08" description="deepseek-plugin v0.3.0">
**Added:**
@@ -3327,13 +3164,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="Vercel AI SDK">
<Update label="2026-09-18" description="Vercel AI SDK v3.0.3">
**Improvements:**
- **Client:** Inherits the three surface-identity headers (`X-Mem0-Source`, `X-Application`, `X-Mem0-Client`) from the TypeScript SDK bump, so platform calls made through the Vercel AI SDK provider are now correctly attributed ([#7326](https://github.com/mem0ai/mem0/pull/7326))
</Update>
<Update label="2026-08-24" description="Vercel AI SDK v3.0.2">
**Security:**
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Embedder Configuration Reference"
sidebarTitle: "Configurations"
title: Configurations
seo:
title: "Embedder Configuration Reference - Mem0"
description: "Reference for embedder configuration options in Mem0, including provider selection and model settings."
---
@@ -1,6 +1,7 @@
---
title: "AWS Bedrock as Embedding Provider"
sidebarTitle: "AWS Bedrock"
title: AWS Bedrock
seo:
title: "AWS Bedrock as Embedding Provider - Mem0"
description: "Configure AWS Bedrock as an embedding provider in Mem0 with IAM credentials and boto3 authentication."
---
@@ -1,6 +1,7 @@
---
title: "Azure OpenAI as Embedding Provider"
sidebarTitle: "Azure OpenAI"
title: Azure OpenAI
seo:
title: "Azure OpenAI as Embedding Provider - Mem0"
description: "Configure Azure OpenAI as an embedding provider in Mem0 with API key, deployment, and endpoint settings."
---
@@ -1,6 +1,7 @@
---
title: "Google AI as Embedding Provider"
sidebarTitle: "Google AI"
title: Google AI
seo:
title: "Google AI as Embedding Provider - Mem0"
description: "Configure Google AI as an embedding provider in Mem0 using Gemini models and the GOOGLE_API_KEY variable."
---
@@ -1,6 +1,7 @@
---
title: "LangChain as Embedding Provider"
sidebarTitle: "LangChain"
title: LangChain
seo:
title: "LangChain as Embedding Provider - Mem0"
description: "Use LangChain as an embedding provider in Mem0 to access a wide range of models through a unified interface."
---
@@ -1,6 +1,7 @@
---
title: "LM Studio as Embedding Provider"
sidebarTitle: "LM Studio"
title: "LM Studio"
seo:
title: "LM Studio as Embedding Provider - Mem0"
description: "Configure LM Studio as an embedding provider in Mem0 for local embedding generation with models like nomic-embed-text."
---
You can use embedding models from LM Studio to run Mem0 locally.
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Ollama as Embedding Provider"
sidebarTitle: "Ollama"
title: "Ollama"
seo:
title: "Ollama as Embedding Provider - Mem0"
description: "Configure Ollama as an embedding provider in Mem0 to generate embeddings locally using open-source models."
---
You can use embedding models from Ollama to run Mem0 locally.
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "OpenAI as Embedding Provider"
sidebarTitle: "OpenAI"
title: OpenAI
seo:
title: "OpenAI as Embedding Provider - Mem0"
description: "Configure OpenAI as an embedding provider in Mem0 using models like text-embedding-3-large for vector generation."
---
@@ -1,6 +1,7 @@
---
title: "Together AI as Embedding Provider"
sidebarTitle: "Together"
title: Together
seo:
title: "Together AI as Embedding Provider - Mem0"
description: "Configure Together AI as an embedding provider in Mem0 with support for 1024-dimensional embedding models."
---
@@ -11,7 +12,7 @@ To use Together embedding models, set the `TOGETHER_API_KEY` environment variabl
<Note> The `embedding_model_dims` parameter for `vector_store` should be set to `1024` for Together embedder. </Note>
<Warning>
**Breaking default change.** The default Together embedding model is now `intfloat/multilingual-e5-large-instruct` (**1024-dim**), replacing the previous default `togethercomputer/m2-bert-80M-8k-retrieval` (**768-dim**). If you created a self-hosted vector store with the old default, its collection is 768-dim and will reject the new 1024-dim vectors — **recreate/reindex the collection at 1024 dimensions** after upgrading. To defer the change, pin the previous values explicitly (`model="togethercomputer/m2-bert-80M-8k-retrieval"`, `embedding_dims=768`) — note Together no longer lists this model among its recommended embeddings, so reindexing at 1024 is the durable path.
**Breaking default change.** The default Together embedding model is now `intfloat/multilingual-e5-large-instruct` (**1024-dim**), replacing the previous default `togethercomputer/m2-bert-80M-8k-retrieval` (**768-dim**). If you created a self-hosted vector store with the old default, its collection is 768-dim and will reject the new 1024-dim vectors **recreate/reindex the collection at 1024 dimensions** after upgrading. To defer the change, pin the previous values explicitly (`model="togethercomputer/m2-bert-80M-8k-retrieval"`, `embedding_dims=768`) note Together no longer lists this model among its recommended embeddings, so reindexing at 1024 is the durable path.
</Warning>
<CodeGroup>
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Embedding Providers Overview"
sidebarTitle: Overview
title: Overview
seo:
title: "Embedding Providers Overview - Mem0"
description: "Overview of all supported embedding model providers in Mem0, including OpenAI, Azure, Ollama, and more."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "LLM Configuration Reference"
sidebarTitle: "Configurations"
title: Configurations
seo:
title: "LLM Configuration Reference - Mem0"
description: "Reference for LLM configuration options in Mem0 for Python and TypeScript, including value precedence rules."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "AWS Bedrock as LLM Provider"
sidebarTitle: "AWS Bedrock"
title: AWS Bedrock
seo:
title: "AWS Bedrock as LLM Provider - Mem0"
description: "Configure AWS Bedrock as an LLM provider in Mem0 with IAM authentication and Claude model support."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Azure OpenAI as LLM Provider"
sidebarTitle: "Azure OpenAI"
title: Azure OpenAI
seo:
title: "Azure OpenAI as LLM Provider - Mem0"
description: "Configure Azure OpenAI as an LLM provider in Mem0 with Azure Identity authentication and deployment settings."
---
+1 -1
View File
@@ -3,7 +3,7 @@ title: DeepSeek
description: "Configure DeepSeek as an LLM provider in Mem0 with API key setup and optional custom endpoint configuration."
---
To use DeepSeek LLM models, you have to set the `DEEPSEEK_API_KEY` environment variable. You can also optionally set `DEEPSEEK_API_BASE` if you need to use a different API endpoint (defaults to `https://api.deepseek.com`).
To use DeepSeek LLM models, you have to set the `DEEPSEEK_API_KEY` environment variable. You can also optionally set `DEEPSEEK_API_BASE` if you need to use a different API endpoint (defaults to "https://api.deepseek.com").
## Usage
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Google AI as LLM Provider"
sidebarTitle: "Google AI"
title: Google AI
seo:
title: "Google AI as LLM Provider - Mem0"
description: "Configure Google Gemini as an LLM provider in Mem0 using the google.genai SDK and GOOGLE_API_KEY variable."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "LangChain as LLM Provider"
sidebarTitle: "LangChain"
title: LangChain
seo:
title: "LangChain as LLM Provider - Mem0"
description: "Use LangChain as an LLM provider in Mem0 to integrate with various chat models through a unified interface."
---
+4 -3
View File
@@ -1,6 +1,7 @@
---
title: "LM Studio as LLM Provider"
sidebarTitle: "LM Studio"
title: LM Studio
seo:
title: "LM Studio as LLM Provider - Mem0"
description: "Configure LM Studio as an LLM provider in Mem0 for running local language models via an OpenAI-compatible API."
---
@@ -77,7 +78,7 @@ m.add(messages, user_id="alice123", metadata={"category": "movies"})
To use LM Studio, you need to:
1. Download and install [LM Studio](https://lmstudio.ai/)
2. Start a local server from the "Server" tab
3. Set the appropriate `lmstudio_base_url` in your configuration (default is usually `http://localhost:1234/v1`)
3. Set the appropriate `lmstudio_base_url` in your configuration (default is usually http://localhost:1234/v1)
</Note>
## Config
+1 -1
View File
@@ -3,7 +3,7 @@ title: MiniMax
description: "Configure MiniMax as an LLM provider in Mem0 with API key setup and optional custom endpoint configuration."
---
To use MiniMax LLM models, you have to set the `MINIMAX_API_KEY` environment variable. You can also optionally set `MINIMAX_API_BASE` if you need to use a different API endpoint (defaults to `https://api.minimax.io/v1`).
To use MiniMax LLM models, you have to set the `MINIMAX_API_KEY` environment variable. You can also optionally set `MINIMAX_API_BASE` if you need to use a different API endpoint (defaults to "https://api.minimax.io/v1").
## Usage
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Ollama as LLM Provider"
sidebarTitle: "Ollama"
title: Ollama
seo:
title: "Ollama as LLM Provider - Mem0"
description: "Configure Ollama as an LLM provider in Mem0 for running local language models with tool-calling support."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "OpenAI as LLM Provider"
sidebarTitle: "OpenAI"
title: OpenAI
seo:
title: "OpenAI as LLM Provider - Mem0"
description: "Configure OpenAI as an LLM provider in Mem0 with support for GPT models and Openrouter compatibility."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Together AI as LLM Provider"
sidebarTitle: "Together"
title: Together
seo:
title: "Together AI as LLM Provider - Mem0"
description: "Configure Together AI as an LLM provider in Mem0 with API key setup and optional custom endpoint configuration."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "xAI Grok as LLM Provider"
sidebarTitle: "xAI"
title: xAI
seo:
title: "xAI Grok as LLM Provider - Mem0"
description: "Configure xAI Grok models as an LLM provider in Mem0 with API key setup and usage examples."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "LLM Providers Overview"
sidebarTitle: Overview
title: Overview
seo:
title: "LLM Providers Overview - Mem0"
description: "Overview of all supported LLM providers in Mem0, including OpenAI, Anthropic, Groq, Ollama, and more."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Reranker Providers Overview"
sidebarTitle: "Overview"
title: Overview
seo:
title: "Reranker Providers Overview - Mem0"
description: 'Pick the right reranker path to boost Mem0 search relevance.'
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Vector Store Configuration Reference"
sidebarTitle: "Configurations"
title: Configurations
seo:
title: "Vector Store Configuration Reference - Mem0"
description: "Reference for vector database configuration options in Mem0, including provider selection and connection settings."
---
+1 -1
View File
@@ -95,7 +95,7 @@ Uses the identity from Azure PowerShell (`Connect-AzAccount`).
7. **Azure Developer CLI Credential:**
Uses the session from Azure Developer CLI (`azd auth login`).
<Note> If an API key is provided, it will be used for authentication over an Azure Identity </Note>
<Note> If an API is provided, it will be used for authentication over an Azure Identity </Note>
To enable Role-Based Access Control (RBAC) for Azure AI Search, follow these steps:
1. In the Azure Portal, navigate to your **Azure AI Search** service.
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "LangChain as Vector Store Provider"
sidebarTitle: "LangChain"
title: LangChain
seo:
title: "LangChain as Vector Store Provider - Mem0"
description: "Use LangChain as a unified vector store provider in Mem0 to access multiple vector databases through one interface."
---
@@ -5,12 +5,6 @@ description: "Use pgvector as a vector store in Mem0 for PostgreSQL-based vector
[pgvector](https://github.com/pgvector/pgvector) is an open-source vector similarity search extension for Postgres. After connecting to Postgres, run `CREATE EXTENSION IF NOT EXISTS vector;` to create the vector extension.
The TypeScript SDK loads the `pg` driver only when you use this store, so install it alongside `mem0ai`:
```bash
npm install pg
```
### Usage
<CodeGroup>
@@ -94,7 +94,6 @@ Here are the parameters available for configuring Pinecone:
| `hybrid_search` | Whether to enable hybrid search | `False` |
| `metric` | Distance metric for vector similarity | `"cosine"` |
| `batch_size` | Batch size for operations | `100` |
| `extra_params` | Additional keyword arguments passed to the `Pinecone` client constructor. Ignored when `client` is supplied. | `None` |
| `namespace` | Namespace for the collection, useful for multi-tenancy. | `None` |
</Tab>
<Tab title="TypeScript">
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Vector Store Providers Overview"
sidebarTitle: "Overview"
title: Overview
seo:
title: "Vector Store Providers Overview - Mem0"
description: "Overview of all supported vector databases in Mem0, including Qdrant, Chroma, PGVector, Pinecone, Oracle, and more."
---
@@ -30,7 +30,7 @@ pip install google-adk mem0ai python-dotenv
## Code Breakdown
Let's get started and understand the different components required in building a healthcare assistant powered by memory.
Let's get started and understand the different components required in building a healthcare assistant powered by memory
```python
# Import dependencies
-388
View File
@@ -1,388 +0,0 @@
---
title: Build a Company Brain with Mem0 Platform and Supabase
description: "Build a shared company brain using Mem0 Platform as the managed memory layer, Supabase as your system of record, and the Mem0 MCP server."
---
<Info icon="server">
**Uses:** Mem0 **Platform** (`MemoryClient`) · **System of record:** Supabase (Postgres + Auth) · **Access layer:** the hosted Mem0 MCP server. **You'll build:** a company brain your whole org (and every agent) writes to and queries, ending with a new-hire onboarding demo.
</Info>
Companies lose knowledge constantly: why you picked Postgres over Mongo, who owns billing, the deploy rule only one engineer remembers. A **company brain** captures this and answers questions about it, for every employee and every agent, and keeps it after people leave.
We'll build one on **Mem0 Platform** (the managed memory layer, so there's no vector DB to run) with **Supabase as the system of record** (where your employees, teams, and source documents actually live) and the **Mem0 MCP server** as the wire that lets Claude Code, Cursor, or a Slack bot all reach the same brain.
<Note>
**How Platform and Supabase divide the work.** Mem0 Platform manages storage and extraction server-side, you do **not** point it at your own database. Supabase is your app's source of truth and identity provider; we *ingest* knowledge from Supabase into the brain and use Supabase Auth to decide who's asking. (If you want to self-host the vector store instead, that's the OSS path, see the [Supabase vector store reference](/components/vectordbs/dbs/supabase).)
</Note>
## Architecture
```mermaid
flowchart LR
subgraph SB["Supabase: system of record"]
K[(knowledge / employees / teams)]
AU[Auth · who is asking]
end
subgraph M0["Mem0 Platform: the brain"]
B[(managed memory)]
end
K -->|ingest| B
AU -->|maps to scope| B
CC[Claude Code] --> MCP[Mem0 MCP server]
CU[Cursor] --> MCP
SL[Slack bot] --> MCP
MCP --> B
```
Memory splits by entity. An individual is a **`user_id`** (their Supabase Auth id). Shared knowledge lives on an **`agent_id`**: the company-wide brain is `org:acme`, and each team is its own agent, e.g. `team:payments`. A person's own facts route to their `user_id`; company and team facts route to the agent. This split is what lets one search return "my" context alongside the shared org knowledge.
## Prerequisites
- **Python 3.9+**
- A **Mem0 Platform API key**, [app.mem0.ai/dashboard/api-keys](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cookbook-company-brain). (Platform runs extraction and embeddings for you, so there's no OpenAI key to manage.)
- A **Supabase** project, [supabase.com](https://supabase.com)
About 20 minutes.
---
## Step 1: Get your Mem0 Platform API key
Sign in at [app.mem0.ai](https://app.mem0.ai) and copy a key from **Dashboard → API Keys**. The key is scoped to your org and project; Mem0 resolves both server-side, so you never pass IDs by hand.
## Step 2: Create the Supabase system of record
In the Supabase **SQL editor**, create the tables your company already thinks in: people, teams, and a `knowledge` table the brain will ingest from. Identity reuses Supabase Auth's built-in `auth.users`.
```sql
-- Employees extend Supabase Auth's users; identity is auth.users.id (uuid)
create table public.employees (
id uuid primary key references auth.users (id) on delete cascade,
name text not null,
team text not null
);
-- The company knowledge the brain ingests. `scope` decides who can recall it.
create table public.knowledge (
id bigint generated always as identity primary key,
scope text not null, -- the shared agent this belongs to: 'org:acme' | 'team:payments'
content text not null,
author uuid references auth.users (id), -- who recorded it (their user_id); null for org seed data
created_at timestamptz default now(),
mem0_synced_at timestamptz -- null until ingested into the brain
);
create index on public.knowledge (mem0_synced_at, created_at);
-- Seed a little company knowledge to ingest.
insert into public.knowledge (scope, content) values
('org:acme', 'We chose Postgres over MongoDB for the core product for strong transactional guarantees and relational joins.'),
('org:acme', 'All production deploys go out Tuesday and Thursday; never on Fridays.'),
('org:acme', 'Customer data must stay in the EU region for GDPR compliance.'),
('org:acme', 'Billing is owned by the Payments team, and Alice is the Payments tech lead.'),
('team:payments', 'Stripe is our processor; webhooks are verified with PAYMENTS_WEBHOOK_SECRET.');
```
Grab your project URL and **service-role** key from **Settings → API** (the ingestion job runs server-side and needs to read every scope).
## Step 3: Project setup
```bash
mkdir company-brain && cd company-brain
pip install "mem0ai>=2.0.17" supabase requests # 2.0.17+ for agent_custom_instructions
```
```bash
export MEM0_API_KEY="m0-..."
export SUPABASE_URL="https://<project-ref>.supabase.co"
export SUPABASE_SERVICE_KEY="<service-role-key>"
```
## Step 4: Configure the brain
Create **`brain.py`**. This constructs the Platform client and teaches it what to remember. The key is the **two** instruction sets: `custom_instructions` governs a person's own (`user_id`) memories, and `agent_custom_instructions` governs shared (`agent_id`) memories, phrased in the third person so company facts read "The company…", not "The user's organization…". `custom_categories` files each memory under a useful label.
```python
# brain.py
import os
from mem0 import MemoryClient
client = MemoryClient(api_key=os.environ["MEM0_API_KEY"])
# Steer extraction (project-wide). Runs server-side; no LLM key needed here.
client.project.update(
# Governs a person's OWN memories (user_id).
custom_instructions=(
"Extract the individual's own durable preferences, context, and how they work. "
"Ignore greetings and one-off chatter."
),
# Governs SHARED memories (agent_id); write them in the third person.
agent_custom_instructions=(
"Extract durable company/team knowledge in the third person "
"(\"The company...\", \"The team...\"): decisions and their rationale, ownership "
"(who owns what), processes, policies, tooling choices, and gotchas. "
"Ignore greetings, scheduling, and one-off chatter."
),
custom_categories=[
{"decision": "Architectural or product decisions and why they were made"},
{"ownership": "Who owns a system, service, or process"},
{"policy": "Compliance, security, and process rules"},
{"tooling": "Tools, services, and how they're configured"},
],
)
# Scopes. A person is a user_id; shared brains are agent_ids.
COMPANY = "org:acme" # agent_id: company-wide shared brain
def team(name): return f"team:{name}" # agent_id: a team's shared brain
def person(uid): return uid # user_id: an individual (Supabase auth id)
```
Run it once to apply the project settings:
```bash
python -c "import brain; print('brain configured')"
```
## Step 5: Ingest company knowledge from Supabase
This is where Supabase and the brain connect. Create **`ingest.py`**: read un-synced rows from `knowledge`, add each to the Platform brain under its scope, then mark it synced. Platform `add()` is **asynchronous**, it returns an `event_id` you can poll, so we include a small `wait_for` helper.
```python
# ingest.py
import os, time, requests
from supabase import create_client
from brain import client, person
sb = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_KEY"])
MEM0_HEADERS = {"Authorization": f"Token {os.environ['MEM0_API_KEY']}"}
def wait_for(event_id, timeout=30):
"""Platform extraction is async; poll the event until it settles."""
for _ in range(timeout):
r = requests.get(f"https://api.mem0.ai/v1/event/{event_id}/", headers=MEM0_HEADERS).json()
if r.get("status") in ("SUCCEEDED", "FAILED"):
return r["status"]
time.sleep(1)
return "TIMEOUT"
# 1. Read knowledge that hasn't been ingested yet
rows = sb.table("knowledge").select("*").is_("mem0_synced_at", "null").execute().data
for row in rows:
# 2. Add it. agent_id = the shared scope (org/team); user_id = who recorded it.
# Mem0 routes shared facts to the agent and personal facts to the individual,
# so pass both when there's an author.
add_kwargs = {
"agent_id": row["scope"],
"metadata": {"source": "supabase", "knowledge_id": row["id"]},
}
if row["author"]:
add_kwargs["user_id"] = person(row["author"])
res = client.add([{"role": "user", "content": row["content"]}], **add_kwargs)
# 3. Platform returns an event_id; wait for extraction to finish
event_id = res.get("event_id") if isinstance(res, dict) else None
if event_id:
wait_for(event_id)
# 4. Mark the row synced so we never double-ingest
sb.table("knowledge").update({"mem0_synced_at": "now()"}).eq("id", row["id"]).execute()
print(f"Ingested {len(rows)} knowledge items into the company brain.")
```
```bash
python ingest.py
```
```text
Ingested 5 knowledge items into the company brain.
```
Re-running is safe, `mem0_synced_at` gates it, so a nightly cron can keep the brain in step with Supabase.
## Step 6: Ask the brain
Create **`ask.py`**. It searches everything relevant to the asker: their own (`user_id`) memories **plus** the shared company and team (`agent_id`) memories. This has to be an **`OR`**, each memory row belongs to exactly one entity, so a flat filter or an `AND` of a `user_id` and an `agent_id` matches nothing.
```python
# ask.py
import sys
from brain import client, COMPANY, team, person
def ask(question: str, uid: str | None = None, user_team: str | None = None) -> str:
scopes = [{"agent_id": COMPANY}] # company-wide brain
if user_team:
scopes.append({"agent_id": team(user_team)}) # the asker's team
if uid:
scopes.append({"user_id": person(uid)}) # the asker's own memories
hits = client.search(
query=question,
filters={"OR": scopes}, # OR, never AND (one FK per memory row)
top_k=5,
rerank=True,
)
return "\n".join(f"- {h['memory']}" for h in hits.get("results", hits))
if __name__ == "__main__":
print(ask(" ".join(sys.argv[1:]) or "When can we deploy?"))
```
```bash
python ask.py "Why did we pick Postgres, and can I deploy on Friday?"
```
```text
- The company chose Postgres over MongoDB for strong transactional guarantees and relational joins
- The company's production deploys go out Tuesday and Thursday, never on Fridays
```
Search returns every relevant memory, so a question resolves across separate facts, here it pulls both the owning team and the person:
```bash
python ask.py "Who should I talk to about billing?"
```
```text
- Billing is owned by the Payments team
- Alice is the Payments tech lead
```
## Step 7: Sharper retrieval
Platform search is hybrid (semantic + keyword) and filterable. Combine a keyword pass with a category filter to answer precise questions:
```python
client.search(
query="webhook signing secret",
filters={"agent_id": "team:payments", "categories": {"in": ["tooling"]}},
keyword_search=True, # hybrid keyword + semantic
rerank=True,
threshold=0.3,
)
```
Filters use keyword operators (`in`, `gte`, `contains`, …) and AND/OR/NOT, so you can scope by date, category, or metadata, for example the company's policies added this quarter:
```python
client.search(
query="compliance rules",
filters={"AND": [
{"agent_id": "org:acme"},
{"categories": {"in": ["policy"]}},
{"created_at": {"gte": "2026-01-01"}},
]},
)
```
## Step 8: Expose the brain to every agent (MCP)
A brain only your script can reach isn't a company brain. Mem0's **hosted MCP server** lets any agent (Claude Code, Cursor, a Slack bot) query and contribute to the *same* brain. The endpoint is `https://mcp.mem0.ai/mcp`, and the supported way to connect is the `mcp-add` helper, which registers the server and runs Mem0's OAuth login so no key ever lands in a config file.
<Tabs>
<Tab title="Claude Code / Cursor">
```bash
npx mcp-add --url "https://mcp.mem0.ai/mcp" --clients "claude code,cursor"
```
Complete the browser login on first connect. Now the agent has the brain's memory tools (`add_memory`, `search_memories`, and more) available in-editor.
</Tab>
<Tab title="Manual (.mcp.json)">
```json
{
"mcpServers": {
"mem0": { "url": "https://mcp.mem0.ai/mcp" }
}
}
```
Auth happens via Mem0's OAuth flow on first use, don't paste a static token into the file (the hosted gateway may reject a raw `Token` header).
</Tab>
<Tab title="Slack bot">
```python
# A Slack bot is just another MCP client. Point its MCP layer at the same URL,
# authenticate via Mem0's OAuth flow, and pass the company scope on each call.
await mcp.call_tool("search_memories", {
"query": user_message,
"agent_id": "org:acme",
})
```
</Tab>
</Tabs>
With this, an engineer asks the brain from their editor and a teammate asks it from Slack, one shared memory behind both.
## Step 9: Onboard a new hire (the payoff)
This is what a company brain is *for*. Dana joins, and her identity comes from **Supabase Auth**, which maps straight to her Mem0 `user_id`. She asks the questions every new hire asks and gets real answers on day one, drawn from the shared company (and her team's) brain, plus anything she's told it herself.
```python
# onboarding.py
from brain import client, person
from ask import ask
# In a real app these come from sb.auth.get_user(jwt) and the employees table.
dana_uid, dana_team = "8f3c...-dana", "payments"
# Dana also tells the brain how *she* works. This is personal, so it goes to her
# user_id, not the shared agent, and stays scoped to her.
client.add(
[{"role": "user", "content": "I prefer early returns over nested ifs, and I review PRs in the morning."}],
user_id=person(dana_uid),
)
for q in [
"Who owns billing and who do I talk to?", # company (agent) knowledge
"When are deploys, and are there hard rules?",
"How do I like to write code?", # Dana's own (user) knowledge
]:
print(f"Q: {q}\nA: {ask(q, uid=dana_uid, user_team=dana_team)}\n")
```
```text
Q: Who owns billing and who do I talk to?
A: - Billing is owned by the Payments team; Alice is the Payments tech lead
Q: When are deploys, and are there hard rules?
A: - The company's production deploys go out Tuesday and Thursday, never on Fridays
Q: How do I like to write code?
A: - User prefers early returns over nested ifs
```
The same `ask()` blends the shared company facts with Dana's own preference, because the `OR` filter spans both her `user_id` and the org and team `agent_id`s.
Dana onboarded herself by asking, drawing on the shared brain the rest of the team had been filling.
## Production notes
<Warning>
**`user_id` vs `agent_id`.** An individual is a `user_id`; shared brains (company, team) are `agent_id`s. Keeping them separate is what gives you the third-person "The company…" framing and lets a person's own context sit alongside org knowledge. Put a secret like a webhook key on a **team** agent, never the company agent, or everyone can recall it, and mirror the boundary in Supabase with a Row Level Security policy on `knowledge`.
</Warning>
<Warning>
**Search must `OR` the scopes.** A memory row belongs to exactly one entity, so `filters={"OR": [{"user_id": ...}, {"agent_id": "org:acme"}, {"agent_id": "team:..."}]}`. A flat filter, or an `AND` of a `user_id` and an `agent_id`, returns nothing.
</Warning>
<Warning>
**`add()` is asynchronous.** It returns `{event_id, status: "PENDING"}` and extraction finishes a moment later, poll `GET /v1/event/{event_id}/` (as in Step 5) when you need to know a write has landed before searching for it.
</Warning>
<Note>
**Where the entity ID goes differs by call.** `search()` and `get_all()` take the scope inside `filters={...}` (a top-level `user_id=`/`agent_id=` is rejected). `add()` and `delete_all()` are the opposite, they take it as a top-level keyword: `client.delete_all(agent_id="team:payments")`. Deletes are asynchronous too, so a `get_all` right after a `delete_all` can still show rows for a few seconds.
</Note>
## Where to take it next
- **Auto-feed the brain** from PR descriptions, RFCs, and incident write-ups so it grows without anyone thinking about it, just insert into Supabase `knowledge` and let the cron ingest.
- **Scope by real identity** end to end: verify the Supabase JWT, read `sb.auth.get_user(jwt).user.id` for the `user_id`, look up the person's team, and `OR` their `user_id` with the company and team `agent_id`s on every recall.
- **Give teams a private view** with Supabase RLS so `team:` knowledge is only readable by that team.
---
<CardGroup cols={2}>
<Card title="Mem0 MCP Server" icon="plug" href="/platform/mem0-mcp">
Connect any agent or editor to the brain over MCP.
</Card>
<Card title="Custom Categories & Instructions" icon="sliders" href="/platform/features/custom-instructions">
Steer exactly what the brain extracts and how it's filed.
</Card>
</CardGroup>
<Snippet file="star-on-github.mdx" />
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Cookbooks and Tutorials"
sidebarTitle: "Overview"
title: Overview
seo:
title: "Cookbooks and Tutorials - Mem0"
description: "Browse cookbook examples and tutorials for building AI applications with Mem0, from companion chatbots to AI agents."
---
+2 -2
View File
@@ -89,8 +89,8 @@ On Mem0 Platform, these stores are managed for you. In OSS, you choose and opera
## Next steps
<CardGroup cols={3}>
<Card title="Entity scoping" icon="brain" href="/platform/features/entity-scoped-memory">
Organize Platform memories by user, agent, app, and run.
<Card title="Memory types" icon="brain" href="/core-concepts/memory-types">
Choose the right scope for user, agent, run, and session memory.
</Card>
<Card title="Memory operations" icon="database" href="/core-concepts/memory-operations/add">
Add, search, update, and delete memories from your app.
@@ -1,6 +1,7 @@
---
title: "Delete Memory Operation"
sidebarTitle: "Delete Memory"
title: Delete Memory
seo:
title: "Delete Memory Operation - Mem0"
description: Remove memories from Mem0 either individually, in bulk, or via filters.
icon: "trash"
iconType: "solid"
@@ -1,6 +1,7 @@
---
title: "Update Memory Operation"
sidebarTitle: "Update Memory"
title: Update Memory
seo:
title: "Update Memory Operation - Mem0"
description: Modify an existing memory by updating its content or metadata.
icon: "pen-to-square"
iconType: "solid"
+118
View File
@@ -0,0 +1,118 @@
---
title: Memory Types
description: "What memory_type actually does in Mem0: procedural memory is implemented, semantic and episodic are not."
icon: "tag"
iconType: "solid"
---
# Memory Types
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.
## Status
| 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. |
<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>
## Procedural memory
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
from mem0 import Memory
memory = Memory()
memory.add(
[
{"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",
)
results = memory.search(
"Any hotel preferences?",
filters={"user_id": "alex", "run_id": "trip-planning-2025"},
)
```
<Tip>
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>
## How memories are extracted and updated
When `infer=True` (the default) on `add()`, Mem0 runs a single pipeline rather than routing through separate type-specific paths:
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.
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 memories: they are retrievable by design. Encrypt or hash sensitive values before calling `add()`.
</Warning>
## Put it into practice
<CardGroup cols={2}>
<Card
title="Explore Memory Operations"
description="Dive into the add/search/update/delete operations next."
icon="circle-check"
href="/core-concepts/memory-operations/add"
/>
<Card
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>
+3 -8
View File
@@ -41,7 +41,6 @@
"pages": [
"platform/quickstart",
"platform/overview",
"platform/copilot",
"platform/agent-signup",
"vibecoding",
"platform/cli",
@@ -54,6 +53,7 @@
"icon": "brain",
"pages": [
"core-concepts/how-it-works",
"core-concepts/memory-types",
"core-concepts/memory-operations/add",
"core-concepts/memory-operations/search",
"core-concepts/memory-operations/update",
@@ -444,8 +444,7 @@
"cookbooks/integrations/mastra-agent",
"cookbooks/integrations/healthcare-google-adk",
"cookbooks/integrations/aws-bedrock",
"cookbooks/integrations/tavily-search",
"cookbooks/integrations/supabase"
"cookbooks/integrations/tavily-search"
]
},
{
@@ -1032,11 +1031,7 @@
},
{
"source": "/concepts/memory-scoring",
"destination": "/core-concepts/how-it-works"
},
{
"source": "/core-concepts/memory-types",
"destination": "/core-concepts/how-it-works"
"destination": "/core-concepts/memory-types"
},
{
"source": "/cookbooks/research-copilot",
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Integrations Overview"
sidebarTitle: "Overview"
title: Overview
seo:
title: "Integrations Overview - Mem0"
description: "Overview of Mem0 integrations with popular AI frameworks and tools for persistent memory and context management."
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "AWS Bedrock Integration"
sidebarTitle: "AWS Bedrock"
title: AWS Bedrock
seo:
title: "AWS Bedrock Integration with Mem0"
description: "Use Mem0 with AWS Bedrock and OpenSearch Service for cloud-native persistent semantic memory storage."
---
+3 -3
View File
@@ -130,10 +130,10 @@ print(response.msgs[0].content)
<CardGroup cols={2}>
<Card
title="How Mem0 works"
description="Understand how Mem0 extracts, stores, and retrieves memories for your Camel agents."
title="Memory types in Mem0"
description="Choose between chat history and semantic search for your Camel agents."
icon="sparkles"
href="/core-concepts/how-it-works"
href="/core-concepts/memory-types"
/>
<Card
title="Try LangChain next"
-30
View File
@@ -172,36 +172,6 @@ claude plugin update mem0@mem0-plugins --scope user
| Sidekick won't start | Must be in a Git repo. Check that your Claude Code version supports plugin agents and worktrees. |
| Remove the plugin | `claude plugin uninstall mem0@mem0-plugins` |
## Telemetry
The plugin sends usage events (which hook ran, timing, result counts, failure
types) so Mem0 can see what's used and what's breaking.
These events are **not anonymous**. When an API key is configured, which
installing the plugin requires, they are sent under your Mem0 account email,
the same way the Python SDK and the CLI attribute theirs. Without a key they
are sent under a random per-machine id.
Each event carries the event name, the plugin version, the harness it ran in,
your OS and Python version, and per-event properties describing what happened:
timings, counts, coarse outcome and failure labels, and which model was
configured. Repository and session identifiers are hashed with a random salt
generated on your machine, so they cannot be linked back to a repository name
or path.
The exact set is enforced in code rather than by this list: every property is
filtered through a denylist of sensitive keys and credential-shaped values are
redacted before anything is sent.
Prompts, memory text, queries, file paths, repository names, and API keys are
never sent.
Turn it off:
```bash
export MEM0_TELEMETRY=false
```
<CardGroup cols={2}>
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
Detailed MCP configuration for all clients
+1 -1
View File
@@ -3,7 +3,7 @@ title: Codex
description: "Add persistent memory to OpenAI Codex with automatic capture, automatic recall, a search tool, and six memory skills."
---
Add persistent memory to [**OpenAI Codex**](https://openai.com/codex/) with the Mem0 plugin. Codex forgets everything between tasks. This plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context on the first prompt of a session. Codex can use the search tool for recall later in the session.
Add persistent memory to [**OpenAI Codex**](https://openai.com/index/codex/) with the Mem0 plugin. Codex forgets everything between tasks. This plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context on the first prompt of a session. Codex can use the search tool for recall later in the session.
<Info>Current plugin version: `0.3.1`.</Info>
+1 -1
View File
@@ -114,7 +114,7 @@ Both tools also accept optional per-call `userId`, `agentId`, and `runId` params
## Telemetry
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0 can attribute usage to this integration. Usage events include operation names, durations, result counts, and coarse failure kinds. They are **not anonymous**: when an API key is configured they are sent under your Mem0 account email, the same way the SDK attributes its own. Queries, memory text, entity IDs, and API keys are never included. Set `MEM0_TELEMETRY=false` to opt out.
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0 can attribute usage to this integration. Anonymous usage events include operation names, durations, result counts, and coarse failure kinds. Queries, memory text, entity IDs, and API keys are never included. Set `MEM0_TELEMETRY=false` to opt out.
<Note>
This plugin is a developer preview and tracks the evolving DeepSeek Harness plugin API.
+1 -1
View File
@@ -23,7 +23,7 @@ npm install -g flowise
npx flowise start
```
2. Access to the Flowise UI at `http://localhost:3000`
2. Access to the Flowise UI at http://localhost:3000
3. Basic familiarity with [Flowise's LLM orchestration](https://flowiseai.com/#features) concepts
## Setup and Configuration
+92 -116
View File
@@ -1,9 +1,9 @@
---
title: Hermes Agent
description: "Add persistent memory to Hermes Agent with Mem0 Cloud, a self-hosted server, or the in-process OSS SDK."
description: "Add long-term memory to Hermes agents with Mem0: managed Platform, a self-hosted server, or fully local OSS — with current-turn recall and background fact extraction."
---
Add long-term memory to [Hermes Agent](https://github.com/NousResearch/hermes-agent), a self-improving AI agent CLI by Nous Research. The [standalone Mem0 plugin](https://github.com/mem0ai/mem0/tree/main/integrations/hermes-plugin-mem0) learns facts from conversations and recalls relevant memories for the current question.
Add long-term memory to [Hermes Agent](https://github.com/NousResearch/hermes-agent), a self-improving AI agent CLI by Nous Research. Hermes has a pluggable memory system, and Mem0 is one of the supported providers. Once enabled, Mem0 learns facts from your conversations and surfaces relevant ones for the current question, without slowing down the chat.
You can run Mem0 in three ways:
@@ -17,15 +17,11 @@ Hermes runs a built-in memory system (file-based `MEMORY.md` and `USER.md`) alon
### 1. Current-turn recall (bounded wait)
When you send a message, Hermes searches your stored memories for the current question and waits up to 3 seconds for results. If they arrive in time, they are injected into the system prompt so the model can see them. If the backend is slower, Hermes skips the injection and the model can still call `mem0_search` itself after the bounded recall wait.
When you send a message, Hermes searches your stored memories for the current question and waits up to 3 seconds for results. If they arrive in time, they are injected into the system prompt so the model can see them. If the backend is slower, Hermes skips the injection and the model can still call `mem0_search` itself — so a slow backend never blocks a turn.
### 2. Background fact extraction (sync)
Once the model finishes, the plugin sends the user message and assistant response to Mem0 in a background thread for fact extraction. Each write includes the agent identifier and gateway channel.
<Note>
Automatic capture truncates each message to **450 characters by default in every mode**, preferring a sentence boundary. Adjust `sync_max_chars` for your model's context limit. Capture is best effort: if the previous sync is still running after a five-second wait, the next turn is skipped. Use `mem0_add` to store specific text verbatim.
</Note>
Once the model finishes, Hermes sends the `(user message, assistant response)` pair to Mem0 in a background thread. Mem0 extracts facts automatically (for example, "user prefers Python" or "user works at Acme Corp"), so you never have to tell it what to remember. Each write is tagged with the gateway channel it came from.
## Agent Tools
@@ -33,33 +29,21 @@ When Mem0 is active, the model gets four tools it can call during a conversation
| Tool | Description | Parameters |
|------|-------------|------------|
| `mem0_search` | Semantic search by meaning, ranked by relevance | `query` (required), `top_k` (default 10, max 50), `rerank` (uses the configured default, Platform mode only) |
| `mem0_search` | Semantic search by meaning, ranked by relevance | `query` (required), `top_k` (default 10, max 50), `rerank` (default `false`, Platform mode only) |
| `mem0_add` | Store a fact verbatim, with no LLM extraction | `content` (required) |
| `mem0_update` | Update a memory's text by ID | `memory_id`, `text` (both required) |
| `mem0_delete` | Delete a memory by ID | `memory_id` (required) |
## Installation
Install [Hermes Agent](https://github.com/NousResearch/hermes-agent) with memory-provider plugin support and Python 3.11 or later. Once the plugin directory is available on Mem0's main branch, install it from the repository subdirectory:
Install Hermes Agent:
```bash
hermes plugins install mem0ai/mem0/integrations/hermes-plugin-mem0
hermes plugins enable mem0
hermes memory setup
hermes memory status
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash
source ~/.bashrc
```
Select **mem0** in setup and choose one of the modes below. Start a fresh Hermes conversation after setup.
Hermes installers with plugin dependency support install `mem0ai>=2.0.10,<3` and `httpx>=0.27,<1` from the plugin's `pyproject.toml`. Older hosts such as Hermes v0.21.3 require those packages to be installed explicitly into the Hermes Python environment. The OSS setup flow installs additional provider packages as needed.
<Note>
Hermes versions that still bundle Mem0 prefer the bundled provider. Use a Hermes release that has completed the standalone-provider migration; installing this plugin alone does not replace the bundled implementation. Existing users should keep their current configuration; see [Migration for existing users](#migration-for-existing-users).
</Note>
<Note>
Run the setup wizard in an interactive terminal. On Hermes hosts whose `hermes memory setup --help` lists only a provider argument, options such as `--mode`, `--host`, and `--oss-llm` are rejected by Hermes before the plugin runs. Use `hermes memory setup mem0` or the manual configuration below. Redirected input cannot select the mode picker and falls back to Platform.
</Note>
The `mem0ai` package is installed automatically when you enable the Mem0 provider, so there is no manual pip step. OSS providers may need extra packages (for example `qdrant-client`, `psycopg2-binary`, or `ollama`), which the setup flow installs for you when you pick them.
## Platform Setup
@@ -68,10 +52,10 @@ Platform mode uses managed Mem0 Cloud and is the fastest way to start.
### Option 1: Interactive wizard (recommended)
```bash
hermes memory setup mem0
hermes memory setup
```
Choose **Platform** and paste your API key when prompted. The wizard writes settings to `$HERMES_HOME/mem0.json` and keeps the key in that profile's `.env`. The default Hermes home is `~/.hermes`; named profiles use their own home directory.
Select **mem0**, choose **Platform**, and paste your API key when prompted. The wizard writes the non-secret settings to `~/.hermes/mem0.json` and keeps the key in `~/.hermes/.env`.
<Note>Get your API key from <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-hermes">app.mem0.ai</a>.</Note>
@@ -79,25 +63,17 @@ Choose **Platform** and paste your API key when prompted. The wizard writes sett
```bash
hermes config set memory.provider mem0
echo "MEM0_API_KEY=your-api-key" >> ~/.hermes/.env
```
Add your key to the active Hermes profile's `.env`:
Then in your `config.yaml`:
```dotenv
MEM0_API_KEY=your-api-key
```yaml
memory:
provider: mem0
```
Set these values in the active profile's `mem0.json`, choosing a stable user identity:
```json
{
"mode": "platform",
"host": "",
"user_id": "my-hermes-user"
}
```
Remove any stale `MEM0_HOST` from the environment and profile `.env`, and remove an old inline `api_key` from `mem0.json` so the new `.env` key is used. The config command sets `memory.provider: mem0` in that profile's `config.yaml`. Restart Hermes and check `hermes memory status`.
That's it. Mem0 runs automatically from here.
## Self-Hosted Server Setup
@@ -106,47 +82,48 @@ Run the [Mem0 server](https://github.com/mem0ai/mem0/tree/main/server) (FastAPI
### Interactive
```bash
hermes memory setup mem0
# Choose "Self-hosted server", then enter the server URL and API key
hermes memory setup
# Select "mem0", then "Self-hosted server", and enter the server URL
```
### Manual configuration
### With flags
Select `mem0` with `hermes config set memory.provider mem0`. Set these values in the active profile's `mem0.json`:
```json
{
"mode": "platform",
"host": "http://localhost:8888",
"user_id": "my-hermes-user"
}
```bash
hermes memory setup mem0 --mode selfhosted \
--host http://localhost:8888 \
--api-key your-admin-api-key
```
Add the server key to that profile's `.env`:
### With environment variables
```dotenv
MEM0_API_KEY=your-admin-api-key
```bash
echo "MEM0_HOST=http://localhost:8888" >> ~/.hermes/.env
echo "MEM0_API_KEY=your-admin-api-key" >> ~/.hermes/.env
```
Remove an old inline `api_key` from `mem0.json` so the `.env` key is used. `MEM0_HOST` can also supply the server URL, but a non-empty `host` in `mem0.json` overrides it. Keep `mode` set to `platform` for the HTTP server backend.
Then start a fresh Hermes session and call `mem0_search` — it connects to your server. The plugin authenticates with `X-API-Key` and uses the server's `/search` and `/memories` routes. The API key is optional only for servers running with `AUTH_DISABLED`.
<Note>Setting `host` routes to the self-hosted server automatically. Don't combine it with `mode: oss` — OSS takes precedence and ignores `host`.</Note>
## OSS (Self-Hosted) Setup
OSS mode runs the Mem0 SDK in the Hermes process with your chosen LLM, embedder, and vector store. It does not use Mem0 Cloud or require a Mem0 API key. Data goes to the model services you configure; use local Ollama models and local storage for a fully local setup.
OSS mode runs Mem0 entirely on your own infrastructure: your LLM, your embedder, and your vector store. No data is sent to Mem0 Cloud, and no Mem0 API key is required.
### Interactive
```bash
hermes memory setup mem0
# Choose "Open Source"
hermes memory setup
# Select "mem0", then "Open Source (self-hosted)"
# Follow the prompts for LLM, embedder, and vector store
```
The wizard uses the listed default OpenAI models and local Qdrant storage. For custom OpenAI-compatible endpoints, deployment names, or a Qdrant server, use manual configuration below.
### With flags
```bash
hermes memory setup mem0 --mode oss \
--oss-llm openai --oss-llm-key sk-... \
--oss-vector qdrant
```
### Supported providers
@@ -156,24 +133,52 @@ The wizard uses the listed default OpenAI models and local Qdrant storage. For c
| Embedder | `openai` (default `text-embedding-3-small`), `ollama` (local, default `nomic-embed-text`) |
| Vector store | `qdrant` (local path or server), `pgvector` |
### Manual configuration
### Flag reference
| Flag | Description |
|------|-------------|
| `--mode` | `platform`, `selfhosted`, or `oss` |
| `--api-key` | Platform API key, or the admin key of a self-hosted server |
| `--host` | Self-hosted server URL (with `--mode selfhosted`) |
| `--oss-llm` | LLM provider (`openai` or `ollama`, default `openai`) |
| `--oss-llm-key` | LLM API key (for `openai`) |
| `--oss-llm-model` | Override the LLM model |
| `--oss-llm-url` | LLM base URL (for `ollama` or a custom endpoint) |
| `--oss-embedder` | Embedder provider (default `openai`) |
| `--oss-embedder-key` | Embedder API key |
| `--oss-embedder-model` | Override the embedder model |
| `--oss-embedder-url` | Embedder base URL (for `ollama` or a custom endpoint) |
| `--oss-vector` | Vector store (`qdrant` or `pgvector`, default `qdrant`) |
| `--oss-vector-path` | Local Qdrant storage path |
| `--oss-vector-url` | Qdrant server URL |
| `--oss-vector-host`, `--oss-vector-port` | PGVector or remote Qdrant host and port |
| `--oss-vector-user`, `--oss-vector-password`, `--oss-vector-dbname` | PGVector connection details |
| `--user-id` | Canonical user identifier |
| `--dry-run` | Preview the resolved config without writing it |
## Switching Modes
You can move between the three modes at any time. Run the setup command again, or edit `~/.hermes/mem0.json` directly.
```bash
hermes config set memory.provider mem0
# Platform to OSS
hermes memory setup mem0 --mode oss --oss-llm-key sk-...
# OSS to Platform
hermes memory setup mem0 --mode platform --api-key sk-...
# Platform to a self-hosted server
hermes memory setup mem0 --mode selfhosted --host http://localhost:8888
# Preview without writing anything
hermes memory setup mem0 --mode oss --oss-llm-key sk-... --dry-run
```
Add the model key to the active profile's `.env`:
```dotenv
OPENAI_API_KEY=your-model-api-key
```
Set the following in that profile's `mem0.json`. Use your existing storage path when migrating; for a new named profile, choose a path inside that profile's home.
A self-hosted `~/.hermes/mem0.json` looks like this:
```json
{
"mode": "oss",
"user_id": "my-hermes-user",
"oss": {
"llm": {"provider": "openai", "config": {"model": "gpt-5-mini", "is_reasoning_model": true}},
"embedder": {"provider": "openai", "config": {"model": "text-embedding-3-small"}},
@@ -182,65 +187,33 @@ Set the following in that profile's `mem0.json`. Use your existing storage path
}
```
For an OpenAI-compatible service such as Azure's `/openai/v1` endpoint, add `OPENAI_BASE_URL` to the profile's `.env` and set each `model` to its deployed name. Both the LLM and embedder use this endpoint unless their `config.openai_base_url` overrides it. The main Hermes chat model is configured separately; this JSON configures Mem0's extraction and embedding models.
For a Qdrant server, replace `vector_store.config.path` with `url`, for example `"url": "http://localhost:6333"`. Manual setup does not install optional provider dependencies: install `qdrant-client`, `psycopg2-binary`, or `ollama` in the **Hermes Python environment** as needed for your selected providers. Start a fresh session and verify a memory write and search; `hermes memory status` reports configuration availability, not a full backend health check.
Desktop sessions in the same process and profile share local Qdrant storage when their OSS settings match. Operations are serialized, and storage closes after the last session releases it. Conflicting settings are rejected without changing existing memories; close active sessions before changing models or credentials. For concurrent CLI and Desktop processes, use a Qdrant server or the self-hosted Mem0 HTTP API instead of sharing a local directory.
## Switching Modes
Run `hermes memory setup mem0` in an interactive terminal and choose the new mode, or edit the active profile's `mem0.json` using the examples above. Switching backends does not transfer memories between them. Preserve existing OSS storage paths when editing configuration. When returning to Platform, set `mode` to `platform`, clear `host`, and remove any stale `MEM0_HOST` setting from your environment and profile `.env`.
## Configuration
Settings live in `$HERMES_HOME/mem0.json` and are written by `hermes memory setup`. API keys normally live in that profile's `.env`; distinct OpenAI LLM/embedder keys and database credentials are stored in the OSS configuration. Setup writes these files atomically with owner-only permissions.
When editing these files manually, restrict both `.env` and `mem0.json` to their owner (`chmod 600` on Unix). Keep configuration and secrets in the same active Hermes profile.
`MEM0_MODE`, `MEM0_HOST`, `MEM0_USER_ID`, and `MEM0_AGENT_ID` supply environment defaults. Non-empty values in `mem0.json` take precedence. `MEM0_API_KEY` supplies the Cloud or server key unless `api_key` is set in the file.
Behavioral settings live in `~/.hermes/mem0.json` and are written for you by `hermes memory setup`. Only the secret `MEM0_API_KEY` belongs in `~/.hermes/.env`.
| Key | Default | Description |
|-----|---------|-------------|
| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-managed, in-process). Self-hosted server routing is set via `host` |
| `host` | none | Self-hosted Mem0 server URL. When set, the plugin talks HTTP to your server instead of the cloud |
| `api_key` | none | Mem0 Platform API key, or the admin key of a self-hosted server. Stored in `.env` as `MEM0_API_KEY` |
| `user_id` | gateway user ID, then `hermes-user` | Identifier that scopes memories. See cross-channel behavior below |
| `user_id` | `hermes-user` | Identifier that scopes memories. See cross-channel behavior below |
| `agent_id` | `hermes` | Agent identifier attached to writes |
| `rerank` | `false` | Platform reranking for recall and tool searches that omit `rerank` |
| `sync_max_chars` | `450` | Per-message character cap for automatic fact extraction in every mode |
| `oss` | `{}` | OSS LLM, embedder, and vector-store configuration |
| `rerank` | `false` | Rerank search results for relevance (Platform mode only) |
### Cross-channel memories
Hermes can run from the CLI and from gateways like Telegram, Slack, and Discord. The `user_id` setting controls how memories are scoped across them:
- **Set a `user_id` other than `hermes-user`** and it applies to every gateway, so one person gets a single merged memory store no matter where they talk to the agent.
- **Leave it unset** (or at the default `hermes-user`) and each gateway uses its own native ID when available, falling back to `hermes-user`.
- **Set a `user_id`** and it applies to every gateway, so one person gets a single merged memory store no matter where they talk to the agent.
- **Leave it unset** (or at the default `hermes-user`) and each gateway uses its own native id, keeping per-platform memories separate.
Every write is tagged with `metadata.channel` (for example `telegram` or `cli`). Plugin searches filter by user identity across sessions; they do not restrict recall to the current channel or session.
## Migration for Existing Users
Keep `memory.provider: mem0`, `mem0.json`, `MEM0_*` settings, user identity, and OSS database paths unchanged. Moving from the bundled provider to this standalone plugin does not require rerunning setup or moving stored memories.
Automatic migration depends on Hermes rollout as well as this repository:
1. Users need a Hermes build containing [PR #114569](https://github.com/NousResearch/hermes-agent/pull/114569).
2. Hermes maintainers must approve a catalog entry named `mem0` with `repo: https://github.com/mem0ai/mem0`, `subdir: integrations/hermes-plugin-mem0`, and a reviewed full commit SHA.
3. The bundled Mem0 provider must be removed so the standalone provider can load.
With these in place, Hermes installs a missing configured provider during `hermes update` across profiles or at agent startup. Startup installation respects `security.allow_lazy_installs`. Offline or disabled installation needs manual action; merging the plugin directory alone does not complete automatic migration.
CLI setup and status are supported. This plugin does not ship a Desktop configuration panel or provider-specific CLI commands.
Either way, every write is tagged with `metadata.channel` (for example `telegram` or `cli`), so per-channel views are still possible at query time.
## Reliability
- **Circuit breaker**: five consecutive backend failures pause calls for two minutes. The agent can continue without memory during that window. Expected update/delete errors such as a missing memory do not trip the breaker.
- **Bounded waits**: recall waits up to three seconds. Capture runs in the background, but an overlapping turn may wait up to five seconds for the previous sync before being skipped.
- **Graceful shutdown**: shutdown and Python process exit wait for active recall and capture workers before closing the backend. Backend network timeouts still apply. Self-hosted HTTP capture uses a 120-second read timeout and a 30-second connection timeout; other self-hosted HTTP operations use 30 seconds.
- **Best-effort capture**: there is no durable queue. Forced termination, including Hermes' 30-second exit watchdog, can interrupt pending writes even while graceful shutdown is waiting.
- **OSS data protection**: an embedding dimension mismatch fails initialization without deleting the existing collection or table.
- **Circuit breaker**: if Mem0 fails five times in a row, Hermes pauses calls for two minutes, then retries. The agent keeps working without memory during that window. Expected client errors, like a 404 on a missing memory id, do not count toward tripping the breaker.
- **Non-blocking**: fact extraction runs in a background daemon thread, and current-turn recall waits at most 3 seconds, so a slow or failed call never blocks your conversation.
- **Thread-safe**: the client uses lazy initialization with locking, and the background sync and recall threads are guarded so concurrent gateway messages cannot produce duplicate memories.
## Troubleshooting
@@ -275,12 +248,15 @@ curl http://localhost:11434/api/tags
- `mem0_add` stores text verbatim with no extraction. Ordinary conversation turns are extracted automatically by the background sync.
- Search is semantic, so try a broader query.
- Confirm `user_id` is the same across sessions (check `$HERMES_HOME/mem0.json`).
- Check `sync_max_chars`: facts beyond the per-message limit are not sent for extraction.
- Confirm `user_id` is the same across sessions (check `~/.hermes/mem0.json`).
### OSS: embedding dimension mismatch
## Key Features
Restore the embedding model and dimensions that created the existing collection, or choose a new collection and migrate data explicitly. The plugin leaves the original collection intact when dimensions differ.
1. **Three ways to run**: managed Platform, a self-hosted server, or fully local OSS, switchable at any time.
2. **Current-turn recall**: memories for the current question are injected within a 3-second window, with `mem0_search` as the model's own backstop.
3. **Automatic extraction**: Mem0 extracts and deduplicates facts from each exchange for you.
4. **Non-blocking and fault tolerant**: background threads plus a circuit breaker keep the agent responsive even when Mem0 is unreachable.
5. **Additive memory**: works alongside Hermes' built-in file memory (`MEMORY.md`, `USER.md`).
<CardGroup cols={2}>
<Card title="OpenClaw Integration" icon="/images/provider-icons/openclaw.svg" href="/integrations/openclaw">
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "LangChain Integration"
sidebarTitle: "Langchain"
title: Langchain
seo:
title: "LangChain Integration with Mem0"
description: "Build personalized AI agents using LangChain for conversation flow and Mem0 for long-term memory retention."
---
+1 -1
View File
@@ -45,7 +45,7 @@ memory_from_client = Mem0Memory.from_client(
)
```
Context is used to identify the user, agent or the conversation in the Mem0. It is required to be passed in at least one of the fields in the `Mem0Memory` constructor. It can be any of the following:
Context is used to identify the user, agent or the conversation in the Mem0. It is required to be passed in the at least one of the fields in the `Mem0Memory` constructor. It can be any of the following:
```python
context = {
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "n8n Integration"
sidebarTitle: "n8n"
title: n8n
seo:
title: "n8n Integration with Mem0"
description: "Add long-term memory to n8n workflows and AI Agents with the Mem0 community node, no code required."
---
+1 -3
View File
@@ -481,9 +481,7 @@ Plugin config is stored in `~/.openclaw/openclaw.json` with file permissions `0o
### Telemetry
Usage telemetry (PostHog) is enabled by default to help improve the plugin. No conversation content or memory values are included, only event counts (recall, capture, tool usage, CLI commands).
These events are **not anonymous**. OpenClaw does not send your account email the way the SDK does, but it does send an unsalted SHA-256 hash of it, falling back to a hash of the API key and then to a random per-machine id. Mem0 holds the email the hash is derived from, so the hash identifies your account rather than concealing it. The first run that resolves an account also emits a PostHog `$identify`, which permanently merges any earlier random id into that identity.
Anonymous usage telemetry (PostHog) is enabled by default to help improve the plugin. No conversation content or memory values are included, only event counts (recall, capture, tool usage, CLI commands).
To opt out, set the environment variable:
+2 -3
View File
@@ -171,7 +171,6 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
- [Introduction](https://docs.mem0.ai/introduction) [Both]: Use when the user wants a one-page overview of how memory fits between the LLM and the app.
- [Vibe Code with Mem0](https://docs.mem0.ai/vibecoding) [Both]: Use when the user is in Claude Code, Cursor, or Windsurf and wants memory wired into their editor.
- [Platform Overview](https://docs.mem0.ai/platform/overview) [Platform]: Use when the user picks the managed product - 4-line integration, hosted API, dashboard.
- [Mem0 Copilot](https://docs.mem0.ai/platform/copilot) [Platform]: Use when inspecting project memories, reviewing configuration changes, or testing extraction in the dashboard.
- [Sign up as an agent](https://docs.mem0.ai/platform/agent-signup) [Platform]: Use when an AI agent needs to mint a Mem0 API key autonomously - four commands, no email or dashboard, human claims ownership later.
- [Platform vs Open Source](https://docs.mem0.ai/platform/platform-vs-oss) [Both]: Use when the user is deciding between managed and self-hosted.
- [Platform Quickstart](https://docs.mem0.ai/platform/quickstart) [Platform]: Use for the first Platform integration - API key plus `MemoryClient.add/search`.
@@ -186,6 +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 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.
@@ -256,7 +256,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
- [Agno](https://docs.mem0.ai/integrations/agno) [Platform]: Use when the user is on Agno.
- [Camel AI](https://docs.mem0.ai/integrations/camel-ai) [Both]: Use when the user is on Camel AI.
- [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 installing or configuring the standalone Hermes memory plugin, or migrating from the bundled Mem0 provider.
- [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 automatic capture, prompt recall, scoped memory, and six memory commands to Pi Agent.
- [DeepSeek Harness](https://docs.mem0.ai/integrations/deepseek-plugin) [Platform]: Use when adding automatic recall, completed-turn capture, and native search/add tools to DeepSeek Harness.
- [OpenAI Agents SDK](https://docs.mem0.ai/integrations/openai-agents-sdk) [Platform]: Use when the user is on the OpenAI Agents SDK.
@@ -326,7 +326,6 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
- [Healthcare Google ADK](https://docs.mem0.ai/cookbooks/integrations/healthcare-google-adk) [Platform]: Use when the domain is medical and the framework is Google ADK.
- [AWS Bedrock](https://docs.mem0.ai/cookbooks/integrations/aws-bedrock) [OSS]: Use when deploying with AWS managed model services.
- [Tavily Search](https://docs.mem0.ai/cookbooks/integrations/tavily-search) [Platform]: Use when the agent layers web search on memory.
- [Company Brain (Mem0 Platform + Supabase)](https://docs.mem0.ai/cookbooks/integrations/supabase) [Platform]: Use to build a shared org brain on Mem0 Platform with Supabase as system of record and the MCP server as the access layer (with a new-hire onboarding demo).
### Framework Examples
- [LlamaIndex React](https://docs.mem0.ai/cookbooks/frameworks/llamaindex-react) [Both]: Use when building a React UI with LlamaIndex and memory.
+4 -4
View File
@@ -136,8 +136,8 @@ curl -X POST 'https://api.mem0.ai/v3/memories/?page=1&page_size=50' \
| Parameter | V1/V2 | V3 | Notes |
|---|---|---|---|
| `top_k` | Supported | Supported (1-1000, default 10) | No change |
| `threshold` | Default: `0.3` (v2) | Default: `0.1` | Pass `0.3` to keep the v2 cutoff, or `0.0` to disable |
| `rerank` | Default: `false` | Default: `false` | No change. Pass `true` to enable (adds latency) |
| `threshold` | Default: none | Default: `0.1` | Pass `0.0` to disable |
| `rerank` | Default: `true` | Default: `false` | Pass `true` to enable (adds latency) |
| Entity IDs in `search` / `get_all` | Top-level | Inside `filters` dict | Top-level raises 400 |
### Response Format
@@ -265,10 +265,10 @@ If your application previously read graph relations from the API response (`rela
<Steps>
<Step title="Review your search thresholds">
The default `threshold` is now `0.1` (it was `0.3` on v2), so lower-scoring matches that v2 hid can now appear. Scores are computed differently in v3, so retune against representative queries. To keep a stricter cutoff, pass an explicit `threshold` in your search calls, or pass `threshold=0.0` to disable filtering.
The default `threshold` is now `0.1` (previously no threshold). If your application was relying on unfiltered results, explicitly pass `threshold=0.0` in your search calls to preserve the old behavior. In most cases, the new default is better: it filters out low-relevance noise.
</Step>
<Step title="Review reranking usage">
Reranking is `false` by default, the same as on v2. If your application depends on reranked results, pass `rerank=True` to your search calls. Note that reranking adds latency (~200-400ms) but can improve ordering quality for complex queries.
Reranking is now `false` by default. If your application depended on reranked results, add `rerank=True` to your search calls. Note that reranking adds latency (~200-400ms) but can improve ordering quality for complex queries.
</Step>
<Step title="Update score handling (optional)">
The top-level `score` field continues to work as before. It is now a combined multi-signal score (semantic + keyword + entity) rather than pure cosine similarity, so the absolute numbers will differ. Relative ranking remains comparable: if you have threshold-based filtering in your app, retune on a representative query set.
@@ -1,6 +1,7 @@
---
title: "Open Source Custom Instructions"
sidebarTitle: "Custom Instructions"
title: Custom Instructions
seo:
title: "Open Source Custom Instructions - Mem0"
description: Tailor fact extraction so Mem0 stores only the details you care about.
icon: "wand-magic-sparkles"
---
@@ -1,6 +1,7 @@
---
title: "Open Source Multimodal Support"
sidebarTitle: "Multimodal Support"
title: Multimodal Support
seo:
title: "Open Source Multimodal Support - Mem0"
description: Capture and recall memories from both text and images.
icon: "image"
---
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Open Source Features Overview"
sidebarTitle: "Overview"
title: "Overview"
seo:
title: "Open Source Features Overview - Mem0"
description: "Self-hosting features that extend Mem0 beyond basic memory storage"
icon: "list"
---
+1 -1
View File
@@ -56,7 +56,7 @@ The Mem0 REST API server exposes every OSS memory operation over HTTP. Run it al
make bootstrap # starts Compose, creates an admin, issues the first API key
```
Or to start the stack only and finish setup via the browser wizard at `http://localhost:3000`:
Or to start the stack only and finish setup via the browser wizard at http://localhost:3000:
```bash
cd server
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "Open Source Overview"
sidebarTitle: "Overview"
title: "Overview"
seo:
title: "Mem0 Open Source Overview"
description: "Self-host Mem0 with full control over your infrastructure and data"
icon: "house"
---
+35 -3
View File
@@ -8179,7 +8179,7 @@
},
"entities": {
"type": "object",
"description": "Settings for user profiles, under `user`.",
"description": "Per-entity-type settings. Only `user` is available in this release.",
"properties": {
"user": {
"type": "object",
@@ -8196,6 +8196,22 @@
"description": "Extra guidance for the extraction step."
}
}
},
"agent": {
"type": "object",
"properties": {
"schema": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
}
}
}
}
},
@@ -8245,7 +8261,7 @@
},
"entities": {
"type": "object",
"description": "Settings for user profiles, under `user`.",
"description": "Per-entity-type settings. Only `user` is available in this release.",
"properties": {
"user": {
"type": "object",
@@ -8284,7 +8300,7 @@
},
"entities": {
"type": "object",
"description": "Settings for user profiles, under `user`.",
"description": "Per-entity-type settings. Only `user` is available in this release.",
"properties": {
"user": {
"type": "object",
@@ -8301,6 +8317,22 @@
"description": "Extra guidance for the extraction step."
}
}
},
"agent": {
"type": "object",
"properties": {
"schema": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
}
}
}
}
},
+3 -2
View File
@@ -1,6 +1,7 @@
---
title: "CLI for Terminal Memory Management"
sidebarTitle: "CLI"
title: CLI
seo:
title: "Mem0 CLI for Terminal Memory Management"
description: "Manage memories from your terminal, for both humans and AI agents."
icon: "terminal"
iconType: "solid"
-194
View File
@@ -1,194 +0,0 @@
---
title: "Mem0 Copilot"
description: "Inspect project memories, review configuration changes, and test extraction from the Mem0 dashboard."
icon: "message"
---
Copilot is an AI assistant in the [Mem0 dashboard](https://app.mem0.ai/dashboard/copilot). Ask it to inspect stored memories, suggest extraction rules and categories, change project settings, or explain the platform SDKs and APIs.
You describe the task in chat. Copilot uses tools to read project data or propose changes. In **Review changes** mode, you approve each change before it runs.
## Start with your project
1. Sign in to the dashboard and select the organization and project you want to work on.
2. Open **Copilot** in the sidebar.
3. Select **Review changes** below the message box.
4. Ask: “Show my current project settings and explain what each one does.”
5. Expand an activity card, such as **Read project config**, to see its **Input** and **Result**. Check these results when reviewing an answer or confirming a change.
You can ask about settings or SDK usage with an empty project. Suggestions based on stored data need enough project history to analyze.
## Inspect stored memories
Start with a project overview, then narrow the question:
| Prompt | What to inspect |
| --- | --- |
| “Summarize what this project has stored and how it is categorized.” | The memories and category patterns Copilot reads. Check which records support its summary. |
| “Analyze the memories for user alice.” | Facts stored for `alice`, their categories, and unwanted information. To investigate a missing fact, also provide the original input. |
| “Search alice's memories for dietary preferences.” | Memories relevant to the query within that user's scope. |
Copilot can list memories across the project. Searching by meaning needs a specific User, Agent, App, or Run ID. Select one in **Scope** or name it in your prompt.
Memory lists are paginated, and suggestions use samples. Check the activity results to see which records were read. Ask for more pages when you need to inspect the rest.
### Choose the memory scope
Open **Scope** below the message box. Select an existing ID or type one and choose it.
| Control | Identifies |
| --- | --- |
| **User** | The end user whose memories you want to inspect, such as `alice`. |
| **Agent** | An AI agent associated with the memories. |
| **App** | An application associated with the memories. |
| **Run** | A particular execution or session associated with the memories. |
A field set to **any** adds no filter for that entity type. **Clear scope** removes the selections. Scope gives Copilot default IDs to use; you can request a different entity in a message. Check the activity's **Input** to confirm which IDs it used. Adding a test memory requires a **User** ID, even when another entity is selected.
<Note>
Scope selects memories, not separate settings. Categories, extraction instructions, memory depth, and multilingual settings apply to the selected project. Category and extraction suggestions also analyze project data, regardless of the selected entity.
</Note>
See [Entity-scoped memory](/platform/features/entity-scoped-memory) for how these IDs organize memories in your application.
## Review and apply changes
The mode control below the message box determines when changes run:
- **Review changes**: Copilot pauses before changing project configuration or adding a test memory. Read the proposal and choose whether to apply it.
- **Auto-apply**: Copilot can change settings and add test memories without asking for approval. Check the selected project and scope before using it.
Ask Copilot to show the current settings before requesting an update. In the approval card, review every listed field. Approving the card applies the whole proposal, including fields you did not edit.
| Action | Effect |
| --- | --- |
| **Apply change** | Approve the proposed write. Inspect the subsequent result to confirm it succeeded. |
| **Edit** | When offered, edit extraction instructions or category names and descriptions. Choose **Apply with edits** to submit your revision. |
| **Discard edits** | Return to the original proposal without applying it. |
| **Decline** | Reject this proposal without applying it. |
| **Ask for something else** | Enter feedback and choose **Send**. This declines the current proposal and sends your feedback as a new message. Review the next proposal before applying it. |
Not every field has an inline editor. If a proposal includes both extraction instructions and categories, only the instructions get an editor. The editor cannot apply blank instructions or an empty category list. Use **Ask for something else** to change other fields or request separate proposals.
<Warning>
A generated prompt profile contains separate prompts for extraction and summaries. If your project uses one, saving extraction instructions through Copilot deactivates it. Review the replacement rules and test the affected behavior before using them in production.
</Warning>
## What Copilot cannot do
Copilot cannot perform these actions, even in **Auto-apply** mode:
- Create or delete API keys.
- Directly edit or delete existing memories.
- Export project data.
- Invite or remove organization or project members, or change their roles and permissions.
- Create or delete organizations or projects.
- Change billing details or subscription plans.
Use the dashboard or the relevant platform API for these tasks. Copilot can explain the steps or point you to documentation, but it cannot carry out the actions.
Copilot supports the managed Mem0 platform. It does not help configure or use the [self-hosted open-source library](/open-source/overview).
## Example 1: Stop storing small talk, then test extraction
This walkthrough uses a project where you have noticed greetings or small talk in stored memories. Use a development project when trying configuration changes, since a test user does not isolate project settings.
<Steps>
<Step title="Inspect the problem">
Select the project, set **User** to `alice`, and ask:
> Analyze the memories for user alice.
Inspect the returned memories. Identify examples of small talk you want to exclude and useful facts you still want to keep.
</Step>
<Step title="Request a targeted change">
With **Review changes** selected, ask:
> Update my extraction instructions to ignore greetings and small talk. Keep the existing rules for durable user preferences.
Check for a **Read project config** activity before reviewing the update. If it is missing, ask Copilot to read the current instructions first. If analysis reports insufficient data, ask it to use the rule you provided. You can also set the rule directly through [Custom instructions](/platform/features/custom-instructions).
</Step>
<Step title="Review the proposal">
Review the full replacement text. Keep existing rules your application needs. For this test, the relevant rules could look like:
```text
Remember the following:
* The user's dietary preferences and food restrictions.
Don't remember the following:
* Greetings and small talk.
```
Use **Edit** to adjust the wording, or **Ask for something else** to request a revision.
Choose **Apply change** or **Apply with edits**, then inspect the result to confirm the instructions were saved.
</Step>
<Step title="Add a test conversation">
Choose a new **User** ID for this test, such as `copilot-extraction-test-01`, and clear any other entity selections. Ask:
> Add this as a user message for copilot-extraction-test-01: “Hi! How's your day? I prefer vegetarian meals and avoid peanuts.”
Review the test-memory proposal and choose **Apply change**.
<Warning>
Adding a test memory writes to the selected project. It is not a dry run. Use a dedicated test user so you can find and remove the test data afterward.
</Warning>
</Step>
<Step title="Wait for extraction, then check the result">
Expand **Add test memory** and inspect **Result**. A `PENDING` status means extraction is still running. Use the returned `event_id` with the [Get Event API](/api-reference/events/get-event) to check progress. Wait for `SUCCEEDED` before evaluating the output. If the event is `FAILED`, inspect its details before retrying.
Then ask:
> List all memories for user copilot-extraction-test-01. Then search that user's memories for dietary preferences.
Check that the memories retain the vegetarian preference and peanut restriction, and exclude the greeting. Inspect the full list as well as search results: a relevant search can hide unwanted small-talk records.
If the output is wrong, describe the mismatch and review another instruction change. Use a new test user for the next attempt so earlier memories do not affect the result. Remove test data afterward through the dashboard or [memory deletion API](/api-reference/memory/delete-memories).
</Step>
</Steps>
Test with new inputs after changing settings. Updating configuration is not a cleanup operation for existing memories. See [Custom instructions](/platform/features/custom-instructions) for guidance on writing extraction rules.
## Example 2: Suggest categories and extraction instructions
These workflows sample the selected project's data. Generating a suggestion does not update settings by itself. Copilot can then propose applying it, which follows the selected review mode. Use **Review changes** to inspect suggestions before they are saved.
| Workflow | Data used | Example prompt |
| --- | --- | --- |
| Custom categories | Stored memories, excluding deleted memories. | “Suggest custom categories from this project's memories.” |
| Extraction instructions | Successful requests to add conversation data, including requests that produced no memories. | “Compare recent add requests with the extracted memories and suggest better extraction instructions.” |
Category suggestions identify recurring themes. Review the names, descriptions, and examples. Applying a category list replaces the project's previous list; it does not add to it or re-tag existing memories. See [Custom categories](/platform/features/custom-categories) for project and per-call behavior.
Extraction suggestions compare conversation inputs with what was extracted. One add request can produce several memories or none, so the number of add requests is different from the number of stored memories. Keep your application's existing requirements when reviewing the proposal.
Both workflows require enough project data. If a suggestion fails because there is too little data, check the activity's **Result** for the current and required counts. You can still inspect memories, ask SDK/API questions, or provide your own rules instead of requesting a data-based suggestion.
## Example 3: Adjust detail and language
Ask Copilot to read the current settings, then request the change you need:
- **Memory depth** controls the level of detail: **Less**, **Medium**, or **More detailed**. Try “Show my current memory depth, then propose More detailed memories.” For deciding *which facts* to retain, use extraction instructions.
- **Multilingual** behavior preserves the user's original language. Try “Enable multilingual behavior so memories preserve the language of the input.” Test it with a new conversation in the language your application uses.
Both are project settings. Review the proposed values and test a fresh input after applying them. See [Organization and project settings](/api-reference/organizations-projects) for configuration through the API.
## Example 4: Ask SDK and API questions
Include your language and the task, for example:
> Show me how to search memories for user alice with the managed Mem0 Python SDK. Link the documentation you used.
Copilot can look up the official platform documentation. Check the **Browse mem0 docs** and **Read docs page** activities and open the cited pages. If an answer has no sources, ask for them before using the example. The [Platform quickstart](/platform/quickstart) covers adding and searching memories in code.
## Resume a conversation and check message limits
Use **History** to reopen your 50 most recently active chats for the selected project. Chats belong to the person who created them; other project members cannot open them. Use **New chat** to start a separate conversation, or **Delete chat** in history to remove one. Deleting a chat does not undo settings changes or remove test memories.
Message allowances depend on the organization's plan and are shared across its projects and members. They reset at the start of each calendar month in UTC.
For plans with a limit, a usage notice appears once 80% of the allowance is used. Below that point, no counter is shown. At the limit, new messages are disabled until the allowance resets or the plan is upgraded. Follow the notice's upgrade link to review plan options.
Sending feedback through **Ask for something else** counts as a new message. Approving or declining a saved proposal without feedback does not use another message. Test additions and searches also use the platform APIs and remain subject to their normal quotas.
@@ -1,6 +1,7 @@
---
title: "Platform Custom Instructions"
sidebarTitle: "Custom Instructions"
title: Custom Instructions
seo:
title: "Platform Custom Instructions - Mem0"
description: 'Control how Mem0 extracts and stores memories using natural language guidelines'
---
@@ -1,6 +1,7 @@
---
title: "Platform Multimodal Support"
sidebarTitle: "Multimodal Support"
title: Multimodal Support
seo:
title: "Platform Multimodal Support - Mem0"
description: Integrate images and documents into your interactions with Mem0
---
+5 -4
View File
@@ -1,6 +1,7 @@
---
title: "Platform Overview"
sidebarTitle: "Overview"
title: "Overview"
seo:
title: "Mem0 Platform Overview"
description: "Managed memory layer for AI agents, production-ready in minutes"
icon: "cloud"
---
@@ -50,8 +51,8 @@ For the full pipeline, see [How Mem0 works](/core-concepts/how-it-works).
<Card title="Run the quickstart" icon="rocket" href="/platform/quickstart">
Get an API key and save your first memory.
</Card>
<Card title="Scope your memories" icon="brain" href="/platform/features/entity-scoped-memory">
Organize memories by user, agent, app, and run.
<Card title="Understand memory types" icon="brain" href="/core-concepts/memory-types">
How user, agent, app, and run memory differ.
</Card>
<Card title="Add, search, and update" icon="layer-group" href="/core-concepts/memory-operations/add">
The core memory operations, end to end.
+1 -1
View File
@@ -39,7 +39,7 @@ The core memory loop is identical on both: `add`, `search`, `get`, `get_all`, `u
- **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**, and **custom extraction instructions** (`custom_instructions`)
- **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
+1 -1
View File
@@ -4,7 +4,7 @@ description: "Standard layout for documenting Mem0 API endpoints."
icon: "code"
---
# API Reference Template
# Api Reference Template
API reference pages document a single endpoint contract. Present metadata, request/response examples, and recovery guidance without narrative detours.
+1 -1
View File
@@ -124,7 +124,7 @@ Walk through a real request/response. Include sample payloads and highlight nota
{/* DEBUG: verify CTA targets */}
<CardGroup cols={2}>
<Card title="Dive Into Memory Scoring" icon="scale-balanced" href="/core-concepts/how-it-works">
<Card title="Dive Into Memory Scoring" icon="scale-balanced" href="/core-concepts/memory-types">
Understand how Mem0 ranks memories under the hood.
</Card>
<Card title="Build a Research Copilot" icon="book-open" href="/cookbooks/operations/deep-research">
+4 -2
View File
@@ -38,6 +38,7 @@ client.memories.add(
user_id: str,
memory: str,
metadata: Optional[dict] = None,
memory_type: Literal["session", "long_term"] = "session",
)
```
@@ -46,13 +47,13 @@ await mem0.memories.add({
userId: string;
memory: string;
metadata?: Record<string, string>;
memoryType?: "session" | "long_term";
});
```
</CodeGroup>
<Info>
[Describe defaults supported by this operation and SDK. Do not infer a
memory type or retention policy from a scoping identifier.]
Defaults to session memories. Override `memory_type` for long-term storage.
</Info>
<Warning>
@@ -66,6 +67,7 @@ await mem0.memories.add({
| `user_id` | string | Yes | Unique identifier for the end user. | Must match follow-up operations. |
| `memory` | string | Yes | Content to persist. | Managed & OSS. Markdown allowed. |
| `metadata` | object | No | Key-value pairs for filters. | OSS stores as JSONB; limit to 2KB. |
| `memory_type` | string | No | Retention bucket | Platform supports `shared`. |
<Tip>
Set `ttl_seconds` when you need memories to expire automatically (OSS only).
+2 -2
View File
@@ -96,8 +96,8 @@ time. Storage: vector embeddings.
**Architecture Overview:**
- Memory is scoped by user_id, agent_id, or run_id
- Core operations: add, search, update, delete
- Store preferences, facts, and past interactions; use run_id to scope a
session. Platform does not expose a memory_type selector.
- Memory types: factual (preferences, facts), episodic (past interactions),
semantic (concept relationships), working (session state)
- Integration pattern: retrieve relevant memories → generate response → store
new memories
+1 -1
View File
@@ -28,7 +28,7 @@
"clsx": "^2.1.1",
"js-cookie": "^3.0.6",
"lucide-react": "^0.477.0",
"next": "15.5.24",
"next": "15.5.21",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.0.1",
+3 -3
View File
@@ -45,11 +45,11 @@ grok_client = OpenAI(
def recommend_movie_with_memory(user_id: str, user_query: str):
# Retrieve prior memory about movies
past_memories = memory.search("movie preferences", filters={"user_id": user_id})
past_memories = memory.search("movie preferences", user_id=user_id)
prompt = user_query
if past_memories["results"]:
prompt += f"\nPreviously, the user mentioned: {[m['memory'] for m in past_memories['results']]}"
if past_memories:
prompt += f"\nPreviously, the user mentioned: {past_memories}"
# Generate movie recommendation using Grok 3
response = grok_client.chat.completions.create(model="grok-3-beta", messages=[{"role": "user", "content": prompt}])
@@ -198,7 +198,7 @@ def search_memory_tool(query: str, user_id: str = "user") -> str:
Relevant vector memories found or message if none found
"""
try:
results = m.search(query, filters={"user_id": user_id})
results = m.search(query, user_id=user_id)
if isinstance(results, dict) and 'results' in results:
memory_list = results['results']
@@ -245,7 +245,7 @@ def search_graph_memory_tool(query: str, user_id: str = "user") -> str:
"""
try:
graph_query = f"relationships connections {query}"
results = m.search(graph_query, filters={"user_id": user_id})
results = m.search(graph_query, user_id=user_id)
if isinstance(results, dict) and 'results' in results:
memory_list = results['results']
@@ -290,7 +290,7 @@ def get_all_memories_tool(user_id: str = "user") -> str:
All memories for the user or message if none found
"""
try:
all_memories = m.get_all(filters={"user_id": user_id})
all_memories = m.get_all(user_id=user_id)
if isinstance(all_memories, dict) and 'results' in all_memories:
memory_list = all_memories['results']
+5 -5
View File
@@ -107,16 +107,16 @@ def main():
for query in search_queries:
print(f"\nQuery: {query}")
memories = memory.search(query=query, filters={"user_id": "user_123"})
memories = memory.search(query=query, user_id="user_123")
for memory_item in memories["results"]:
for memory_item in memories:
print(f" - {memory_item['memory']}")
print("\n--> Getting all memories for user...")
all_memories = memory.get_all(filters={"user_id": "user_123"})
print(f"Total memories stored: {len(all_memories['results'])}")
all_memories = memory.get_all(user_id="user_123")
print(f"Total memories stored: {len(all_memories)}")
for memory_item in all_memories["results"]:
for memory_item in all_memories:
print(f" - {memory_item['memory']}")
print("\n--> vLLM integration demo completed successfully!")
+4 -25
View File
@@ -23,17 +23,6 @@
"> production. The cells below read both from the environment.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Use a disposable project.** This notebook overwrites the project's profile settings\n",
"> (enabled, schema, custom instructions). The last cell restores the values saved at the\n",
"> start, but only if you reach it: if a cell fails midway, the project keeps the demo\n",
"> schema until you run the cleanup cell or reset it yourself. Do not point it at a\n",
"> project other people or production traffic depend on.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -217,10 +206,7 @@
" print(f\" add {event_status}\")\n",
" time.sleep(5)\n",
"print(f\"add finished: {event_status}\")\n",
"# Stop here unless the add SUCCEEDED. A failed or unfinished add would otherwise let the\n",
"# generation below bill for a profile built without these memories.\n",
"if event_status != \"SUCCEEDED\":\n",
" raise RuntimeError(f\"add did not succeed (status={event_status}); not generating a profile\")\n",
"assert event_status != \"FAILED\", \"the add itself failed — nothing downstream can work\"\n",
"\n",
"# 2. Extraction lands in batches, so the FIRST non-empty page is not the whole set.\n",
"# Wait for the count to stop growing instead of breaking on the first result.\n",
@@ -288,10 +274,8 @@
"`generate_profile()` closes the bootstrapping gap: without it a new user has no profile\n",
"until their tenth message. One entity, a few seconds.\n",
"\n",
"Each call sends a new `Idempotency-Key` unless you pass one, and a new key starts a new job.\n",
"To retry a dropped request safely, generate the key yourself and pass the same\n",
"`idempotency_key` on every attempt: the server then returns the original job instead of\n",
"billing a second one.\n"
"Every call is sent with a fresh `Idempotency-Key`, so a retry after a dropped connection\n",
"returns the same job instead of paying twice.\n"
]
},
{
@@ -404,17 +388,12 @@
" # Wait for the add to land before triggering: a generation queued before the new\n",
" # memories exist rewrites the profile from the OLD ones and looks like a no-op.\n",
" deadline = time.time() + 300\n",
" status = None\n",
" while time.time() < deadline:\n",
" status = client.client.get(f\"/v1/event/{followup['event_id']}/\").json().get(\"status\")\n",
" if status in (\"SUCCEEDED\", \"FAILED\"):\n",
" break\n",
" time.sleep(5)\n",
" print(\"follow-up add:\", status)\n",
" # Generating after a FAILED or unfinished add bills for a profile built from the OLD\n",
" # memories only, so stop instead.\n",
" if status != \"SUCCEEDED\":\n",
" raise RuntimeError(f\"follow-up add did not succeed (status={status}); not regenerating\")\n",
" time.sleep(15) # let extraction settle\n",
"\n",
" print(json.dumps(client.generate_profile(USER_ID), indent=2))\n",
@@ -697,7 +676,7 @@
"execution_count": null,
"metadata": {},
"outputs": [],
"source": "# Restore the project's profile settings to the start-of-run snapshot in `finally`, so a\n# failed delete still leaves a shared project as we found it. Passing the original values\n# (including None) clears anything this notebook set: the SDK treats an explicit None as\n# \"clear\" and an omitted argument as \"unchanged\".\ntry:\n # `fresh` only exists if the error-handling section ran.\n for entity_id in (USER_ID, globals().get(\"fresh\")):\n if entity_id is None:\n continue\n r = client.client.delete(f\"/v2/entities/user/{entity_id}/\")\n print(entity_id, \"->\", r.status_code)\nfinally:\n _user = ORIGINAL_SETTINGS.get(\"entities\", {}).get(\"user\", {})\n client.update_profile_settings(\n enabled=ORIGINAL_SETTINGS.get(\"enabled\", False),\n schema=_user.get(\"schema\"),\n custom_instructions=_user.get(\"custom_instructions\"),\n )\n print(\"profile settings restored to the pre-notebook snapshot\")"
"source": "for entity_id in (USER_ID, fresh):\n r = client.client.delete(f\"/v2/entities/user/{entity_id}/\")\n print(entity_id, \"->\", r.status_code)\n\n# Restore the project's profile settings to the start-of-run snapshot, so a shared\n# env is left exactly as we found it. Passing the original values (including None)\n# clears anything this notebook set — the SDK distinguishes an explicit None from an\n# omitted argument.\n_user = ORIGINAL_SETTINGS.get(\"entities\", {}).get(\"user\", {})\nclient.update_profile_settings(\n enabled=ORIGINAL_SETTINGS.get(\"enabled\", False),\n schema=_user.get(\"schema\"),\n custom_instructions=_user.get(\"custom_instructions\"),\n)\nprint(\"profile settings restored to the pre-notebook snapshot\")"
},
{
"cell_type": "markdown",
-44
View File
@@ -6,7 +6,6 @@ Agent and editor integrations. Most packages are self-contained; coding-agent pl
|-----------|---------|-------|------|------|
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` | tsup (CJS+ESM) | ESLint + Prettier | jest + vitest (edge/node) |
| `openclaw/` | `@mem0/openclaw-mem0` | tsup (ESM) | none | vitest |
| `hermes-plugin-mem0/` | Standalone Hermes memory provider | none | ruff + isort | pytest (host-stubbed offline); live CLI/Desktop validation |
| `agent-plugin-core/` | Shared Python/TypeScript behavior, skill templates, builds, and conformance | Python build script | ruff + tsc | pytest + node:test |
| `mem0-agent-plugin/` | One portable Agent Plugins v1 package | Python | ruff | shared conformance |
| `claude-code-plugin/`, `cursor-plugin/`, `codex-plugin/`, `kimi-plugin/`, `antigravity-plugin/` | Self-contained native plugins generated from the shared Python core | Python | ruff | pytest |
@@ -50,48 +49,6 @@ Run the type check after every TypeScript change: `pnpm run typecheck` or `tsc -
- **`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).
- **`mem0-strands/`** is a native Strands `MemoryStore` (Python, published to PyPI as `mem0-strands`). 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 `mem0-strands/python/`.
## Surface attribution
Every integration tells the Mem0 platform which surface it is. Three headers,
and the rules on them are what keep one layer from erasing another:
| Header | Carries | Rule |
|--------|---------|------|
| `X-Mem0-Source` | one canonical source value | **set-once** — write only if absent |
| `X-Application` | the host app it runs inside | **set-once** — write only if absent |
| `X-Mem0-Client` | `name/version`, outermost first | **append-only** — add yourself, never replace |
Set-once means check-then-set, never assignment. An integration that wraps the
SDK is the outermost layer and sets the source; the SDK underneath defers to it.
Assignment is exactly how every agent plugin came to be indistinguishable from
every other one at the platform.
How to declare it from an integration, in order of preference:
1. Send the headers yourself, if you make the HTTP call directly.
2. Pass `source` in the call options, if you go through an SDK.
3. Set `MEM0_SOURCE` / `MEM0_APPLICATION` / `MEM0_CLIENT_STACK` in the
environment before constructing the client. The SDKs read these and defer to
anything already present.
Append-only applies where a stack can actually form: an SDK handed a client that
already carries `X-Mem0-Client` appends itself rather than replacing. An SDK
constructed with no outer context simply reports itself, which is correct — it
is the outermost layer in that process.
The backend recognizes a fixed list of source values and buckets everything else
into `OTHERS`. A new value has to land in the platform's `EventSource` enum, so
do not invent one without that change going in too.
`X-Application` is allowlisted the same way, and this one has a rule of its own:
**omit the header when you do not know the host.** A value outside the allowlist
is discarded server-side, so guessing produces an event that claims an
attribution we do not actually have. The portable bundle is the case that
matters. It runs in whatever editor a user drops it into, so its build leaves
`PLATFORM_APPLICATION` empty and `memory_core` sends no header at all, while the
native bundles each name the host they were generated for. If you add a build
target, decide which of those two it is.
## Adding an integration
1. For a native coding-agent host, add `integrations/<name>-plugin/` with `plugin-build.json`, its manifest, and a thin adapter, then generate its shared runtime. Portable clients use the single `mem0-agent-plugin/` package. Independent TypeScript integrations stay self-contained and import shared lifecycle behavior from `agent-plugin-core/typescript/`.
@@ -102,4 +59,3 @@ target, decide which of those two it is.
5. If it is a Claude Code or editor marketplace plugin, register the generated native bundle path in the applicable marketplace files. Preserve the existing public plugin name.
6. Document it under `docs/integrations/` and add the page to `docs/docs.json` and `docs/llms.txt`.
7. Add rows to the table above and to the CI/CD tables in [`../.github/AGENTS.md`](../.github/AGENTS.md).
8. Send the three headers in [Surface attribution](#surface-attribution), and land the matching `EventSource` value on the platform in the same week. Until it exists, your traffic reports as `OTHERS`.
+2 -2
View File
@@ -8,7 +8,7 @@ This directory is the single source of shared memory behavior for Mem0 coding-ag
integrations/
├── agent-plugin-core/ # Shared source; never installed as a plugin
│ ├── python/ # Claude-derived capture, recall, MCP, scoping, and telemetry
│ ├── typescript/ # Shared lifecycle, search prompts, formatting, identity, scoping, and telemetry
│ ├── typescript/ # Shared lifecycle, formatting, identity, scoping, and telemetry
│ ├── skills/ # The only source for the six generated memory skills
│ ├── build/ # Bundle builder, schemas, and validation
│ ├── conformance/ # One offline/live verification entry point
@@ -45,7 +45,7 @@ New Git repository writes use a hashed remote identity for shared `agent_id`. Se
Captured prompts and responses preserve their full text after secret redaction. Python extraction splits oversized input across requests without dropping message text. The session-end worker flushes the conversation already collected by hooks without adding the final answer again. Search queries, retrieved context, and tool evidence have separate limits.
TypeScript hosts reuse redaction, lifecycle utilities, and the search prompts in `typescript/src/prompts.ts`, which a test keeps identical to the Python core. They retain their own tools, scopes, and capture events. They do not inherit the Python `repo`/`dir`/`mine` contract or its background batching. OpenCode captures selected user prompts; Pi and DeepSeek capture completed conversation turns; OpenClaw selects recent messages and earlier summaries, then filters noise. Removing message-length truncation does not turn these integrations into complete transcript archives.
TypeScript hosts reuse redaction and lifecycle utilities but retain their own tools, scopes, and capture events. They do not inherit the Python `repo`/`dir`/`mine` contract or its background batching. OpenCode captures selected user prompts; Pi and DeepSeek capture completed conversation turns; OpenClaw selects recent messages and earlier summaries, then filters noise. Removing message-length truncation does not turn these integrations into complete transcript archives.
For installation, follow the host guides: [Claude Code](../../docs/integrations/claude-code.mdx), [Cursor](../../docs/integrations/cursor.mdx), [Codex](../../docs/integrations/codex.mdx), [Kimi](../../docs/integrations/kimi.mdx), and [Antigravity](../../docs/integrations/antigravity.mdx).
@@ -81,38 +81,6 @@ def replace_output(staged: Path, output: Path) -> Path:
return output
def _render_harness_id(host: str, *, portable: bool = False) -> str:
"""Emit core/_harness_id.py for one host.
Carries both vocabularies from a single definition: the PostHog `source` tag
and the platform's X-Mem0-Source / X-Application pair. Keeping them together
is what stops the two from drifting into separate vocabularies for the same
thing.
The portable bundle runs in whatever editor a user drops it into, so it does
not know its host and must not guess one. HARNESS_ID stays "coding-agent",
which is true and useful for grouping in PostHog, but PLATFORM_APPLICATION is
left empty: X-Application names a real host app, is checked against an
allowlist server-side, and a value that is always discarded is worse than no
value -- it reads like an attribution we have and do not.
"""
tag = host.upper().replace("-", "_") + "_PLUGIN"
application = "" if portable else host
return (
'"""Generated by integrations/agent-plugin-core/build/build.py. Do not edit."""\n'
"\n"
f'HARNESS_ID = "{host}"\n'
f'SOURCE_TAG = "{tag}"\n'
"\n"
"# Platform-side vocabulary (mem0_event.source + X-Application). The whole\n"
"# plugin family is one source; which editor it runs in is the application.\n"
"# An empty application means the host is unknown, and memory_core omits\n"
"# the header entirely rather than sending a placeholder.\n"
'PLATFORM_SOURCE = "MEM0_PLUGIN"\n'
f'PLATFORM_APPLICATION = "{application}"\n'
)
def _bundle_python(
staged: Path,
host: str,
@@ -128,12 +96,6 @@ def _bundle_python(
continue
shutil.copy2(source, core / source.name)
# Generated per host so identity does not depend on an entrypoint remembering
# to call telemetry.init(). mcp_server.py and the detached telemetry.py sender
# never did, which is how MCP searches reported harness=generic and every
# batch they drained was labelled MEM0_PLUGIN regardless of the real host.
(core / "_harness_id.py").write_text(_render_harness_id(host, portable=portable), encoding="utf-8")
values = {
"PLUGIN_ROOT": plugin_root,
"PLUGIN_DATA": "${PLUGIN_DATA}",
@@ -290,11 +290,6 @@ def run(
if args.plugin_data_dir:
os.environ[data_dir_env] = args.plugin_data_dir
# Snapshot BEFORE anything writes to the data dir: cache_plugin_api_key
# writes `api-key` and EvidenceStore creates `evidence.sqlite3`, so asking
# after them always saw content and every fresh install reported an upgrade.
data_dir_was_empty = telemetry.data_dir_was_empty()
cache_plugin_api_key()
if args.action == "session-start":
clear_stale_api_key_cache()
@@ -310,19 +305,8 @@ def run(
return 0
if args.action == "session-start":
# Claims the marker atomically and says which event to record, so a
# second session starting alongside this one cannot record it too.
first_event = telemetry.claim_install(was_empty=data_dir_was_empty)
if first_event == "install":
if telemetry.is_first_run():
telemetry.record("install")
elif first_event == "upgrade":
# First run after a build that never wrote the marker; the
# predecessor version was never recorded anywhere.
telemetry.record("upgrade", from_version="pre-0.3")
else:
previous = telemetry.claim_version_change()
if previous:
telemetry.record("upgrade", from_version=previous)
recovered = recover_pending_handoffs()
record_session_start(store, hook_input)
if recovered:
@@ -21,9 +21,16 @@ from memory_core import (
PROTOCOL_VERSION = "2024-11-05"
TOOL_NAME = "search_memories"
TOOL_DESCRIPTION = (
"Search memories from earlier work in this repository. Use it before "
"repeating investigation or when earlier decisions, fixes, commands, or "
"results may help."
"Search memories from earlier work in this repository. ALWAYS call this "
"tool before answering anything that could depend on prior context: the "
"user's preferences, facts about this codebase, history, people, projects, "
"or earlier decisions. Do not rely on the chat window alone. The "
"repository's memory is shared by everyone who works in it and includes "
"what it took to run, test, or build here, so search before assuming an "
"invocation works. The scope argument changes what is searched: 'repo' "
"(default) is the whole repository's shared memory plus your own "
"preferences, 'dir' narrows the shared part to the directory you are "
"working in, and 'mine' is your preferences alone."
)
TOOL_SCHEMA = {
"type": "object",
@@ -29,7 +29,7 @@ from typing import Any, Iterable
import telemetry
DEFAULT_API_URL = "https://api.mem0.ai"
PLUGIN_VERSION = "0.3.3"
PLUGIN_VERSION = "0.3.1"
_harness_name: str = "generic"
_harness_env_prefix: str = "MEM0_PLUGIN"
@@ -71,13 +71,15 @@ MAX_FLUSH_ATTEMPTS = 5
FORGET_PAGE_SIZE = 100
FORGET_MAX_PAGES = 50
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help with future coding work.
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help anyone with future coding work in this repository.
A completed change should produce one memory explaining the resulting behavior, where it is implemented when useful, and any important constraints or reasoning. Exploration or accepted decisions may produce separate memories only when they are independently useful.
Use the coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
A command that failed and was then made to work should produce one memory naming the failing invocation, the error it returned, and the invocation that succeeded. Do not save one-off errors caused by an edit still in progress, transient network failures, or anything a rerun would fix on its own.
Write about the repository, not the user, assistant, session, or task. Do not include test results, documentation updates, release notes, or temporary state.
Use the current coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
Write about the repository, not the user, assistant, session, or task. Do not save personal preferences. Do not save a memory that only states which repository, branch, or directory the session worked in. Do not include test results, documentation updates, release notes, or temporary state.
If nothing useful was established, return no memories."""
@@ -1798,34 +1800,6 @@ def extraction_message_batches(
return batches
# Platform surface attribution. Read from the generated per-host module so a new
# entrypoint is correct without remembering to configure anything.
try: # pragma: no cover - absent only in the un-built shared source tree
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
except ImportError:
_PLATFORM_SOURCE = "MEM0_PLUGIN"
_PLATFORM_APPLICATION = ""
def platform_headers(key: str) -> dict[str, str]:
"""Auth plus the three surface-identity headers.
X-Mem0-Source and X-Application are set-once by contract: this is the
outermost layer, so it sets them, and nothing below may overwrite them.
X-Mem0-Client is append-only — anything downstream adds itself to the tail.
"""
headers = {
"Authorization": f"Token {key}",
"Content-Type": "application/json",
"X-Mem0-Source": _PLATFORM_SOURCE,
"X-Mem0-Client": f"mem0-plugin/{PLUGIN_VERSION}",
}
if _PLATFORM_APPLICATION:
headers["X-Application"] = _PLATFORM_APPLICATION
return headers
def _request_json(
url: str, key: str, payload: dict[str, Any], timeout: float
) -> tuple[dict[str, Any] | list[Any], int, int]:
@@ -1833,7 +1807,7 @@ def _request_json(
request = urllib.request.Request(
url,
data=raw,
headers=platform_headers(key),
headers={"Authorization": f"Token {key}", "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
@@ -1860,7 +1834,7 @@ def _get_json(
) -> tuple[dict[str, Any] | list[Any], int]:
request = urllib.request.Request(
url,
headers=platform_headers(key),
headers={"Authorization": f"Token {key}", "Content-Type": "application/json"},
method="GET",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
@@ -2006,13 +1980,6 @@ def flush_session(
"user_id": write_user,
"app_id": repo.app_id,
"run_id": session_id,
# Top level, not metadata: the backend reads `source` from the body or
# the query string, never from metadata, which is where this used to
# sit. The X-Mem0-Source header is also read, but only from the
# platform release that ships alongside this change, so the body value
# is what makes attribution work on both. The harness tag stays in
# metadata as hook provenance.
"source": _PLATFORM_SOURCE,
"metadata": {**metadata, "author": write_user, "dirs": directory_chain(repo)},
"agent_custom_instructions": PROJECT_MEMORY_INSTRUCTIONS,
"custom_instructions": PERSONAL_MEMORY_INSTRUCTIONS,
@@ -2556,7 +2523,7 @@ def _collect_memory_ids(
def _delete_memory(api_url: str, key: str, memory_id: str) -> bool:
request = urllib.request.Request(
f"{api_url}/v1/memories/{urllib.parse.quote(memory_id)}/",
headers=platform_headers(key),
headers={"Authorization": f"Token {key}", "Content-Type": "application/json"},
method="DELETE",
)
try:
@@ -1,9 +1,5 @@
#!/usr/bin/env python3
"""Usage telemetry for Mem0 agent plugins.
Events are linked to your Mem0 account email when an API key is configured, and
to a random per-machine id otherwise. Not anonymous — the Python SDK and CLI
attribute the same way.
"""Anonymous usage telemetry for Mem0 agent plugins.
Hooks run on a 3-6 second budget and fire on every tool call, so recording never
touches the network: `record` appends one JSON line to a local spool and returns.
@@ -13,8 +9,7 @@ started once per session and again from the flush worker that is already detache
Pure stdlib, matching the rest of the plugin. Opt out with MEM0_TELEMETRY=false.
Never sends prompts, memory text, queries, file paths, repository names, or API
keys: only event names, durations, counts, coarse outcomes, and repo/session
identifiers hashed with a random per-install salt.
keys: only event names, durations, counts, coarse outcomes, and salted hashes.
"""
from __future__ import annotations
@@ -34,24 +29,8 @@ from typing import Any
import memory_core
# Seeded from the per-host module the build generates into core/. Two processes
# in this pipeline never call init() — mcp_server.py, and the detached
# `python3 telemetry.py` sender that spawn_flush() starts — so a module default
# was what every one of their events got labelled with.
try: # pragma: no cover - absent only in the un-built shared source tree
from _harness_id import HARNESS_ID as _DEFAULT_HARNESS
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
from _harness_id import SOURCE_TAG as _DEFAULT_SOURCE_TAG
except ImportError:
_DEFAULT_HARNESS = "generic"
_DEFAULT_SOURCE_TAG = "MEM0_PLUGIN"
_PLATFORM_SOURCE = "MEM0_PLUGIN"
_PLATFORM_APPLICATION = ""
_salt_cache: str = ""
_harness: str = _DEFAULT_HARNESS
_source_tag: str = _DEFAULT_SOURCE_TAG
_harness: str = "generic"
_source_tag: str = "MEM0_PLUGIN"
_PRIVATE_KEYS = {
"apikey",
"authorization",
@@ -77,19 +56,10 @@ _PRIVATE_KEYS = {
}
def init(harness: str = "", source_tag: str = "") -> None:
"""Override the generated identity. Optional — core/_harness_id.py is the default.
The fallback shape matches memory_core.configure_harness's (``<HOST>_PLUGIN``).
It used to be ``MEM0_<HOST>_PLUGIN`` here and ``<host>_plugin`` there, which
meant one plugin could emit three different source values depending on which
process happened to send the batch.
"""
def init(harness: str = "generic", source_tag: str = "") -> None:
global _harness, _source_tag
_harness = harness or _DEFAULT_HARNESS
_source_tag = source_tag or (
f"{_harness.upper().replace('-', '_')}_PLUGIN" if harness else _DEFAULT_SOURCE_TAG
)
_harness = harness
_source_tag = source_tag or f"MEM0_{harness.upper().replace('-', '_')}_PLUGIN"
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
POSTHOG_CAPTURE_URL = "https://us.i.posthog.com/i/v0/e/"
@@ -100,16 +70,6 @@ BATCH_SIZE = 100
SEND_TIMEOUT = 5
CLAIM_STALE_SECONDS = 120
CLAIM_EXPIRY_SECONDS = 7 * 24 * 60 * 60
# A batch is only discarded once it has genuinely been retried this many times.
MAX_CLAIM_ATTEMPTS = 3
# Parked claims drained per run, after the live spool. Bounded so a long backlog
# cannot turn one flush into an unbounded send loop.
MAX_PARKED_PER_RUN = 3
# Added to the wait before a released claim becomes reclaimable, per attempt
# already spent. Releasing straight to "reclaimable now" let two senders burn the
# whole budget within seconds of one another on a single momentary failure, and
# discard a batch a retry a minute later would have delivered.
RETRY_COOLDOWN_SECONDS = 60
def is_enabled() -> bool:
@@ -123,126 +83,9 @@ def is_enabled() -> bool:
def _digest(value: str, length: int = 16) -> str:
"""Unsalted digest. Only for values that are already secrets (API keys)."""
return hashlib.sha256(value.encode("utf-8")).hexdigest()[:length]
def _salt_path() -> Path:
return memory_core.data_dir() / "telemetry-salt"
def _install_salt() -> str:
"""Random per-install salt, created once and memoized for the process.
Deliberately its own file, claimed with O_CREAT|O_EXCL, rather than a key in
the identity file. Three reasons, all of which produced wrong data when this
lived in the identity dict:
- Hooks are short-lived separate processes firing on every tool call, and
people run more than one agent window. A read-modify-write would let each
process mint its own salt, so one repository would hash several ways in the
window before a writer won.
- resolve_distinct_id holds a copy of the identity dict across a network call
to /v1/ping/, so whichever write landed second erased the other's key —
losing either the salt (repo_hash changes mid-stream) or the email (a
second $identify, splitting the person).
- Touching the identity file from record() would create it, and is_first_run
keys off that file, so recording an event would silently suppress the
install event.
Published atomically, and there is deliberately no derived fallback. Creating
the file with O_CREAT|O_EXCL and then writing into it leaves a window where
the file exists and is empty, and a concurrent hook that reads it in that
window gets nothing. Falling back to a digest of the path would hand that
process a salt an attacker can compute, memoized for its whole run, which is
the privacy control this function exists to provide silently turning itself
off under load. The salt is written to a private temp file first and linked
into place, so the name either does not exist or already has the full value.
Returns "" when it genuinely cannot persist. Callers omit the hash entirely
rather than emit an unsalted one.
"""
global _salt_cache
if _salt_cache:
return _salt_cache
path = _salt_path()
# Read before writing. Hooks are separate processes firing on every tool
# call, so all but the first find the salt already published; going straight
# to create-fsync-link-unlink meant every one of them paid an fsync to
# discover that, on a path whose whole promise is appending a line and
# returning.
try:
_salt_cache = path.read_text(encoding="utf-8").strip()
if _salt_cache:
return _salt_cache
except OSError:
pass
temporary = path.with_name(f"{path.name}.{os.getpid()}.tmp")
try:
path.parent.mkdir(parents=True, exist_ok=True)
handle = os.open(temporary, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
with os.fdopen(handle, "w", encoding="utf-8") as stream:
stream.write(uuid.uuid4().hex)
stream.flush()
os.fsync(stream.fileno())
try:
# Atomic claim: fails if another process already published one.
# os.link rather than replace, which would clobber theirs.
os.link(temporary, path)
except FileExistsError:
pass
except OSError:
# No hardlinks here (some network mounts, some container volumes).
# Claim the name directly instead. That reopens the empty-file
# window, but the window is now benign: a reader that lands in it
# gets "" and omits the hash for that process rather than caching a
# guessable one. Losing the hashes on every run of an entire
# filesystem is the worse failure.
try:
fallback = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
with os.fdopen(fallback, "w", encoding="utf-8") as stream:
stream.write(temporary.read_text(encoding="utf-8"))
except OSError:
pass
except OSError:
pass
finally:
try:
temporary.unlink()
except OSError:
pass
try:
_salt_cache = path.read_text(encoding="utf-8").strip()
except OSError:
_salt_cache = ""
return _salt_cache
def _scoped_digest(value: str, length: int = 16) -> str:
"""Salted digest for values drawn from a guessable space.
repo.identity is a git remote URL, or ``local:<absolute path>`` when there is
no remote — which normally contains the account username. Sixteen unsalted
hex characters over that input space is enumerable, so this is not a
privacy control without the salt. Salting per install keeps every
within-account join the analytics actually use and gives up only
cross-machine joins on the same repository, which nothing computes.
Returns "" when there is no salt, so record() omits the property. An
unsalted digest over this input space is close to plaintext, and emitting one
under a name that implies it is hashed is worse than sending nothing.
"""
if not value:
return ""
salt = _install_salt()
if not salt:
return ""
return hashlib.sha256(f"{salt}:{value}".encode("utf-8")).hexdigest()[:length]
def _safe_value(value: Any) -> Any:
if isinstance(value, str):
return memory_core.redact(value)
@@ -302,176 +145,9 @@ def anonymous_id(identity: dict[str, str] | None = None) -> str:
return created
def _rotate_anonymous_id(identity: dict[str, str]) -> str:
"""Mint a fresh anonymous id because the account context is gone.
The previous id may already have been merged into a person profile by an
$identify, and that merge is permanent. Reusing it after a logout or a key
change attributes everything that follows to the account that just went
away, which is the same misattribution the key fingerprint exists to stop,
only arriving through the anonymous path instead.
`aliased` is cleared with it: the new id has never been merged, so it is
eligible to be aliased into whatever account comes next.
"""
created = f"code-anon-{uuid.uuid4().hex}"
identity["anonymous_id"] = created
identity.pop("aliased", None)
_write_identity(identity)
return created
def _install_state_path() -> Path:
return memory_core.data_dir() / "install-state.json"
def is_first_run() -> bool:
"""Whether install has never been recorded on this machine.
Deliberately NOT the identity file. That file is only written by a
successful flush, so an offline or firewalled user recorded code.install on
every single session, forever — and every 0.2.x user recorded one on their
first 0.3.x session because 0.2.x never wrote it at all.
"""
return not _install_state_path().exists()
def data_dir_was_empty() -> bool:
"""Whether the data directory is untouched. Call BEFORE anything writes to it.
hook_runner reaches claim_install() only after cache_plugin_api_key() has
written `api-key` and EvidenceStore() has created `evidence.sqlite3`, so
asking at claim time always saw content and every fresh install reported an
upgrade. The caller snapshots this at the top of the run instead.
"""
return not _data_dir_has_content()
def claim_install(was_empty: bool | None = None) -> str | None:
"""Claim the one install/upgrade record for this machine, atomically.
Returns the event to record ("install" or "upgrade"), or None if another
session already claimed it. O_CREAT|O_EXCL so two sessions starting together
cannot both win.
`was_empty` must come from data_dir_was_empty() called before this process
wrote anything. Omitting it falls back to checking now, which is only
correct for a caller that has touched nothing.
"""
if not is_enabled():
# Never consume the one-shot claim while the user is opted out, or they
# would silently lose their install event if they later opt in.
return None
path = _install_state_path()
upgrading = not (data_dir_was_empty() if was_empty is None else was_empty)
try:
path.parent.mkdir(parents=True, exist_ok=True)
handle = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError:
return None
except OSError:
return None
try:
with os.fdopen(handle, "w", encoding="utf-8") as stream:
json.dump(
{
"plugin_version": memory_core.PLUGIN_VERSION,
"installed_at": memory_core.utc_now(),
"upgraded": upgrading,
},
stream,
)
# Durable before this returns. The O_EXCL open is what makes the
# claim exclusive, so it cannot be replaced by a temp-and-rename
# without losing that, which leaves the content as the thing to make
# safe. A kill between the open and this fsync used to leave a marker
# that exists but parses to nothing: is_first_run reads it as claimed
# and claim_version_change cannot read a version out of it.
stream.flush()
os.fsync(stream.fileno())
except OSError:
pass
return "upgrade" if upgrading else "install"
def _data_dir_has_content() -> bool:
"""Whether anything predates this session in the plugin data directory."""
try:
for entry in memory_core.data_dir().iterdir():
if entry.name != "install-state.json":
return True
except OSError:
pass
return False
def _repair_install_state(path: Path) -> None:
"""Rewrite an unparseable marker so version tracking can resume."""
try:
temporary = path.with_suffix(f".{os.getpid()}.tmp")
temporary.write_text(
json.dumps({"plugin_version": memory_core.PLUGIN_VERSION, "repaired_at": memory_core.utc_now()}),
encoding="utf-8",
)
temporary.replace(path)
except OSError:
pass
def claim_version_change() -> str | None:
"""Return the previously recorded version if it differs, updating the marker.
Only meaningful once the marker exists — the first transition into 0.3.x has
no recorded predecessor and reports "pre-0.3" instead. Claiming by rewriting
the marker means the next session sees no change and records nothing.
"""
path = _install_state_path()
try:
state = json.loads(path.read_text(encoding="utf-8"))
except OSError:
return None
except json.JSONDecodeError:
# A crash between O_EXCL and the write leaves an empty marker. Left
# alone it disables every future upgrade event on this machine, because
# claim_install sees the file and this function cannot parse it.
state = None
if not isinstance(state, dict):
_repair_install_state(path)
return None
previous = str(state.get("plugin_version") or "")
if not previous or previous == memory_core.PLUGIN_VERSION:
return None
# Claim the transition with an exclusive sentinel before rewriting the
# marker. A plain read-modify-write let every concurrently starting session
# observe the old version and each record its own upgrade — and the first
# session after a version bump is exactly when several agent windows restart
# together.
sentinel = path.with_name(f"upgraded-{memory_core.PLUGIN_VERSION}")
try:
os.close(os.open(sentinel, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600))
except FileExistsError:
return None
except OSError:
return None
state["plugin_version"] = memory_core.PLUGIN_VERSION
state["upgraded_at"] = memory_core.utc_now()
temporary = path.with_suffix(f".{os.getpid()}.tmp")
try:
temporary.write_text(json.dumps(state), encoding="utf-8")
temporary.replace(path)
except OSError:
# Release the claim. The marker still records the old version, so
# without this the sentinel makes claim_version_change return early on
# every later run and this version's upgrade is never recorded again.
for leftover in (sentinel, temporary):
try:
leftover.unlink()
except OSError:
pass
return None
return previous
"""Whether this machine has never recorded a plugin event before."""
return not _identity_path().exists()
def record(
@@ -492,32 +168,19 @@ def record(
except OSError:
pass
properties = _safe_value(properties)
# Stamped in the RECORDING process, beside harness. `source` used to be
# read in the sending process from a module global, so whichever process
# drained the spool named every event in it. flush() spreads per-event
# properties last, so this now wins over any sender's default.
properties.update(
harness=_harness,
source=_source_tag,
plugin_version=memory_core.PLUGIN_VERSION,
os=sys.platform,
python_version=platform.python_version(),
)
# Assigned only when the digest is real. _scoped_digest returns "" when
# the salt could not be persisted, and an empty property is worse than an
# absent one: it survives the None filter below and reads as a value.
if repo is not None:
repo_hash = _scoped_digest(getattr(repo, "identity", ""))
if repo_hash:
properties["repo_hash"] = repo_hash
properties["repo_hash"] = _digest(getattr(repo, "identity", ""))
if session_id:
session_hash = _scoped_digest(session_id)
if session_hash:
properties["session_hash"] = session_hash
properties["session_hash"] = _digest(session_id)
line = json.dumps(
{
"event": f"{EVENT_PREFIX}.{event}",
"uuid": str(uuid.uuid4()),
"timestamp": memory_core.utc_now(),
"properties": {
key: value for key, value in properties.items() if value is not None
@@ -576,201 +239,38 @@ def spawn_flush() -> bool:
return False
def _claim_name(attempt: int = 0) -> str:
"""Claim filename. The attempt count rides in the name so the 7-day expiry
only ever discards a batch that was actually retried and failed."""
return f"telemetry-{os.getpid()}-{uuid.uuid4().hex[:8]}-a{attempt}.sending"
def _claim_attempt(claim: Path) -> int:
"""Attempts recorded in a claim filename; 0 for the pre-attempt-count shape.
Anchored on field position, not on a leading "a": the legacy shape is
``telemetry-<pid>-<hex>.sending`` and a hex id such as ``a1234567`` would
otherwise parse as attempt 1234567 and be discarded unsent on the first
flush after an upgrade.
"""
stem = claim.name[: -len(".sending")] if claim.name.endswith(".sending") else claim.name
parts = stem.split("-")
if len(parts) != 4:
return 0
tail = parts[3]
if tail.startswith("a") and tail[1:].isdigit():
return int(tail[1:])
return 0
def _touch(path: Path) -> None:
"""Refresh mtime so a claim's age measures time since it was claimed.
``Path.replace`` is ``os.rename``, which preserves mtime — so a claim created
after a quiet minute inherited the spool's last-write time and looked
abandoned the instant it was made. A second sender would then take it over
while the first was still posting, and both would deliver the batch.
"""
try:
os.utime(path, None)
except OSError:
pass
def _claim_spool() -> Path | None:
"""Rename the spool aside so exactly one sender owns each batch."""
directory = memory_core.data_dir()
claim = directory / _claim_name()
claim = directory / f"telemetry-{os.getpid()}-{uuid.uuid4().hex[:8]}.sending"
spool = _spool_path()
try:
spool.replace(claim)
_touch(claim)
return claim
except OSError:
pass
return _claim_parked(directory)
def _sweep_debris(directory: Path) -> None:
"""Remove files nothing else will ever pick up again.
*.partial is a temp file orphaned by a crash between write and rename.
*.corrupt is a batch quarantined for undecodable content. No glob in this
module matches either, so without this they accumulate on disk for the life
of the install.
Quarantined batches are kept far longer than debris: they are the only
evidence left of events that could not be delivered, and someone diagnosing
a report of missing telemetry has to be able to find one.
"""
now = time.time()
for debris in directory.glob("telemetry-*.partial"):
try:
if now - debris.stat().st_mtime > CLAIM_STALE_SECONDS:
debris.unlink()
except OSError:
continue
for quarantined in directory.glob("telemetry-*.corrupt"):
try:
if now - quarantined.stat().st_mtime > CLAIM_EXPIRY_SECONDS:
quarantined.unlink()
except OSError:
continue
# The same reasoning covers *.tmp. _write_identity and _install_salt both
# create one and unlink it in a finally, which a SIGKILL skips, and no glob
# in this module matches the leftovers either.
for temporary in directory.glob("telemetry-*.tmp"):
try:
if now - temporary.stat().st_mtime > CLAIM_STALE_SECONDS:
temporary.unlink()
except OSError:
continue
def _claim_parked(directory: Path) -> Path | None:
"""Take the oldest abandoned claim, if any lease has actually expired.
Kept separate from the live spool so flush() can drain both in one run.
Previously parked batches were only reachable when no spool existed at all,
and because sessions keep recording there usually was one — so a batch
parked by a failed send waited until the 7-day expiry deleted it unsent,
even though its own presence is what started the sender.
"""
now = time.time()
for orphan in sorted(directory.glob("telemetry-*.sending"), key=_safe_mtime):
for orphan in sorted(directory.glob("telemetry-*.sending")):
try:
age = now - orphan.stat().st_mtime
except OSError:
continue
if age < CLAIM_STALE_SECONDS:
# Someone else holds a live lease on it. This check has to come
# first. Claiming a file bumps its attempt count and refreshes its
# mtime, so a sender that has just taken the final attempt looks
# exhausted to everyone else while it is actively draining. Judging
# exhaustion before liveness let a second sender unlink a batch out
# from under its owner, losing every event in it.
continue
# Attempts, not age. Every re-claim touches the mtime and every release
# backdates it by a fixed amount, so age is pinned near the stale
# threshold and never reaches the expiry. Age stays only as a backstop
# for files that never carried an attempt marker.
if _claim_attempt(orphan) >= MAX_CLAIM_ATTEMPTS or age > CLAIM_EXPIRY_SECONDS:
if age > CLAIM_EXPIRY_SECONDS:
try:
orphan.unlink()
except OSError:
pass
continue
claim = orphan.parent / _claim_name(_claim_attempt(orphan) + 1)
if age < CLAIM_STALE_SECONDS:
continue
try:
orphan.replace(claim)
_touch(claim)
return claim
except OSError:
continue
return None
def _safe_mtime(path: Path) -> float:
try:
return path.stat().st_mtime
except OSError:
return 0.0
def _rewrite_claim(claim: Path, remaining: list[dict[str, Any]]) -> bool:
"""Persist the unsent remainder, atomically, and refresh the lease.
Called after every successful batch. Two jobs: a retry resumes where the
send stopped instead of re-posting from the top, and the rewrite doubles as
the lease heartbeat, so a slow sender does not have its claim stolen
mid-flight. Interval is one batch, well inside CLAIM_STALE_SECONDS.
"""
if not remaining:
try:
claim.unlink()
except OSError:
pass
return True
temporary = claim.with_suffix(f".{os.getpid()}.partial")
try:
payload = "".join(json.dumps(event, separators=(",", ":"), default=str) + "\n" for event in remaining)
# fsync before the rename: without it the rename can land while the
# bytes have not, and the claim comes back empty or truncated after a
# crash. _drain then reads zero events and unlinks it.
with open(temporary, "w", encoding="utf-8") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
temporary.replace(claim)
_touch(claim)
return True
except OSError:
try:
temporary.unlink()
except OSError:
pass
return False
def _release_claim(claim: Path, remaining: list[dict[str, Any]]) -> None:
"""Persist the remainder and drop the lease, because this sender has given up.
Distinct from the per-batch heartbeat: heartbeating on the way out would
make an abandoned batch look actively owned for a further
CLAIM_STALE_SECONDS, delaying the retry for no reason. Ageing it past the
threshold lets the next flush pick it up immediately, while the attempt
count in the filename still bounds how many times that can happen.
"""
if not _rewrite_claim(claim, remaining):
return
try:
# Backdate past the stale threshold so the next flush can pick it up,
# minus a cooldown that grows with the attempts already spent. Clamped so
# the mtime never lands in the future, which would read as a live lease.
cooldown = min(_claim_attempt(claim) * RETRY_COOLDOWN_SECONDS, CLAIM_STALE_SECONDS)
released = time.time() - CLAIM_STALE_SECONDS - 1 + cooldown
os.utime(claim, (released, released))
except OSError:
pass
def _resolve_email(key: str) -> str:
"""Trade the API key for the account email so events join other Mem0 surfaces."""
url = os.environ.get("MEM0_API_URL", memory_core.DEFAULT_API_URL).rstrip("/") + "/v1/ping/"
@@ -800,130 +300,34 @@ def _post(payload: dict[str, Any], url: str) -> bool:
def resolve_distinct_id() -> tuple[str, str]:
"""Return the PostHog distinct id and the anonymous id it replaced, if any.
The second value becomes a PostHog $identify alias. It is ONLY ever an
anonymous id: aliasing one account email to another merges two real person
profiles and cannot be undone, so a key that now belongs to a different
account re-resolves with no alias.
"""
"""Return the PostHog distinct id and the anonymous id it replaced, if any."""
identity = _read_identity()
key = memory_core.api_key()
fingerprint = _digest(key) if key else ""
email = identity.get("email", "")
if email and fingerprint:
recorded = identity.get("key_fingerprint", "")
if recorded == fingerprint:
return email, ""
if not recorded:
# Rows written before fingerprints existed. Verify rather than
# adopt: a key changed before the upgrade would otherwise bind the
# new key to the previous account's email, permanently, and the
# fingerprint would then agree with itself forever after.
verified = _resolve_email(key)
if not verified:
# Offline, firewalled, or the API is down. Keep the previous
# behaviour and retry on the next flush rather than dropping a
# real account attribution. Safe because the same network that
# failed /v1/ping/ is about to fail the PostHog POST, so nothing
# is delivered under the unverified identity in the meantime.
return email, ""
identity["email"] = verified
identity["key_fingerprint"] = fingerprint
_write_identity(identity)
return verified, ""
if email:
return email, ""
key = memory_core.api_key()
if not key:
# No key to verify the account with; do not keep attributing to it.
if email:
identity.pop("email", None)
identity.pop("key_fingerprint", None)
return _rotate_anonymous_id(identity), ""
return anonymous_id(identity), ""
resolved = _resolve_email(key)
if not resolved:
# The key changed and will not resolve (revoked, offline, API down).
# Reaching here with an email means the recorded fingerprint disagreed,
# so the key really did change. Drop the account and rotate: the stored
# anonymous id may already be merged into that account's person, and
# reusing it would keep the events on the profile we are trying to
# leave.
if email:
identity.pop("email", None)
identity.pop("key_fingerprint", None)
return _rotate_anonymous_id(identity), ""
email = _resolve_email(key)
if not email:
return anonymous_id(identity), ""
# Alias only when going anonymous -> email for the first time. Once an anon
# id has been merged into an account it must never be offered again: an
# alias naming an already-identified id is what could link two real people.
previous = "" if (email or identity.get("aliased")) else identity.get("anonymous_id", "")
if previous:
identity["aliased"] = True
identity["email"] = resolved
identity["key_fingerprint"] = fingerprint
previous = identity.get("anonymous_id", "")
identity["email"] = email
_write_identity(identity)
return resolved, previous
return email, previous
def flush() -> int:
"""Drain the live spool, then any parked claims, and return events sent."""
"""Drain claimed spools to PostHog and return the number of events sent."""
if not is_enabled():
return 0
sent, delivered = _drain(_claim_spool())
if not delivered:
# The network is failing. Retrying other batches now would only burn
# their attempt budget against the same broken connection.
return sent
# Parked batches used to starve behind the live spool indefinitely. Bounded
# per run so a long backlog cannot turn one flush into an unbounded loop.
directory = memory_core.data_dir()
_sweep_debris(directory)
for _ in range(MAX_PARKED_PER_RUN):
parked = _claim_parked(directory)
if parked is None:
break
count, delivered = _drain(parked)
sent += count
if not delivered:
break
return sent
def _drain(claim: Path | None) -> tuple[int, bool]:
"""Post one claimed batch file, recording progress after every batch.
Returns (events sent, whether everything was delivered).
"""
claim = _claim_spool()
if claim is None:
return 0, True
return 0
try:
lines = claim.read_text(encoding="utf-8").splitlines()
except ValueError:
# UnicodeDecodeError from a torn write: the content is unrecoverable, so
# quarantine rather than retry. flush() runs from a bare `finally:` in
# flush_worker, so raising here also skips the handoff cleanup, and an
# undecodable file would otherwise be re-read on every flush forever.
# Reported as delivered because there is nothing left to deliver and the
# rest of the run should continue.
try:
claim.replace(claim.with_suffix(".corrupt"))
except OSError:
try:
claim.unlink()
except OSError:
pass
return 0, True
except OSError:
# Could not read it, which is not the same as having nothing to send.
# The file is left exactly where it is: a vanished or briefly unreadable
# claim is retryable, and quarantining it here would discard events over
# a transient filesystem error. Reported as undelivered so the run stops
# instead of counting a batch nothing was posted from as delivered.
return 0, False
return 0
events = []
for line in lines:
try:
@@ -933,18 +337,11 @@ def _drain(claim: Path | None) -> tuple[int, bool]:
if isinstance(value, dict) and value.get("event"):
events.append(value)
if not events:
# Only delete when the file really is empty. A non-empty file that
# parses to nothing is a torn write, and its contents are the unsent
# remainder — deleting it is the data loss this PR exists to prevent.
try:
empty = claim.stat().st_size == 0
except OSError:
empty = True
try:
claim.replace(claim.with_suffix(".corrupt")) if not empty else claim.unlink()
claim.unlink()
except OSError:
pass
return 0, True
return 0
distinct_id, aliased_anonymous_id = resolve_distinct_id()
if aliased_anonymous_id:
@@ -963,17 +360,12 @@ def _drain(claim: Path | None) -> tuple[int, bool]:
sent = 0
for start in range(0, len(events), BATCH_SIZE):
chunk = events[start : start + BATCH_SIZE]
batch = [
{
"event": event["event"],
"distinct_id": distinct_id,
# Carried through from record() so a resend can be collapsed.
"uuid": event.get("uuid"),
"timestamp": event.get("timestamp"),
"properties": {
# Fallback only: events recorded by a build before source
# moved into record() have none of their own.
"source": _source_tag,
"language": "python",
"$process_person_profile": False,
@@ -981,24 +373,16 @@ def _drain(claim: Path | None) -> tuple[int, bool]:
**(event.get("properties") or {}),
},
}
for event in chunk
for event in events[start : start + BATCH_SIZE]
]
if not _post({"api_key": POSTHOG_API_KEY, "batch": batch}, POSTHOG_BATCH_URL):
# Keep only what has not been delivered, and release the lease.
# Previously the whole file was kept and the retry re-posted every
# batch, including the ones that had already arrived.
_release_claim(claim, events[start:])
return sent, False
sent += len(chunk)
# Record progress and refresh the lease after each successful batch, so
# a crash repeats at most one batch instead of the entire file. If the
# rewrite fails the claim still holds delivered events, so stop rather
# than carry on as though progress were recorded — continuing is how the
# duplicate delivery this PR fixes would come back.
if not _rewrite_claim(claim, events[start + len(chunk) :]):
_release_claim(claim, events[start + len(chunk) :])
return sent, False
return sent, True
return sent
sent += len(batch)
try:
claim.unlink()
except OSError:
pass
return sent
def main() -> int:
@@ -7,8 +7,8 @@ disable-model-invocation: true
# Pause memory capture
To pause (hooks stop capturing and sending session content; a minimal
telemetry ping still fires at session start, under your Mem0 account email,
unless `MEM0_TELEMETRY=false`):
anonymous telemetry ping still fires at session start unless
`MEM0_TELEMETRY=false`):
```bash
python3 "{{PLUGIN_ROOT}}/core/memory_cli.py" --harness "{{HARNESS_ID}}" {{PLUGIN_DATA_ARG}} pause
@@ -12,9 +12,11 @@ Call `search_memories` with the user's question. Treat `--top-k`, `--category`,
query.
Omit `top_k` to use Mem0's configured default. Omit `category` to search every
category. Omit `scope` to use the configured default, normally `repo`: this
repository's shared memory, which everyone who works in it contributes to,
plus your own preferences.
category; a category is a best-effort label Mem0 assigned when it saved the
memory, so if a category search misses, repeat it without the category. Omit
`scope` to use the configured default, normally `repo`: this repository's
shared memory, which everyone who works in it contributes to, plus your own
preferences.
Pass `scope` when the question needs something else: `dir` to narrow the
shared memory to the directory you are working in (a package inside a

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