On this page
Part 3 of this series surveyed agent memory in a few paragraphs. Its conclusion was that the systems agree on a shape: raw episodes kept as the record, facts derived from them by a model, and a few blocks pinned into the prompt. They also agree, by omission, on two harder problems: where the state lives, and the fact that anything an agent remembers can later steer it. This post goes one level down. It covers four systems and the specific lesson each taught us, then how those lessons became decisions in design 52 (Loams Memory) and design 48 §26 (the graph engine underneath).
The title's claim is the design's core rule: memory is data. It is rows with keys, owners, two clocks and a deletion path, stored in the same primitives as everything else. It is not a side store with its own rules, and not instructions for the next turn.
Status labels follow the 0.1.0 release notes: Available ships and is tested, Experimental ships but may change more, In progress is being built and does not ship in 0.1.0, and Planned is designed but not built.
| Piece | Status |
|---|---|
| Loams Graph: GQL on Grafeo, writes through the Loams log, snapshots in the bucket | Available |
Graph expansion from search hits, graph_* SQL table functions | Experimental |
Change-log adjacency index with pagerank, wcc and leiden (D1063, D1064) | Experimental: in the 0.1.0 code, not yet in the release notes' table |
| Fair multi-source traversal, result caches, write-path statistics, lease checklist, fault harnesses (D1065 to D1069) | Planned |
| Loams Memory (MM1; design 52, D870 to D891, which are Proposed) | In progress: the plan's first two tasks are in review; nothing ships in 0.1.0 |
Mem0: judging a memory at write time is expensive
Mem0 (Apache-2.0) is one of the most widely used open memory layers. Memories are scoped by user_id, agent_id and run_id, and the API is small: add, search with filters, get_all, update, delete and history. The open-source default stores vectors in Qdrant and history in SQLite (quickstart).
The Mem0 paper (Chhikara et al., 2025) describes a two-phase pipeline. A model extracts candidate memories from a conversation, then compares each with similar memories already stored and decides whether to add it, update an existing one, delete one it contradicts, or do nothing. It reports, on the LOCOMO benchmark, a 26% relative improvement over OpenAI's memory under an LLM judge, and 91% lower p95 latency with over 90% fewer tokens than putting the whole history in context. There is also a graph variant, Mem0g.
The interesting part is what happened next. In April 2026 the project's README announced a new default algorithm: single-pass, ADD-only extraction. One model call, no UPDATE or DELETE, and memories accumulate. It reports LoCoMo rising from 71.4 to 92.5 on its managed platform, and tells open-source users to expect "directionally similar" gains. We have not reproduced either figure.
We read that as an honest result. The update phase asks a model, once per candidate and on the request path, to decide whether two sentences say the same thing or contradict each other. That is the most expensive and least reliable call in the pipeline, and it runs exactly when the user is waiting.
What Loams took (D877). Add stores the episode durably and answers at once with a consistency token. Extraction runs afterwards as a durable function whose model calls are checkpointed, so a crash repeats at most the call in flight. Resolution goes cheap and deterministic first:
- An exact duplicate (same entities, predicate and normalised text) only adds the new episode to the existing fact's provenance.
- Functional predicates such as
LIVES_IN,WORKS_ATandPREFERS_TOOL_FORcan only have one current value per subject, so a new value is a contradiction, applied by rule with no model call. - Only when a candidate is close and neither rule applies does one bounded model call classify the new fact as a duplicate, an extension, an update or independent.
- A store can set
resolution: deferredand skip that call entirely, which is Mem0's add-only behaviour, and leave overlap to the nightly consolidation run.
We kept Mem0's scoping triple and history as a compatibility shim in the planned SDKs, so code written against Mem0 can move with small changes.
Graphiti and Zep: keep the episode, and keep two clocks
Graphiti (Apache-2.0) is the open-source engine behind Zep. The Zep paper (Rasmussen et al., 2025) describes it as a temporal knowledge graph, and three of its choices shaped our design more than anything else we read.
Episodes are the ground truth. Every raw input is kept, not lossily summarised, and every fact links to the episodes it came from. When a fact is wrong, you can see why it exists. When a source is deleted, you know which facts depended on it.
Two clocks. Graphiti is bi-temporal. Each fact edge carries when it was true in the world (t_valid, t_invalid) and when the system learned and stopped believing it (t′_created, t′_expired). Relative dates such as "two weeks ago" resolve against the episode's reference time. When a new edge contradicts an old one, the old edge's t_invalid is set to the new one's t_valid: in Graphiti's own words, old facts are invalidated, not deleted. So the graph can answer what was true at a given time, and what the system believed at a given time, which are different questions.
A retrieval pipeline with a constructor. Search runs cosine similarity, BM25 and breadth-first graph search, reranks with RRF, MMR, episode mentions, node distance or a cross-encoder, and then a constructor renders the result as facts with their validity ranges. The model sees "Alice works at Stripe (valid from March 2026)", not a bare string.
The paper reports 94.8% on the DMR benchmark against MemGPT's 93.4%, and on LongMemEval 63.8% with gpt-4o-mini and 71.2% with gpt-4o, against 55.4% and 60.2% for full context, with about 1.6k context tokens instead of 115k. It is also candid that the system did worse on single-session-assistant questions. Those are Zep's numbers.
What Loams took (D874, D875, D878). Episodes are the truth and every fact names its episodes. A fact whose last source episode is forgotten is forgotten too. Facts carry world time [valid_at, invalid_at) and system time [recorded_at, expired_at), and the API keeps three operations apart that most memory layers merge:
- Contradiction ("Alice moved to Stripe") closes the old fact's world interval. History is kept.
- Correction ("that was never true") closes its system interval. History is kept, marked as an error.
- Forget ("forget that") deletes it. No history is kept.
as_of and known_at take a time each, and both can be given: "what did we believe on 1 March about where Alice worked in January?". Search follows Graphiti's pipeline: BM25 and vector seeds over facts, entities and episodes in one hybrid query, one or two hops through the graph with the temporal filter pushed into the expansion, fusion and an optional rerank, then a constructor that prints validity ranges and sources. The hybrid retrieval post covers the fusion step.
What we did not take. Graphiti runs on a graph database you bring: Neo4j, FalkorDB or Amazon Neptune, with a vector store beside it. That is the "state in three services" problem from part 3. We also did not take model calls on the read path. A Loams Memory search makes at most one embedding call for the query, which is cached, plus an optional reranker the store configures.
The others, briefly
Two smaller lessons came from elsewhere. Letta (Apache-2.0) has core memory blocks, labelled strings with a character limit, pinned into the prompt. Its documentation notes that with concurrent writers the last write wins. Loams blocks keep the shape but require if_version on every update, so a stale writer gets ABORTED and must re-read and merge (D876). Cognition's Agent Memory Repo spec (MIT) and Devin's memory gave us the open export format, a Git repository of Markdown with a MEMORY.md index, and dreaming: a scheduled consolidation that merges, prunes and reorganises while keeping source links and explicit preferences. In Loams that is a durable job per space with a token budget, and it writes a report the user can read and revert (D880, D881).
HydraDB: keep the index fresh from the log
HydraDB is a Rust graph database that keeps its data on object storage. It speaks Cypher over Bolt and HTTP, stores graph records as keys in SlateDB databases under a bucket prefix, runs a separate indexer that publishes immutable compressed sparse column (CSC) files, and decides writer ownership with object-store leases. We reviewed it at commit fe496d4 (release 0.2.0, 2026-10-15).
HydraDB is AGPL-3.0, so we took ideas from it and nothing else: no code, no prose, no tests, no scripts and no grammar files (D1072). Contributors who read it write Loams' version from the design's own descriptions. If Loams ever needs the Cypher grammar HydraDB vendors, it will come from Neo4j's own Apache-2.0 repository, not from HydraDB's copy.
HydraDB is not a memory product, but it is the most direct answer we found to a question memory raises. Memory graphs are written all the time and read with graph algorithms: communities for consolidation summaries, centrality for importance. An algorithm needs an adjacency structure, compressed sparse rows (CSR) and columns, of the whole graph at one point in time. Rebuilding that on every call means scanning the engine. Keeping it in the engine means a second storage format. HydraDB's indexer does neither. It publishes content-addressed index generations, patches them from the write-ahead log's tail, and queries read the index with the tail overlaid.
Design 48 §26 compares the two systems honestly. HydraDB was ahead in five places: a persisted traversal index kept fresh without full rebuilds, multi-source path search with a fair budget, result caches keyed by read epoch, statistics kept on the write path, and care about lease time, plus fault scripts that exercise all of it. Loams chose differently on purpose in five others: an engine-neutral log, a standard query language (GQL), atomic batches with preconditions, server-assigned identities, and one API across SQL, search and graph.
Adopted and built: the change-log adjacency index (D1063, D1064), Experimental. Every write to a Loams Graph, from GQL or from a link, already becomes one change-set stream in the Loams log. The index is built from that stream. It holds a vertex table, the forward CSR and reverse CSC, and edge ids and types, in a canonical encoding: one graph state always produces the same bytes and the same hash, however it was built. Golden vectors freeze the format. When a previous index exists, the stream still holds the tail, and the tail is short enough, the indexer patches the old index in one linear pass. Otherwise it scans the engine, and a full build is counted with its reason, as normal control flow. The file is written once under its content hash, and the graph's manifest names it through a fenced compare-and-swap that only moves forward, so an older index never replaces a newer one.
The reader's rule is the important part. A job pins an offset M, loads the index covering N, and overlays the stream [N, M). Its answer is exact at M. If the tail has been trimmed or is longer than 50,000 records, it scans the engine instead. It never answers from a stale index. The index is derived state: deleting it loses nothing. Counters report edges applied from tails against edges scanned, so we can see whether the incremental path pays for itself. GR1 Task 22 built it, with pagerank, wcc and leiden as SQL table functions on top.
Planned, from the same review (D1065 to D1069, approved as defaults on 2026-10-16):
- Multi-source traversal with a fair budget. Expansion from many seeds hands out its result limit round-robin, so one seed with many neighbours cannot take the whole answer. A batch form of shortest paths runs one breadth-first search over all pairs. It stays inside Search and SQL, with no
CALLprocedures. - Result caches keyed by (graph, pinned offset, spec hash). Any write advances the offset, so a cached answer can never be stale and invalidation needs no logic at all.
- Write-path statistics: counts per label and edge type, degrees, property counts. They are derived state, rebuilt on open, and used to answer schema calls exactly and to spot hub vertices.
- A lease checklist (expiry judged by the authority's clock, a bounded grace period, and different errors for "not the owner" and "fenced") and fault harnesses: an object-store pause, a forced writer takeover with a checker over the stream, multi-process stress and a disposable Kubernetes failover.
Not adopted (D1071). Caller-chosen integer ids as upsert keys, which would collide across the links and writers of one graph. Auto-commit only, where we have atomic batches with if_version. Cursors that are consumed when read, which break client retries. Partitions the client picks and the server cannot list. C dependencies and a large minimum stack. The graph stored as LSM keys as the primary store, which would replace the engine-neutral log. And two path semantics. The decision not to serve Bolt stands. The review corrected its rationale, since a maintained pure-Rust Bolt library now exists, but Connect stays the one contract.
Memory as data, concretely
Put together, Loams Memory (In progress) has five rules that a side store would not give you.
Collections are the truth, and the graph is a projection. A memory shard is five collections (episodes, entities, facts, structural edges and blocks) plus one linked graph, kept by a collection-to-graph link that applies changes exactly once. Each Add writes collections only. Embeddings live once, in the collections. With an owned graph, every add would be two writes to two systems with no atomicity between them (D870, D873).
Isolation is an invariant, not a filter you remember to add. Every key starts with the space id. Every query the service builds binds the caller's authorised spaces in one place that handlers cannot bypass. No edge joins two spaces, so expansion that starts inside a space cannot leave it. A read may compose up to eight spaces, for example a user, a project and an org. A write goes to exactly one (D872).
Forget means gone. Forget deletes the rows at once and cascades to blocks, profiles and exports. Within 30 days the erasure path rewrites the underlying files. Then the linked graph is rebuilt from the purged collections, and its old snapshots and log are dropped, because a graph's history would otherwise keep what the user asked to delete. The guarding test asserts that a forgotten fact never comes back through search, expansion, profiles, history, export, the watch stream or MCP, at any consistency level (D879).
Memories are data, never instructions. Every episode has a trust level. Facts from external content cannot become blocks without review. Instruction-like content is quarantined, secrets are refused or redacted at add, and returned memory is framed as untrusted data. Part 3 explains why this is the rule most memory layers leave out (D888).
Benchmarks are reproduced, not quoted. Every number in this post is another project's own claim. Loams will run LongMemEval (Wu et al., ICLR 2025) and LoCoMo (Maharana et al., ACL 2024) itself, with pinned judges, and publish the configurations and raw outputs. Other vendors' numbers stay context, never comparisons (D889).
Where it stands
Loams Graph is Available in 0.1.0, expansion from search hits is Experimental, and the adjacency index and its algorithms are in the 0.1.0 code. Loams Memory is In progress. The MM1 plan's first two tasks, reconciling the plan with the code as built and the loams.memory.v1 protos, are in review, and its decisions are still Proposed in the log. The plan's order is the owner's: a per-user MVP first (add, search, forget, blocks and MCP tools), then the temporal graph and dreaming, then code memory, then evaluation, then SDKs and adapters. Until the MVP gate passes, nothing in the "memory as data" section is something you can run.
The trade-offs
- Asynchronous extraction means a gap. A fact is not searchable until extraction finishes. The episode is searchable at once, and a consistency token lets a caller wait, but "I just told you that" can still miss for a few seconds.
- Rules before models is coarser. Functional predicates resolve contradictions with no model call, and sometimes the rule is wrong: people do hold two jobs. Stores can edit the predicate registry, and consolidation gets a second look.
- A linked graph lags its sources. Expansion reads the graph, which trails the collections by the link's lag. Reads can wait on the graph's token, which costs latency.
- One substrate is one blast radius. Memory, the graph, Git and durable state share one namespace and one authorisation model. We think one boundary you can audit beats five you cannot, but it is a bet.
- Ideas only is slower. Rewriting HydraDB's ideas from our own descriptions, rather than adapting its code, costs time. The AGPL makes that the only option for an Apache-2.0 project, and we would rather say so than blur it.
What we take from all this
Mem0 showed that the clever part of a memory layer, judging what a new sentence means for old ones, belongs off the request path. Graphiti and Zep showed that keeping the raw episode and two clocks makes memory explainable and correctable. HydraDB showed that a graph index can be derived from the log and published like any other immutable file, as long as readers overlay the tail and never trust a stale copy. None of these needs a new database. They need rows with owners, a log with offsets, a bucket for immutable files, and a deletion path that reaches everything. Loams already has those, so Loams Memory is a set of rules on top of them, and the rules are written down in design 52 before the code is.