Chapter 10
What are we building?
The design of our hybrid memory layer, called `hmem`. We will see where each piece comes from, walk one fact through the whole system, and look at the code layout. The goal is a system you can explain on a whiteboard in five minutes.
Before you read, guessWhat is the primary purpose of benchmarks in this context?
Take ten seconds and guess — even a wrong guess makes the answer stick. Tap to see where the chapter lands, or just read on.
Everything else is tuning, and tuning is what the benchmarks are for.
In this blog, we will learn the design of our hybrid memory layer, called hmem. We will see where each piece comes from, walk one fact through the whole system, and look at the code layout. The goal is a system you can explain on a whiteboard in five minutes.
The one-line version
A card file where every card has a date, a source, and a pointer to the card it replaced; a librarian who searches by meaning, words, and names; and a secretary who writes cards but is not allowed to tear any up.
Where the parts come from
class notes Mem0 v3 HydraDB / Zep
secretary (write) categories, ADD-only, rich supersede with
importance, sentences, dedup, time stamps,
privacy rules entity links provenance
librarian (read) recency + dense + BM25 + time filter,
importance entity boost one-hop expansion
card file (store) one collection, history table append-only,
user_id filter two clocks
forgetting episodic decay, (none) (roadmap)
write-time dedup
One card
id 7f3a...
user_id maya
text "Maya moved from London to Paris in March 2026 for a payments startup job"
category semantic
importance 7
event_date 2026-03-01
valid_from 2026-03-02T09:14Z
valid_to NULL
superseded_by NULL
source_ids ["s18241:t1"]
hash md5(text)
embedding 768 x float32
last_accessed 2026-06-09T18:02Z
Beside the cards: episodes (the raw turns, so the answer model can read the original words), entities (name to card ids), and an FTS5 index over text for BM25. All in one SQLite file.
The whole system
flowchart LR
subgraph write["WRITE (async)"]
M["session messages"] --> S["10 similar memories<br/>+ last k turns"]
S --> L[["extractor LLM<br/>one call, JSON"]]
L --> D["dedup: hash, cosine>0.95"]
D --> W["insert + embed + link entities<br/>close superseded rows<br/>save episodes"]
end
subgraph read["READ (sync)"]
Q["question"] --> P["time parse + entities"]
P --> V["dense"] & B["BM25"] & E["entity"]
V & B & E --> F["fuse + recency + importance<br/>+ date filter"]
F --> H["hop: superseded chain,<br/>source turns"]
H --> O["prompt block, dated"]
end
W --> DB[("SQLite")] --> V
DB --> B
DB --> E
Walk one fact through
Session, March 2: Maya says "I just moved from London to Paris for a new role at a payments startup."
Write path. The messages are embedded; the ten closest existing memories come back, including "Maya lives in London" (id 3). The extractor sees both and returns:
{"text": "Maya moved from London to Paris in March 2026 for a job at a payments startup",
"category": "semantic", "importance": 7, "event_date": "2026-03-01",
"entities": ["Maya", "London", "Paris"], "supersedes": ["3"]}
Hash is new, cosine to id 3 is 0.84 (not a duplicate). Insert as id 7. Link the three entities. Stamp id 3 with valid_to = 2026-03-02, superseded_by = 7. Save the raw turn as an episode. One LLM call, one embed call.
Read path, June: "Any good bakeries near me?" The time parser finds nothing. Dense search ranks id 7 high (Paris, near). BM25 finds nothing ("bakeries" is not in any memory). Entity search finds none. Fusion keeps id 7; recency and importance keep it there. Id 3 is closed, so it is not in the default result. The prompt block reads:
- Maya lives in Paris, moved from London in March 2026. (since 2026-03)
Read path, later: "Where did I live before Paris?" Dense search finds id 7. The hop pulls its superseded chain: id 3, London, 2025-01 to 2026-03. Both go into the prompt, with dates. The answer model has everything.
The code
hmem/
config.py one dataclass: models, endpoint, db path, fusion weights
llm.py LLM protocol, OpenAI-compatible client, MockLLM
embed.py Embedder protocol, OpenAI-compatible, local, MockEmbedder
store.py SQLite: memories, episodes, entities, FTS5, vector blobs
extract.py write path
retrieve.py read path
timeparse.py "last summer" -> (start, end)
prompts.py EXTRACT, ANSWER, JUDGE
memory.py Memory.add / search / get_all / forget / delete_user
bench/ locomo.py, longmemeval.py, beam.py, common.py
tests/ offline, with mocks
Rules: every file small enough to read in one sitting, dependencies only on numpy, sqlite3, and the OpenAI client, mocks for every network call so tests run offline, and one seam (the Store protocol) for Postgres or a vector database later.
Why SQLite and numpy are enough for v1
The class notes size a user at about 200 memories. A dot product over 200 vectors of 768 floats is microseconds. Even a benchmark conversation with 50,000 memories is a 150 MB matrix and a 10 ms dot product. FTS5 gives BM25 without a search server. A single file is easy to copy, inspect, and delete. When it stops being enough, that is the scale blog.
What we expect to score, and why
- single-hop and extraction: hybrid recall with over-fetch
- multi-hop and multi-session: entity links plus one hop
- temporal: dates on every row plus time-aware queries
- knowledge update: superseded chain in the prompt
- abstention: the answer prompt must cite a memory id or say unknown
- preferences and instructions: procedural rows always injected
Targets: 75 on LoCoMo and LongMemEval-S with a small extractor and a GPT-4o-class answerer, then push toward 85 with a token budget under 5K per question. Reported next to tokens and latency, every time.
Closing
A secretary who never tears pages out, a card file with two clocks, and a librarian with three senses. That is the whole system. Everything else is tuning, and tuning is what the benchmarks are for.