1. Executive Summary
MemMachine is a memory server — FastAPI plus MCP,
backed by a real database — that splits memory in two and refuses the
trade most of this atlas makes. Every conversation message is written to
an episode store and kept. Separately, a background loop reads those
episodes and derives SemanticFeature rows: short typed
statements about a user or account, each carrying
citations back to the episode IDs it came from.
The paper calls this ground-truth-preserving, and the phrase is load-bearing rather than marketing. Most systems here extract a fact and discard or archive the conversation that produced it, which makes "why do you believe that?" a question the store cannot answer. MemMachine keeps both halves, so its citations resolve to text that still exists. That is the single most valuable property in this design and very few systems in this atlas have it.
The cost is paid in the same coin. Because the source is retained
forever and a deleted feature leaves no record of having been rejected,
the evidence that originally justified a deleted belief remains
permanently available. The system is saved from re-deriving it by a
one-way is_ingested watermark rather than by anything that
knows the value was wrong — a guard that works, and works for an
unrelated reason.
The engineering is stronger than most of what this atlas reviews: 1,978 test functions across 112 files, alembic migrations, four vector backends, an HNSW-corruption test suite that reads like it was written after an incident, and a write-path guard that rejects reserved metadata keys explicitly "to prevent cross-producer / cross-session impersonation". Against that, the deletion path carries the weakest code in the repository — a duplicated method that silently discards the error handling its twin has, on the one path where partial failure is invisible to the caller.
2. Mental Model
A memory here is one of two things, and they have different epistemic status.
An episode is a message that happened. It is not a claim, it is a record, and the system treats it as immutable ground truth: written once, never edited, never re-interpreted. Nothing in the code updates an episode's content.
A semantic feature is a claim about someone, derived
from episodes. It is a four-part tuple — category,
tag, feature_name, value — plus
metadata.citations, a list of the episode IDs that produced
it (semantic_memory/semantic_model.py).
The state machine is shallow, and the shallowness is the finding:
flowchart TB
M[message arrives] --> E["Episode (is_ingested = false)<br/>retained forever"]
E -->|background loop, every 2s, 5 per set| L["LLM emits SemanticCommand: ADD or DELETE"]
L -->|ADD| SF["SemanticFeature + citations"]
SF -->|once a tag group reaches 20 features| CON["consolidation: LLM merges,<br/>citations unioned"]
L -->|DELETE| GONE["row gone — no record that it was ever asserted,<br/>no record that it was rejected"]
L --> ING["Episode marked is_ingested = true —<br/>never reconsidered"]
SemanticCommandType has exactly two members,
ADD and DELETE. There is no supersede, no
reject, no confidence, no candidate state. A feature is either in the
table or it is not, and the table is the belief set.
So: memory is background-managed, not
agent-controlled and not user-controlled. The LLM that emits commands is
not the agent holding the conversation; it is an ingestion worker
running on a timer against a prompt template. A user can call
delete_features through the API, and a developer can
configure which categories exist, but nobody approves an individual
claim before it becomes one. Features are treated as inferred
state presented as fact — with the significant mitigation that
each one shows its work.
How a belief dies: someone deletes it explicitly, or a consolidation pass merges it into a neighbour and rewrites the value. Nothing expires, nothing decays, and nothing is ever marked wrong.
3. Architecture
A Python 3.12 server in a uv workspace of six packages —
server, common, client,
ts-client, meta, skills. The
server splits into episodic_memory,
semantic_memory, a retrieval_agent, and a
main orchestrator (main/memmachine.py, 1,671
lines) that every entry point goes through.
flowchart TB
C[REST v2 / MCP stdio / MCP HTTP / SDKs] --> MM[MemMachine orchestrator]
MM -->|synchronous| ES[(Episode store<br/>SQLite or Postgres)]
MM --> EM[Episodic memory<br/>short-term + long-term]
MM --> SM[Semantic session manager]
SM --> H[(Semantic history<br/>is_ingested flag)]
ING[Background ingestion loop<br/>every 2s, 5 per set] --> H
ING --> LLM[Extraction + consolidation LLM]
LLM --> SF[(Semantic features + citations<br/>Neo4j or pgvector)]
MM --> RA[Retrieval agent] --> ES
RA --> SF
DQ[Session deletion queue] --> W[Delete worker]
MM --> DQ
Persistence is pluggable at three layers: episodes through
SQLAlchemy, semantic features through either
neo4j_semantic_storage.py or
sqlalchemy_pgvector_semantic.py, and vectors through
sqlite_vec, sqlite,
qdrant_vector_store.py,
milvus_vector_store.py, or an in-process hnswlib engine.
Alembic migrations live under
semantic_memory/storage/alembic_pg.
Deployment and ergonomics
Honest summary: this is a service, not a library.
There is a docker-compose.yml, a
memmachine-compose.sh, a configuration wizard
(installation/configuration_wizard.py), and a
Dockerfile, which is the right amount of scaffolding — but
you are standing up a server, a database, and an LLM provider before
anything is stored.
- The SQLite plus
sqlite-vecpath means it can run local and offline for storage, and the vector search will work. - It cannot run usefully without an LLM. Semantic features exist only because a language model emits commands; with no model configured, ingestion produces nothing and you are left with an episode log. An API key is not required to store — episodes commit regardless — but it is required for memory to mean anything beyond transcript search.
- Neo4j is a first-class semantic backend and the Postgres/pgvector path is the alternative; either is a real operational commitment.
- The store is inspectable — episodes are rows with plain content, features are rows with plain values — so hand-repair is realistic in a way it is not for systems that keep only embeddings.
4. Essential Implementation Paths
Capture/write. MemMachine.add_episodes
(main/memmachine.py:680). Writes to
episode_storage.add_episodes first and awaits it, then fans
out to episodic memory and
semantic_session_manager.add_message. The semantic branch
only records history rows; no LLM runs here.
Extraction.
IngestionService._process_single_set
(semantic_memory/semantic_ingestion.py:122). Pulls up to
five history rows with is_ingested=False, fetches the
episodes, runs the category's update prompt, and applies the resulting
commands in _apply_commands (line 280), which calls
add_citations(f_id, [citation_id]) on every ADD. Ends by
calling mark_messages_ingested (line 270).
Consolidation.
_consolidate_set_memories_if_applicable (line 329) groups
features by tag, keeps groups at or above
consolidation_threshold, and hands each to
_deduplicate_features (line 447), which merges via LLM and
unions the input citations (line 484).
Retrieval. MemMachine.query_search
(line 947) and list_search (line 1025), with
_search_episodic_memory (line 742) and the agent-mediated
_query_episodic_with_retrieval_agent (line 800) plus
_dedupe_and_score_agent_long_term_episodes (line 927).
Delete/forget. Four distinct paths:
delete_features(line 1179) — hard delete by feature ID.delete_episodes(line 1131) with_cleanup_semantic_history(line 1166).delete_session(line 580) — marks the sessionDeleted, then enqueues the real work on_deletion_queue._delete_session_worker(line 340) →_delete_queued_session(line 355) →_delete_session_episode_store(line 367), which loops in batches ofEPISODE_DELETE_BATCH_SIZE = 1000.
Schema.
common/episode_store/episode_model.py,
semantic_memory/semantic_model.py.
Integration. server/app.py,
server/api_v2/, server/mcp_stdio.py,
server/mcp_http.py, with api_v2/mcp.py:549
exposing mcp_delete_memory.
Tests. 112 files under
packages/*/*_tests/, 1,978 test functions.
5. Memory Data Model
Episode carries uid, content,
session_key, created_at,
producer_id, producer_role,
produced_for_id, sequence_num,
episode_type, content_type,
filterable_metadata and metadata. One
timestamp — the moment the message was recorded. There is no
validity interval, so nothing here is bi-temporal; a fact that
was true last year and false now is two features with no relationship,
or one feature the consolidator rewrote.
SemanticFeature carries set_id,
category, tag, feature_name,
value and Metadata{citations, id, other}. The
citation list is the only provenance mechanism and it is a good one:
Sequence[EpisodeIdT], resolvable against a store that still
holds the episodes.
Scoping is three-level and real.
SessionData is a protocol requiring org_id,
project_id and session_key
(main/memmachine.py:82), and these are threaded into
storage calls rather than merely stored — see
semantic_session_manager.py:382, where
project_id is passed only when the set type is not
org-level, so an org-level feature set is deliberately visible across
projects. Because the key is applied as a filter on the read path, this
earns scope_enforced.
No versioning, no correction chain, no TTL, no
pinning. The deleted_at string appears once in the
repository and not as a column on either memory type.
6. Retrieval Mechanics
Two subsystems queried in parallel and merged by
SearchResponse (main/memmachine.py:736), which
is simply episodic_memory and semantic_memory
side by side — the caller receives both rather than one fused
ranking.
Episodic retrieval runs vector search over episodes with a
configurable reranker. Semantic retrieval matches features by embedding
with min_distance and limit, and takes a
load_citations flag, so provenance is opt-in on the read
path rather than always paid for.
The optional retrieval agent
(retrieval_agent/, wired at
_query_episodic_with_retrieval_agent) orchestrates a longer
path: run a long-term search, pull short-term context, then dedupe and
score across the results.
_dedupe_and_score_agent_long_term_episodes is where
cross-arm overlap is resolved.
A filter language (common/filter/filter_parser.py, with
And and Comparison nodes and
to_property_filter) is shared between the search and delete
paths, which is why session-scoped deletion can reuse the same
expression the reader uses — a small, good piece of design.
Failure modes to expect: the two arms are merged by the caller rather than fused, so an application that concatenates them will double-count a fact that appears both as an episode and as a feature derived from it. And features carry no recency signal beyond their citations, so a stale profile line competes with a fresh one on embedding distance alone.
7. Write Mechanics
Features are created only by the ingestion LLM.
SemanticCommand is validated by pydantic and strips null
bytes; the command vocabulary is add and
delete. Crucially, the LLM does not get to invent structure
— categories and their allowed tags are configuration, and
server_tests/.../test_semantic_ingestion.py:889 asserts
"LLM-invented tag must be rejected" after a consolidation
pass that tried to rename bugfix to
Productivity Style. A constrained write vocabulary with a
test proving the constraint holds is rarer in this atlas than it should
be.
The other guard worth naming is on the episode path:
long_term_memory.py:665 refuses
filterable_metadata keys beginning with _,
because they would collide with _producer_id,
_session_key and _episode_uid — rejected, in
the code's own words, "to prevent cross-producer / cross-session
impersonation". That is an explicit defence against a caller forging
provenance, and it has tests either side of it
(test_episode_to_event.py:135 and :159).
Conflict handling is consolidation and nothing else. When a tag group reaches 20 features the LLM is asked to merge them; the merged row's citations become the union of its inputs'. Repeated over months this produces a feature citing dozens of episodes — provenance that is still technically correct and progressively less useful. Call it citation dilution; it is the predictable cost of merging without keeping the pre-merge rows.
Operational cost
Unusually for this atlas, the numbers are in the code rather than absent.
- The write path does not block on an LLM.
add_episodesawaits the episode store and the history insert, both database writes. - The lag before a new memory is retrievable as a
feature is one poll of the background loop plus one extraction
call.
feature_update_interval_secdefaults to 2.0 (semantic_memory.py:98) and each pass takes five history rows per set, so a burst of 20 messages needs four passes, roughly eight seconds plus model latency. That is a genuinely fast deferred write, and it is one of the few systems here where the lag can be stated from configuration rather than estimated. - No pass re-reads the whole store. Consolidation is
scoped to one tag group above threshold, and ingestion is watermarked by
is_ingested, which is set true in one place (neo4j_semantic_storage.py:789) and never reset. The token bill scales with the day's traffic, not with the corpus — the opposite of the nightly-rebuild systems elsewhere here. - On the read path,
limitandmin_distancebound the semantic side and the episodic side is reranked, but the assembled prompt is the caller's responsibility; the server returns results, not a formatted block.
8. Agent Integration
Four surfaces: REST v2 (server/api_v2/), MCP over stdio
(mcp_stdio.py) and over HTTP (mcp_http.py), a
Python SDK, and a TypeScript client. There is an OpenClaw integration
under integrations/.
Agency is deliberately limited, and the split is the interesting
part. The agent can search, add
episodes, and delete —
api_v2/mcp.py:549 exposes mcp_delete_memory
and to_delete_memories_spec builds its argument shape. The
agent cannot write a semantic feature directly; features are the
ingestion worker's output. So an agent can destroy a belief but not
manufacture one, which is a defensible asymmetry and the reverse of the
more common design where the model writes memories directly.
Injection is not automatic. Nothing here hooks a turn boundary or a compaction event; the application calls search and decides what to do with the results. Adapting it to another agent is therefore easy — it is an HTTP or MCP call — and the work is on the integrator's side.
9. Reliability, Safety, and Trust
Provenance is the strength. Citations resolve. That sentence is worth more than the rest of this section, and it is true here and false for most of this atlas.
Uncertainty cannot be represented. There is no confidence field on a feature, no candidate state, no trust level. Something is a feature or it is absent. The extraction LLM's judgement is the only filter, and its output is stored as fact.
Nothing records rejection. Deleting a feature
removes the row. The episodes that justified it remain, un-annotated.
The system does not re-derive the feature — is_ingested
prevents that — but the protection comes from a processing watermark,
not from knowledge that the claim was wrong. Two consequences follow.
First, if a message were ever re-ingested by any future path, the
deleted belief returns with no obstacle. Second, and more immediately, a
user correcting a fact in conversation produces a new message,
which is un-ingested, which produces a new feature — leaving
the old and new features side by side until a consolidation pass at
twenty features decides what to do with them.
The deletion path is the weakest code in the repository, in two ways.
Deletion is acknowledged before it is performed.
delete_session (main/memmachine.py:580) sets
the session status to Deleted and then calls
self._deletion_queue.put_nowait(session_data). The caller's
request returns successfully at that point. The actual removal happens
later in _delete_session_worker, whose exception handler is
logger.exception("Failed to delete session %s", ...). So a
failure leaves a session marked Deleted, reporting itself
as deleted, with its episodes still in the store and a line in a log
file as the only evidence. There is no retry and no dead-letter path;
the queue is an in-process asyncio.Queue, so a restart
between acknowledgement and completion loses the request entirely.
A duplicated method silently drops error handling on that same
path. Class MemMachine defines
_cleanup_semantic_history twice — at line
563 and again at line 1166. Verified by AST: both are members of the
same class, and Python binds the later one. They are not identical. The
first wraps get_semantic_service() in
try/except ResourceNotReadyError, logs, and returns so
cleanup can be skipped. The second has no error handling at all. The
defensive version is dead code.
That matters because of where the sole caller sits
(_delete_session_episode_store, line 367):
while True:
episode_ids = await episode_store.get_episode_ids(...)
if not episode_ids:
break
await self._cleanup_semantic_history(episode_ids) # can now raise
await episode_store.delete_episodes(episode_ids)
If the semantic service is not ready, the live version raises
before delete_episodes runs. The batch is
not deleted, the loop aborts, the worker logs, and the session's status
still reads Deleted. The author of the first definition
anticipated exactly this failure and wrote the handler for it; a later
duplicate discarded it.
The project's own tooling does not catch this.
ruff check on the file reports no violations, although
F is selected in pyproject.toml and
F811 is not in the ignore list — the rule fires on a
minimal reproduction of the same shape but not on this file.
ty check is likewise clean. Only the AST, or a reader,
finds it.
Multi-tenancy is otherwise handled with care: scope keys on the read path, and the reserved-key guard described in section 7 specifically to stop callers forging cross-session identity.
10. Tests, Evals, and Benchmarks
1,978 test functions across 112 files, which is
among the most thoroughly tested repositories in this atlas. The suite
mirrors the source tree, under
server_tests/memmachine_server/{semantic_memory,episodic_memory,common,server,main}.
The standout is
common/vector_store/vector_search_engine/test_hnswlib_engine.py:
a suite about index corruption from accumulated deletions and re-adds,
including test_accumulated_tombstones_then_single_readd and
a _no_corruption invariant helper. Its comments distinguish
an expected "deleted" error from "the corruption signature" of a label
that resolves to no real slot. This is production scar tissue, and it is
worth noting that the word tombstone throughout that file means
an hnswlib index slot, not a rejected value — a vocabulary collision
that will mislead anyone grepping this atlas's terms.
There is real coverage of the semantic path too:
test_semantic_ingestion.py runs to at least 890 lines and
includes the invented-tag assertion quoted above and
test_user_tags_preserved_after_ingestion_and_consolidation.
Deletion has test_memmachine_delete_session.py, which mocks
_cleanup_semantic_history directly — and, because it mocks
it, cannot detect that two definitions exist.
evaluation/ holds harnesses for LoCoMo and BEAM plus a
longmemeval_test.py, with data/locomo10.json
and data/wikimultihop.json committed. The design is better
than most: evaluation/README.md documents three test
targets — MemMachine alone, MemMachine with the retrieval agent, and a
pure LLM baseline given the full session content. A
baseline that is handed everything is a strong baseline, which is the
opposite of the usual complaint on the benchmarks page.
What is claimed but not committed: the paper reports LoCoMo 0.9169 with gpt-4.1-mini and 93.0% on LongMemEval-S. The harness and the datasets are here; scored output artifacts are not. So the numbers are reproducible in principle and unverified in this repository, which is the ordinary state of affairs and worth stating plainly rather than treating as either proof or fault.
Tests I would want before trusting it: one that
deletes a session while the semantic service is unavailable and asserts
the session is not reported deleted; one that asserts a corrected fact
does not leave the superseded feature retrievable before consolidation;
and any test at all that exercises
_cleanup_semantic_history without mocking it.
11. For Your Own Build
Steal
- Keep the source and cite it. A derived fact that carries the IDs of the records that produced it, in a store that still contains those records, is the cheapest honest answer to "why do you believe that?" — and it costs one array column. Most systems here cannot answer that question at any price.
- A one-way ingestion watermark. A boolean set once and never reset is a simpler and more reliable guard against re-processing than any timestamp comparison, and it makes the token bill scale with traffic rather than corpus.
- Constrain the extractor's vocabulary and test the constraint. Categories and tags as configuration, with a committed test asserting the model's invented tag was discarded, converts "the LLM might hallucinate structure" from a worry into a caught regression.
- Reject reserved metadata keys by prefix. A caller
who can set
_producer_idcan impersonate another producer. A prefix rule with a clear error message closes it in ten lines. - State your write lag in configuration.
feature_update_interval_sec = 2.0is a number a reader can reason about. Almost nothing else in this atlas offers one.
Avoid
- Acknowledging a deletion you have only scheduled.
If the API returns success when the work is queued, the queue must be
durable and the failure path must un-acknowledge. An in-process queue
plus
logger.exceptionplus a status already flipped toDeletedis the combination that produces a system which sincerely believes it deleted your data. - Trusting a linter to catch a duplicate definition. It did not here, on a configuration that selects the rule. If a method matters, the cheap check is an AST pass over your own source asserting no class defines a name twice.
- Merging without keeping what you merged. Unioning citations on every consolidation makes provenance monotonically coarser. If merges are frequent, keep the pre-merge rows or record which input contributed which clause.
- Treating a processing watermark as a correction mechanism. It stops re-derivation as a side effect of bookkeeping. Change the pipeline and the protection silently disappears, because nothing ever recorded that the value was wrong.
Fit
This suits a team that already runs Postgres or Neo4j and is building a personalization layer they expect to still be operating in two years. The test suite, the migrations, the four vector backends and the incident-shaped HNSW tests all point at people who intend to support this, and the citation model gives a support engineer something to look at when a user says the assistant believes something false. If your obligation is to explain a memory rather than merely produce one, this is the strongest starting point in the atlas.
Walk away if you want a library. There is no embedded mode worth the
name: the smallest useful deployment is a server, a database and a model
provider, and the Python-package-in-your-process story that
langmem or basic-memory offer is absent by
design. Walk away too if your correctness bar includes "a deleted thing
is provably gone" — the acknowledgement-before-deletion behaviour and
the duplicated cleanup method are both fixable in an afternoon, but you
would be fixing them, and until then the honest description is that this
system's deletion is best-effort while its API says otherwise.
For a research prototype or a weekend agent, the operational cost is out of proportion to the benefit, and the ceremony around categories and tags will feel like work before the first memory exists.
12. Open Questions
- Is there any path that resets
is_ingested? None was found, and the re-derivation guarantee rests entirely on that. A migration, a repair tool, or a backfill script that cleared it would silently resurrect every deleted feature. Confirming this needs the operational tooling, not just the source. - What happens to the deletion queue on shutdown?
stop()putsNoneon the queue to end the worker, but whether in-flight or pending sessions are drained first, and what a container kill does to them, needs a running system. - Does consolidation ever contradict itself across categories? Features are grouped and merged per category and per tag; two categories describing the same person are never compared. Whether that produces visible contradictions in practice needs a populated store.
- Are the paper's LoCoMo and LongMemEval numbers reproducible here? The harness is committed and was not run.
- How does the retrieval agent decide when to stop? The orchestration path was read structurally; its termination behaviour under a poor query was not traced.
Appendix: File Index
Storage/schema
packages/server/src/memmachine_server/common/episode_store/episode_model.pypackages/server/src/memmachine_server/semantic_memory/semantic_model.pypackages/server/src/memmachine_server/semantic_memory/storage/neo4j_semantic_storage.pypackages/server/src/memmachine_server/semantic_memory/storage/sqlalchemy_pgvector_semantic.pypackages/server/src/memmachine_server/semantic_memory/storage/alembic_pg/
Write path
packages/server/src/memmachine_server/main/memmachine.py(add_episodes, line 680)packages/server/src/memmachine_server/semantic_memory/semantic_session_manager.py(add_message, line 138)packages/server/src/memmachine_server/episodic_memory/long_term_memory/long_term_memory.py(reserved-key guard, line 665)
Extraction and consolidation
packages/server/src/memmachine_server/semantic_memory/semantic_ingestion.pypackages/server/src/memmachine_server/semantic_memory/semantic_llm.pypackages/server/src/memmachine_server/semantic_memory/cluster_splitter.py
Retrieval
packages/server/src/memmachine_server/main/memmachine.py(query_search, line 947)packages/server/src/memmachine_server/retrieval_agent/packages/server/src/memmachine_server/common/filter/filter_parser.py
Delete path
packages/server/src/memmachine_server/main/memmachine.py(lines 340–392, 580, 1131, 1166, 1179)
Background workers
packages/server/src/memmachine_server/semantic_memory/semantic_memory.py(_background_ingestion_task, line 783)
MCP/API/SDK
packages/server/src/memmachine_server/server/app.pypackages/server/src/memmachine_server/server/api_v2/mcp.pypackages/server/src/memmachine_server/server/mcp_stdio.py,mcp_http.py
Tests/evals
packages/server/server_tests/memmachine_server/semantic_memory/test_semantic_ingestion.pypackages/server/server_tests/memmachine_server/main/test_memmachine_delete_session.pypackages/server/server_tests/memmachine_server/common/vector_store/vector_search_engine/test_hnswlib_engine.pyevaluation/README.md,evaluation/retrieval_agent/,evaluation/data/locomo10.json