1. Executive Summary
mnemora describes itself as a cognitive layer to lay underneath an existing LLM application — MIT, TypeScript, version 0.1.1, 87,938 lines across six packages with 1,212 test cases in 127 files, documentation in Japanese. Its stated aim is one sentence: give LLM applications remembering, rather than saving. It is explicit that it does not want to replace LangGraph, Mastra or a hand-written agent — it wants to sit below them, between the agent loop and storage, running observation, recall, association, reinforcement, forgetting, consolidation and reflection.
Three things earn marks, and one line of code explains the project better than its README does.
Provenance is the tag of a discriminated union. The module says why:
"The owner's principle 7, 'distinguish the AI's inference from facts the user stated', is implemented as the value of
kinditself rather than as an additional flag."
stated | inferred | consolidated | reflected | imported,
and each arm carries what that kind must prove: stated
needs a source observation id and a time; inferred needs
the model, the prompt version, a basis of the memory and
observation ids it reasoned from, and a confidence. A memory whose
provenance was never established is not a memory with a null field — it
is a value the type system will not let you build. Nearly every system
in this corpus carries provenance as an optional string; this one
carries it as the thing the record is.
The spelling of that union then lives in exactly one place, after it
was found duplicated by hand into the recall query's
excludeProvenanceKinds:
"When a closed union's spelling exists in two places, fixing one and forgetting the other depends on attention, and will certainly fail."
A contested memory is still recalled.
MemoryStatus is
active | superseded | contested | archived | forgotten, and
the recall scan admits two of them:
WHERE status IN ('active', 'contested'). Superseded,
archived and forgotten stay in the store and leave retrieval; contested
stays in both. A memory under dispute is surfaced rather than resolved
away at write time — the same stance yacmemo
takes with its warning marker, reached here through a status value. The
count beside the scan carries the identical predicate, so a withheld row
does not move the number either.
Three time concepts are kept apart.
occurred_at is when the thing happened,
recorded_at is when the store learned it, and
valid_from/valid_until are the window the
memory applies in. The effective time is
COALESCE(occurred_at, recorded_at) under a comment tying
the convention to the freshness formula, and a caller's
validAt gates the window — with isExpired and
isNotYetValid built as separate predicates, so the
system can distinguish a memory that has lapsed from one that has not
started, rather than only excluding both.
The third mark is the conformance kit.
expect(ids).not.toContain(excluded.id) with
expect(ids).toContain(kept.id) on the next line, over a
real store call — and the test after it asserts the eligible count
equals the in-scope, non-excluded total even when the limit is reduced,
which is the side channel rather than the list. It lives in a shared kit
every backend must pass, so the contract binds future adapters and not
just Postgres.
There is no tenancy to enforce, and the code says so before a reader can assume otherwise:
"mnemora keeps no ledger of tenants.
tenantIdis an opaque string the caller passes; it performs no existence check and no authentication."
Every read does emit WHERE tenant_id = ${ctx.tenantId},
so a correct caller is isolated — but the string is the caller's, and
the safety boundary therefore lives in the embedding application. For a
library meant to sit beneath somebody else's agent that is the right
place for it, and the same file draws the distinction that makes it
workable: the tenant is the isolation boundary, the unit of safety; the
subject is the unit of organisation within a tenant; do not confuse the
asymmetry. Anda DB makes the same separation
between a Space and a Domain, from the opposite end of the scale.
One more decision worth borrowing. MAX_STRENGTH = 1
exists because without a ceiling "a Memory written with one larger value
would dominate that tenant's recall — the same hole ADR 0036 closed for
freshness", and the constant is exported so a caller can
read that the ceiling exists and what it is. A scoring multiplier with
an unbounded tail is a self-service priority escalation, and this is the
second time the project has closed it.
2. Mental Model
A memory knows what kind of thing it is: something said, something inferred, something merged, something reflected on, something imported.
Contested is not a reason to hide.
Happened, recorded and valid between are three different clocks.
Strength has a ceiling, because the second time is when you learn that.
Diagram source
%% caption: provenance is the union tag rather than a flag, so each kind demands its own evidence; recall admits active and contested, and three time concepts are gated separately
flowchart TB
OBS["observation in"] --> EXT["extraction"]
EXT --> PROV{"Provenance — a discriminated union,<br/>not a flag on a row"}
PROV -->|"stated"| S["requires sourceObservationId,<br/>speaker, at"]
PROV -->|"inferred"| I["requires model, promptVersion,<br/>basis{memoryIds, observationIds},<br/>confidence"]
PROV -->|"consolidated · reflected · imported"| O["each with its own required fields"]
S & I & O --> MEM[("memory: status · strength (0, 1] ·<br/>occurred_at · recorded_at ·<br/>valid_from · valid_until")]
MEM --> ST{"status"}
ST -->|"active"| IN["admitted to recall"]
ST -->|"contested"| IN
ST -->|"superseded · archived · forgotten"| OUT["stored, not recalled —<br/>supersededById records what replaced it"]
IN --> TIME{"time gates"}
TIME --> EFF["effective time =<br/>COALESCE(occurred_at, recorded_at)"]
TIME --> VAL{"validAt supplied?"}
VAL -->|"no"| ALL["no validity narrowing"]
VAL -->|"yes"| WIN["valid_from <= validAt AND<br/>valid_until > validAt"]
WIN --> WHY["isExpired and isNotYetValid are<br/>separate predicates — the system can say<br/>WHY something fell outside its window"]
EFF & ALL & WIN --> SCORE["similarity × decay × tagMatch ×<br/>freshness × strength<br/>— strength capped at 1 so one row<br/>cannot dominate a tenant's recall"]
SCORE --> PACK["recall result + a recorded footprint<br/>of what the recall actually touched"]
CTX["Ctx{ tenantId, subjectId? }"] -.->|"'mnemora keeps no ledger of tenants…<br/>no existence check, no authentication' —<br/>the boundary is the caller's to hold"| MEM3. Architecture
| Package | Role |
|---|---|
core |
The model, recall runtime, provenance, outbox, strategies (33,220 lines) |
postgres |
The store adapters and their SQL (17,544) |
testkit |
Conformance suites every adapter must pass (11,238) |
openai, anthropic,
local-embedding |
Model and embedding adapters |
Inside core, the files worth their names:
provenance.ts, recall-footprint.ts,
recall-output-validation.ts,
idempotent-create.ts,
event-retention-purge.ts, digest-band.ts,
ann-truncation.ts.
4. Essential Implementation Paths
packages/core/src/provenance.ts:1-40 — the union, and
the argument for it being a union.
packages/core/src/memory.ts:5-30 — five statuses and a
ceiling with its history.
packages/postgres/src/memory-store.ts:999-1050 — three
clocks, two admitted statuses, one predicate shared with the count.
packages/core/src/ctx.ts:1-20 — what the tenant is and
is not.
5. Memory Data Model
Content, status, strength, the two timestamps, the validity window,
supersededById, and the provenance union. Creation is
idempotent on a content hash, and status changes take an
expectedStatus so two writers cannot both believe they
performed the transition.
6. Retrieval Mechanics
total = similarity × decay × tagMatch × freshness × strength,
documented in docs/recall.md, with every factor bounded and
the bounds exported. A digest band returns a compact list alongside the
full results, and a recall footprint records what the recall touched —
which is what makes a later question about why something was recalled
answerable.
7. Write Mechanics
Observations are extracted into memories; background cognition runs through an outbox rather than inline, with an inline scheduler for tests. Forgetting is a status, not a delete — and nothing is consulted at write time against a forgotten memory, so the same content can be created again afterwards.
8. Agent Integration
Deliberately none of its own. The positioning — beneath LangGraph, Mastra or a hand-written agent — is unusual in this corpus, where most projects want to own the loop, and it is what makes the tenant decision coherent: a library one layer down should not invent an authentication model for the application above it.
9. Reliability, Safety, and Trust
The typed provenance is the trust mechanism and it is the strongest version of that idea here: not a field a writer may fill, but a tag a writer must choose, with each choice demanding its own evidence.
What is absent: no record of what a memory said before it changed, beyond the supersession link; no audit of who changed a status; and no authentication, by design.
10. Tests, Evals, and Benchmarks
1,212 test cases across 127 files, with the conformance kit as the interesting part — store behaviour is specified once and every adapter is held to it, which is why the negative-evaluation mark attaches to a contract rather than to an implementation. Nothing was installed or run for this reading.
11. For Your Own Build
Make provenance the tag, not a flag. A discriminated union whose arms demand different evidence turns "we forgot to record where this came from" into a compile error.
Spell a closed union once. This project found the same five values copied into a query schema by hand and wrote down why that always fails eventually.
Let contested stay in recall. Suppressing a disputed memory is a decision made at write time by whoever wrote last; surfacing it is a decision the reader can make with both halves in front of them.
Bound every multiplier and export the bound. An uncapped factor in a ranking formula is a way for one row to outrank everything else, and the fix costs one constant.
And say what your isolation does not do. "No existence check, no authentication" in the file that defines the tenant is worth more than a tenancy chapter that implies otherwise.
12. Open Questions
Whether a forgotten memory can be re-created. Nothing consults status at write time, and idempotency is keyed on a content hash whose interaction with a forgotten row was not traced.
What the recall footprint is used for downstream. It is recorded per recall; which surface reads it was not established.
How reflection differs from consolidation in practice. Both are provenance kinds and both are background passes; the boundary is drawn in the docs rather than in the types.
Appendix: File Index
| Path | What to read it for |
|---|---|
packages/core/src/provenance.ts:1-40 |
Provenance as a union, and the duplication that taught them |
packages/core/src/memory.ts:5-30 |
Five statuses, and a ceiling closed twice |
packages/postgres/src/memory-store.ts:999-1050 |
Three clocks and two admitted statuses |
packages/core/src/ctx.ts:1-20 |
An isolation boundary that declines to authenticate |
packages/testkit/src/memory-store-conformance.ts:4238-4262 |
A negative assertion, its control, and the count beside it |
History
2026-09-16 — 3676311e…
— first reading, at a commit dated 16 September 2026. Screened before
opening, from a shallow clone: nineteen files scanned, no auto-run
surfaces, no build-time execution points, three unpinned surfaces and
nine dependency files inside the seven-day cooldown. Nothing was
installed, built or run.