Memory for AI Agents
Article 07 of 11

How does Mem0 work?

4 min read · The 2026 rewrite: ADD-only, rich memories, entities, three-signal search.

In this blog, we will learn how Mem0, the most used open-source memory library, works under the hood. We will walk through the actual code in mem0/mem0/memory/main.py and see what changed between the 2025 paper and the 2026 rewrite.

The 2025 version, in one picture

The Mem0 paper (April 2025) described a two-phase pipeline.

extraction   summary + last 10 messages + new pair --> LLM --> facts
update       for each fact: top-10 similar memories --> LLM tool call
             --> ADD | UPDATE | DELETE | NOOP --> vector store

On LoCoMo it scored 66.9 with a judge model, against 72.9 for pasting the whole conversation, while using 7K tokens instead of 26K and answering in 1.4 s at p95 instead of 17 s. A graph variant (Mem0g) added entity and relation triples for 68.4.

Good numbers for the time, and the per-fact update call was the bottleneck: one extraction call plus one decision call per fact.

The 2026 rewrite

In April 2026 Mem0 shipped a new algorithm and reported LoCoMo 92.5, LongMemEval 94.4, and BEAM-1M 64.1 on its managed platform. The open-source code in this folder is that algorithm, minus some platform-only features. Four changes did the work.

1. ADD-only extraction

_add_to_vector_store is now labelled "V3 phased batch pipeline". Read it top to bottom:

Phase 0  fetch the last 10 messages of this session (SQLite)
Phase 1  embed the new messages, fetch the 10 most similar existing memories
Phase 2  ONE LLM call: ADDITIVE_EXTRACTION_PROMPT + existing memories + new messages
Phase 3  batch-embed every extracted text
Phase 4  md5 hash of each text; skip if already stored
Phase 6  batch insert vectors + payloads; batch write history rows
Phase 7  extract entities (spaCy), embed them, link to memory ids
Phase 8  save the raw messages

There is no UPDATE and no DELETE anywhere in that path. The add() docstring still mentions them; the code returns "event": "ADD" only.

2. Richer memories

The prompt in mem0/configs/prompts.py (ADDITIVE_EXTRACTION_PROMPT) is long, and its rules are the interesting part:

  • 15 to 80 words per memory, one to two sentences, self-contained, no pronouns.
  • Capture transitions: "switched from almond milk to oat milk after an almond sensitivity", not "prefers oat milk".
  • Resolve every relative date against the observation date, not today.
  • Keep every proper noun, title, number, and qualifier. "assistant manager", not "manager".
  • Extract from assistant messages too, when they add something new (recommendations, plans), framed as "User was recommended X".
  • When in doubt, extract. Dedup happens downstream.
  • Return linked_memory_ids pointing at related existing memories.

The old prompt produced "Name is John", "Is a software engineer". The new one produces a sentence a future reader can use on its own. Longer memories cost more tokens per hit, and they win because the answer model gets context instead of fragments.

3. Entities as a third index

mem0/utils/entity_extraction.py pulls proper nouns, quoted titles, and noun compounds out of each memory with spaCy. Each entity is embedded and stored in a second collection with linked_memory_ids. Same-name entities are merged by exact text or by cosine above 0.95.

At search time, _compute_entity_boosts extracts entities from the query, looks them up, and hands every linked memory a boost of similarity x 0.5 x weight, where the weight shrinks for entities linked to hundreds of memories (a generic entity should not boost everything).

4. Multi-signal retrieval

_search_vector_store runs three searches and fuses them in mem0/utils/scoring.py:

dense    over-fetch max(4k, 60) by cosine
bm25     lemmatized query against a sparse BM25 vector (Qdrant + fastembed)
entity   boosts from the entity store

combined = (cosine + sigmoid(bm25) + entity_boost) / max_possible

max_possible is 1.0, 2.0, or 2.5 depending on which signals were available, so the score stays in [0, 1] and the threshold (default 0.1) applies to the cosine alone before fusion.

What is not in the open-source code

Two features the README lists are platform-only: temporal reasoning ("ranks the right dated instance for current state, past events, upcoming plans") and graph memory. The OSS timestamp argument raises an error that says so. The 92.5 number also uses a top-200 retrieval budget, about 7K tokens per question. Open-source users are told to expect "directionally similar" results.

What we take, and what we change

Take: ADD-only, the rich-memory prompt rules, hash dedup, entity linking, three-signal fusion with an explainable score.

Change:

  • Supersede instead of link. Mem0 links a new fact to the old one and keeps both open. We let the extractor say "this replaces that" and close the old row's validity window, so default search is not diluted and history is still there (see the bitemporal blog).
  • Dates on every row. event_date and valid_from / valid_to are first-class columns, so the temporal reasoning Mem0 keeps for its platform is just a WHERE clause for us.
  • No spaCy, no Qdrant. Entities come back from the extractor call it already made; BM25 comes from SQLite FTS5. Fewer moving parts, same three signals.
  • Raw rounds indexed too. Facts are keys; the original turn is the value the answer model can read. LongMemEval showed this is worth points.

Closing

Mem0 in 2026 is not a clever update engine. It is a careful extractor that never deletes, writes full sentences with dates and names, and a search that listens to meaning, words, and names at once. Our hybrid keeps all of that and adds two clocks to every row.

← All articles