1. Executive Summary
a-memory is "4-tier agent memory with hybrid search and a real knowledge graph — all in plain SQLite files. Zero cloud. Zero external APIs." MIT, Python, 66,891 lines with 281 test files and 1,606 test functions, on PyPI, with an MCP server and an optional embeddings extra.
The four tiers are an L1 reflex buffer (now persisted — "was in-memory only since forever"), an L2 session store, L3 episodes and L4 core facts. Three mechanisms in the L4 store are worth the visit, and they share a weakness.
Visibility is a quarantine that sticks. A fact
carries visible | pinned | private | hidden. Search and key
lookup both select
WHERE layer=? AND user_id=? AND visibility NOT IN ('private','hidden'),
and the pinned-injection block selects only
visibility='pinned' — with the invariant written down
beside it: "C8: private facts never leave the store via recall (the
inject pinned block does not read them)."
What makes it a quarantine rather than a flag is the re-save path. Passing no visibility on a write re-reads the stored one before updating, under a comment that records both the bug and its date:
"a 'hidden' row re-saved with the same canonical key would otherwise be back to 'visible' — F1 sanitation, 2026-09-12"
So, as the API docstring puts it, "'hidden' works as a key quarantine: future writes update the row but never un-hide it." A memory suppressed once cannot be resurrected by writing to it again, which is the failure most systems with a suppression flag have.
Scope is a property of the handle.
MemoryManager.get_layer(layer_type, user_id) returns a
MemoryLayer that carries both and builds its episodic and
core stores beneath it; user_memory() and
agent_memory() are the named accessors. Every core-memory
read begins WHERE layer=? AND user_id=?, and
episodes carries a layer column with two
indexes on (layer, user_id).
That is a predicate on a shared table, not a file per layer, and the caller never passes it — which is what the README's "[u]ser facts and agent identity never share a namespace" amounts to in code, and the reason it earns the mark rather than reading as a filing convention.
Every mutation appends a before-and-after row.
_record_history writes "one A2.2 ledger row with full
before/after row JSON" and is called from all three paths: update with
the old row and the new, insert with a null before, and delete with a
null after attributed triggered_by or "delete". Beside it,
_record_temporal maintains an interval chain per key in
core_memory_temporal, closing the open interval and opening
a new one on each write, with a point-in-time read selecting
valid_from <= ? AND (valid_to IS NULL OR valid_to > ?).
The shared weakness is that the last two are advisory.
_record_history "[d]egrades to a warning so memory writes
never fail on history"; _record_temporal is "advisory,
never fails a save". Both are wrapped in a bare exception handler. The
intent is right — a ledger failure should not lose a memory — but the
implementation drops the record silently, so a gap in the ledger and a
broken interval chain are both invisible after the fact, and nothing
counts them.
The interval chain is also not a second time axis.
valid_from is set to the write instant, the same clock as
updated_at, so the point-in-time query answers what the
store held at a past moment rather than when anything was true in the
world. No bitemporal mark is claimed.
2. Mental Model
A layer is user or agent, and you are handed one rather than naming it.
A fact is a key and a value with an importance, a kind, a source and a visibility.
Hidden is a decision about a key that later writes inherit.
A ledger row is what changed, with both images and who triggered it.
Diagram source
%% caption: the layer and user are bound into the handle and appear in every read; a re-save with no visibility re-reads the stored flag first, so a hidden key stays hidden
flowchart TB
MGR["MemoryManager.get_layer(layer_type, user_id)"] --> H["MemoryLayer — carries<br/>layer_type and user_id"]
H --> UM["user_memory()"]
H --> AM["agent_memory()"]
H --> TIERS["L1 ReflexBuffer (persisted) ·<br/>L2 SessionStore ·<br/>L3 EpisodicMemory(layer=…) ·<br/>L4 CoreMemory"]
W["remember(key, value, …, visibility=None)"] --> EX{"key already exists?"}
EX -->|"yes, and visibility is None"| REREAD["SELECT visibility FROM core_memory<br/>WHERE entry_id=? — keep the stored flag<br/>('F1 sanitation, 2026-09-12')"]
REREAD --> UPD["update in place"]
EX -->|"no"| INS["insert"]
UPD --> LEDG[("A2.2 ledger row:<br/>old row JSON · new row JSON ·<br/>triggered_by")]
INS --> LEDG
DEL["delete"] --> LEDG
UPD & INS --> TEMP[("core_memory_temporal:<br/>close the open interval,<br/>open a new one at now")]
LEDG -.->|"'degrades to a warning so memory<br/>writes never fail on history'"| GAP["a failed append is dropped<br/>and nothing counts it"]
TEMP -.->|"'advisory, never fails a save'"| GAP
TIERS --> R{"reads"}
R -->|"search · key lookup"| F1["WHERE layer=? AND user_id=?<br/>AND visibility NOT IN ('private','hidden')"]
R -->|"pinned injection"| F2["WHERE layer=? AND user_id=?<br/>AND visibility='pinned'<br/>— C8: private facts never leave<br/>the store via recall"]
TEMP --> AS["point-in-time read:<br/>valid_from <= ? AND<br/>(valid_to IS NULL OR valid_to > ?)<br/>— one clock, the write instant"]3. Architecture
| Area | Role |
|---|---|
core/__init__.py |
MemoryManager, MemoryLayer, and the tier
wiring |
core/memory.py |
CoreMemory: save, the visibility rules, the ledger, the
interval chain |
core/episodic.py |
L3 episodes, layer-scoped with its own indexes |
core/session.py, reflex.py |
L2 and L1 |
graph/, rag/ |
The knowledge graph and hybrid retrieval |
lifecycle/, features/ |
Consolidation, promotion and TTL |
mcp_server/, hooks/ |
The agent surfaces |
alembic/ |
Schema migrations |
4. Essential Implementation Paths
core/memory.py:110-131 — the re-save that preserves
visibility, the ledger call and the interval close, in one block.
core/memory.py:398-410 — the search predicate, and the
C8 comment.
core/__init__.py:38-53 and :122-132 — how a
handle comes to carry its own scope.
5. Memory Data Model
An L4 fact is a key, a value, an importance, a memory kind, a
visibility, a source, an optional expiry and a layer and user.
source is a documented provenance contract —
user_explicit, staging_promotion,
episode_promotion or manual — which
distinguishes a fact a person stated from one the consolidation sweep
promoted out of an episode. That distinction is the one the atlas most
often finds missing in systems that extract from their own output.
Memory kind is validated and optionally reclassified on save, which
makes it a write-time genre rather than an epistemic state;
importance is a float. visibility is the
discrete field that governs what a reader sees.
6. Retrieval Mechanics
Hybrid lexical and vector search over the core store with a knowledge graph beside it, over-fetching ten times the limit so Python-side ranking can prefer rows matching more query tokens.
The two exclusions are the part to copy. private and
hidden never appear in search results or key lookups, and
the pinned block that injects facts into context reads only
pinned — so the set a model sees is narrower than the set a
search returns, deliberately.
7. Write Mechanics
One save path handling both update and insert, each appending a ledger row and maintaining the interval chain, with delete doing the same in reverse.
The re-save visibility rule is the design decision worth naming: an
explicit visibility argument wins, and its absence means
keep what is stored rather than use the default. Those
two readings of a missing argument are easy to conflate and the
difference here is whether a quarantine holds.
8. Agent Integration
An MCP server, hooks, and an OpenAPI surface, with the layer
accessors as the integration seam: an agent tool bound to
agent_memory() cannot reach the user layer through any
argument it accepts.
9. Reliability, Safety, and Trust
The three mechanisms above are the trust story and the caveat is uniform: two of them degrade rather than fail.
A ledger that "never fails a write" is the correct policy and a
silent except is the wrong implementation of it, for the
same reason it was wrong in the other systems this atlas has found doing
it. The fix is small: count the drops, expose the count, and a ledger
can be both non-blocking and honest about its holes.
The interval chain has the same handling and one further limit — closing and opening intervals outside a guarantee means a crash between the two statements leaves a key with either two open intervals or none, and nothing reconciles it.
10. Tests, Evals, and Benchmarks
281 test files and 1,606 test functions, with an eval/
directory, a CI badge and coverage reporting. The repository also
carries an AUDIT_REPORT_20260629.md and an
ARIEL_RULES.md, so the F1 and C8 labels in the comments
appear to index a review process; that process was not read here.
11. For Your Own Build
Decide what a missing argument means, and write it down.
visibility=None meaning "keep the stored flag" rather than
"use the default" is the whole difference between a quarantine and a
suggestion, and the comment recording the bug that taught them is what
will stop it regressing.
Bind scope to the handle, not the query. user_memory()
and agent_memory() return objects that carry their own
predicate, so isolation is not something a caller can forget to pass —
which is the failure mode this atlas finds in almost every system whose
scope is an argument.
Narrow the injected set further than the searched set. Search excludes private and hidden; the block that actually reaches the model reads only pinned. Two different questions, two different predicates.
And if a ledger must never block a write, count what it drops. A
silent except turns "we never lose a memory" into "we
sometimes lose the record and cannot tell".
12. Open Questions
Whether anything reconciles a broken interval chain.
_record_temporal closes and opens in two statements under
an advisory handler, and no repair pass was found.
What the F1, C8 and A2.2 labels index. They read as review findings,
and AUDIT_REPORT_20260629.md exists; the relationship was
not traced.
Whether hidden is reachable from the agent-facing tools.
It is a parameter on remember; which surfaces expose it was
not established.
Appendix: File Index
| Path | What to read it for |
|---|---|
core/memory.py:110-131 |
A re-save that cannot un-hide, with the dated bug behind it |
core/memory.py:398-410 |
The search predicate and the C8 invariant |
core/__init__.py:38-53, :122-132 |
Scope carried by the handle |
core/memory.py:217-231 |
The before-and-after ledger, and what it does on failure |
core/memory.py:265-300 |
An interval chain on one clock, and a point-in-time read |
History
2026-09-16 — 6a9f4663…
— first reading, at a commit dated 16 September 2026. Screened before
opening, from a shallow clone: eight files scanned, two auto-run
surfaces, one build-time execution point, no unpinned surfaces and two
dependency files inside the seven-day cooldown. Nothing was installed,
built or run.