Skip to main content

mneme: Source provenance, reproducible ranking, and drift checks.

mneme

Accountable agent memory. Mneme records source provenance for stored memories, returns recall receipts that reproduce ranking, and detects source drift when checks run.

Install

Released v0.5.0 wheel

The public v0.5.0 wheel is the current released package. It covers the released memory, recall, drift, provenance, local-origin freshness, and MCP Crucible export/replay workflows documented below, and adds a forget that erases the raw turns and every derived form, mneme status and mneme doctor.

python -m pip install flywheel-mneme

flywheel-mneme is the HarperZ9 distribution, published with PEP 740 attestations. The bare name mneme-memory on PyPI belongs to an unrelated project.

To check the bytes yourself rather than trust the index, install the release wheel directly:

python -m pip install "https://github.com/HarperZ9/mneme/releases/download/v0.5.0/flywheel_mneme-0.5.0-py3-none-any.whl"

Source install

For development from a source checkout:

python -m pip install -e .

For a non-editable install from the public source repository:

python -m pip install "flywheel-mneme @ git+https://github.com/HarperZ9/mneme.git"

Zero runtime dependencies · local by default · deterministic · fair-source. The store stays on your machine; text you send through an LLM extractor, an embedder or an MCP client goes to that model's provider under its terms.

Why it matters

Agent memory systems need evidence for two operational questions:

  • Why did you recall this memory? Mneme returns the ranked hits, component scores, and fusion rule so the ranking can be reproduced.
  • Is this memory still grounded in its cited source? Mneme records source hashes and re-checks them to detect drift, missing sources, or unverifiable grounding.

Mneme stores that evidence with the memory workflow instead of leaving it as a separate operator note.

The 4-tier memory model

L0 turn      raw dialogue                 -> stored verbatim
L1 atom      atomic user facts            -> extracted, each bound to its turn
L2 scenario  scene blocks of related atoms
L3 persona   the user profile             -> synthesized, citing its atoms

Retrieval is hybrid: BM25 (pure Python, always on) fused with an optional embedding channel by Reciprocal Rank Fusion, with no required embedding API.

Eight stages from a raw turn to a receipt, ending in reproduced or did not reproduce.

Accountability features

A recall you can re-derive. Every recall returns a receipt with the ranked hits, their BM25 and vector scores, and the exact fusion rule. And verify_recall ships the check: it re-runs the scorer over the same rows and confirms the ranking, so a fabricated or tampered recall is caught even if its definition hash still matches, and a store that changed no longer reproduces. The recall is auditable by a function you can put in CI, not a claim you take on faith.

from mneme import recall, verify_recall

r = recall("deploy steps", rows, strategy="hybrid", embedder=embed)
assert verify_recall(r, rows, embedder=embed)   # re-derived from the store, not trusted
mneme remember chat session.json --user alice
mneme recall "where does the user live" --user alice --json
# -> {"schema":"mneme.recall/1","hits":[{"memory_id":"…","bm25":2.14,"fused":…}],
#     "recheck":"mneme recall --query Q --state DB  (re-run the scorer, reproduce the ranking)"}

A drift check for source changes. drift re-derives every memory's grounding against the current store: MATCH (source present and unchanged), DRIFT (a source changed under the memory), UNVERIFIABLE (a source is gone).

mneme drift            # -> {"overall":"DRIFT","drifted":["…"], …}  exit 1 on drift

Eight stages from a stored memory to a verdict of match, drift, or unverifiable.

Two details make that verdict hard to fake. A memory row has to reproduce its own content hash before any of its sources are looked at, which catches a direct edit of the text, the source list, or the criterion. Each cited source is then re-hashed from its actual fields rather than read back from the hash stored beside it, because trusting that stored value would let someone edit the database directly, leave a stale hash in place, and collect a MATCH. The check re-derives on both sides before it will agree with itself.

The three verdicts are also ordered when they roll up across a store: any DRIFT makes the whole report DRIFT, otherwise any UNVERIFIABLE makes it UNVERIFIABLE, and only a clean sweep reports MATCH. That order fails closed. A memory whose source has been deleted is never rounded up to a match on the grounds that nothing contradicted it, so absence of evidence is reported as absence rather than as agreement.

Nine conditions a memory's grounding check can land on, one to a row, each with the verdict it produces. Four produce DRIFT: unreadable provenance, a memory row edited in place, a source whose bytes disagree with the address it carries, and a source that hashes differently than it did at extraction. Four produce UNVERIFIABLE: a missing memory, a memory citing no sources at all, a cited source that has left the store, and a source present but never snapshotted. One produces MATCH: all cited sources are present and re-hash to what was recorded. The row for a source whose bytes disagree with the address stored beside it is accented, because that is the one case a check reading only the stored address would call a match.

Nine conditions reach one of those three verdicts, and the drawing above lists every one of them. Four resolve to DRIFT and four to UNVERIFIABLE. Exactly one reaches MATCH, which is the shape of a check that has to earn agreement rather than assume it.

Provenance on every memory. Every atom names the turn it came from, the extractor, the criterion, and a content hash. The persona is not free text: it cites its atoms, so it is drift-checkable too.

Library

from mneme import AgentMemory

mem = AgentMemory("mem.db")                       # or ":memory:"
mem.remember("chat", [{"role": "user", "text": "I live in Denver and love dark roast."}],
             user="alice")

receipt = mem.recall("coffee preference", user="alice")  # RecallReceipt, re-derivable
print(mem.drift()["overall"])                     # MATCH until a source changes

An embedder (AgentMemory(..., embedder=fn)) turns on the vector channel; an LLM Extractor plugs in for richer atoms. Neither is required: the deterministic floor works with no model and no API.

Where your memory lives

The CLI takes --state before the command (mneme --state mem.db recall …), and the MCP server reads MNEME_STATE. Without either, mneme uses mneme.db in the directory it runs in, so a command run in another directory starts another database. From 0.5.0, two commands show where it is:

mneme status    # absolute path, default or not, files, row counts, snapshot directory
mneme doctor    # the same, plus the audit chain; exit 1 when something needs you

Both open the database read-only: they never create a missing database and never write its rows (on a WAL database SQLite can create the -wal and -shm files, as any reader does). They warn when the database sits inside a git work tree, where, unless git ignores it, one git add . stages your memory for the next commit, when an older mneme has reopened it, when replay snapshots were left behind, and when an erase has not finished. The MCP mneme.doctor tool reports the path facts without opening the database, with your home directory written as ~.

Replay snapshots are full copies of the database. From 0.5.0 they live in a per-user state directory, <LocalAppData>/mneme/snapshots on Windows and $XDG_STATE_HOME/mneme/snapshots (or ~/.local/state/mneme/snapshots) elsewhere, never beside the database. mneme status counts them. forget and mneme scrub delete the copies of their store, and snapshots whose process is gone are swept when a store opens for writing, when the MCP server starts, and when a snapshot is made. A delete is a plain unlink, so the disk blocks can keep the bytes until the file system reuses them.

Mneme keeps everything above on your machine. What leaves it is what you send: turns given to an LLM extractor or an embedder go to that model's provider, and so does any result an MCP client reads (recall, provenance, forget previews). An erase cannot reach those copies.

Use one mneme version per database file. An older mneme that reopens a database a newer one wrote gives the rows it writes only its own, older guarantees. From 0.5.0 mneme records when that happened, and mneme doctor reports it.

The ecosystem: memory that traces to its source

Point mneme at an accountable intake tool (gather, the sibling flagship) and the provenance chain can run end to end:

origin ref --(intake sha256)--> mneme turn --> mneme atom --> recall
mneme ingest research items.json --user alice     # gather-shaped {id,text,source,ref,method,sha256}
mneme recall "where is the user based" --user alice
mneme chain <memory_id>              # -> the supplied origin ref + intake hash
mneme origin-recheck <memory_id> --allowed-root docs/

An agent that remembers what it researched, and can prove a recalled memory traces to the receipt supplied by its intake tool. For supported local Gather docs receipts, origin-recheck can re-read the operator-approved file under --allowed-root and compare Gather's normalized decoded text hash. Legacy receipts do not prove raw byte integrity, and unsupported refs remain UNVERIFIABLE rather than silently promoted. origin-recheck opens Mneme state read-only, refuses local path aliases and unsupported refs, and reports MATCH, DRIFT, or UNVERIFIABLE without including source content in the report. Any intake tool that emits the receipt shape composes; mneme never imports gather. Named-user remember and Gather ingest derive source turn IDs from the user, session, supplied item/turn ID, and for Gather the origin hash. The shared default user keeps the legacy raw-ID namespace, except new default-user writes cannot use Mneme's reserved internal source ID prefix.

And the loop closes at the other end. mneme to-crucible emits a schema-v2 crucible export: each memory is a claim paired with Mneme's source-bound drift measurement. Crucible independently recomputes and seals MATCH, DRIFT, or UNVERIFIABLE from that measurement. Each exported measurement now carries a declarative mneme.recheck/1 descriptor. After Crucible writes an assessment-bound replay template, Mneme can re-read the supplied state and fill its replay pack without importing Crucible or embedding a database path or executable command in the descriptor:

crucible recheck REGISTRY --template replay-template.json
python -c "import sqlite3; s=sqlite3.connect('file:mneme.db?mode=ro', uri=True); d=sqlite3.connect('mneme-replay-snapshot.db'); s.backup(d); d.execute('PRAGMA journal_mode=DELETE'); d.close(); s.close()"
mneme --state mneme-replay-snapshot.db replay-crucible replay-template.json --out replay-pack.json
crucible recheck REGISTRY --pack replay-pack.json --json

The replay command fails closed when the assessment triple, claim binding, descriptor, original measurement contract, or target memory grounding differs. Ordinary source drift remains a replay result (1.0); a missing source remains unverifiable (null). Crucible still does not independently re-read Mneme's source. The source recheck is Mneme-owned, and Crucible verifies that the replayed measurement exactly reproduces its sealed contract.

The command consumes crucible.replay-template/1 from a caller-owned, quiescent, single-link rollback-journal snapshot. The example uses SQLite's backup API to materialize one; keep that file unchanged until replay returns.

Open the source read-only when you take that snapshot, exactly as the example does. A read-write handle on a WAL database whose writer exited without a clean close will recover and checkpoint it: measured on Windows, that rewrote the main file and deleted both sidecars. The read-only handle leaves the main file and the WAL byte-identical. It can still update the -shm index, because SQLite readers coordinate through shared memory. Stop source writers first when even that is unacceptable.

Replay refuses WAL, SHM, or journal sidecars and hardlink aliases, fingerprints the source around a consistent private SQLite backup, and reads only that process-owned copy in immutable mode. It then verifies and preserves the compact descriptor-only crucible.replay-set/1 binding, and emits crucible.replay-pack/1. The binding records descriptor and skipped-row counts without disclosing descriptorless assessment rows. Historical schema-less templates remain compatible only when they have no replay binding and their complete measurement seal reproduces; bound templates require the canonical schema. Read-only schema compatibility is checked without migration, and a completed, synced pack is published atomically without overwriting an existing path. Output paths that alias the state database or a standard SQLite sidecar are rejected. Malformed provenance is rejected before descriptor or pack creation. Replay does not modify the supplied snapshot or its sidecar namespace. Changes detected while the private copy is created fail the handoff; later source changes cannot affect that copy.

Library callers use the same contract explicitly and always close the private snapshot owner:

memory = AgentMemory(
    "mneme-replay-snapshot.db",
    read_only=True,
    immutable_snapshot=True,
)
try:
    pack = memory.replay_crucible(template)
finally:
    memory.close()
gather (intake) --> mneme (drift + replay) --> crucible (sealed recomputation)

The export keeps measurement and assessment separate without claiming independent source certification.

Accountable forgetting

From 0.5.0, forget erases a memory, the turns it came from, and everything derived from them: memories that cite an erased turn or memory, scenario and persona rows, the fact's supersession history, and the source turns of near-duplicates that consolidate merged into it. Two kinds of extra rows need your consent. Collateral rows are other memories taken from the same turn. Duplicates are rows of the same user that repeat an erased text whole, such as a second turn that said the same sentence. The CLI shows both and asks, and the library refuses both unless you pass allow_collateral=True.

mneme forget <memory_id> --dry-run               # the plan: every row it would erase
mneme forget <memory_id> --reason "user asked"   # shows the plan, then asks
mneme forget <session> --session --yes           # a whole session
mneme forget <turn_id> --turn --yes              # a turn, e.g. one an old forget left
mneme audit          # -> {"entries":…,"chain_intact":true,"log":[{"op":"erase", …}]}

The plan shows row text only on a terminal or with --show-text, because a plan printed to a pipe can reach an agent's model. --dry-run changes nothing, not even an older database's version stamp.

Each erased row leaves one entry in the hash-chained audit log. The entry names a random erase ref and stores a salted commitment to the erased text. The salt is never stored, so the entry cannot confirm a guess of what was erased. --emit-opening prints the salts once; run it yourself, not through an agent. The reason is stored verbatim, so a reason that repeats erased text, or names an erased row's id or content hash, is refused. The check catches verbatim repeats only: a paraphrase or a spaced-out spelling passes.

The rows go in one transaction with secure_delete on. Mneme then vacuums the file, removes this store's replay snapshots, and scans the database files and known copies for the erased bytes. The receipt's status is erased only when every check passed, and findings names each one that did not:

  • erased_residue_found: the store still holds erased text, for example in another user's row, in an earlier audit reason that quoted it, or in the source turn of a near-duplicate an older mneme merged away without a link.
  • erased_copies_remain: a replay snapshot outside this store's directory, or one removed while its process still ran, holds erased text.
  • incomplete: a store file or a known copy could not be read.
  • erased_sources_kept: you kept the source turns (--keep-sources).
  • erased_unverified: the rows are gone, but the scrub or the scan did not finish. mneme status and mneme doctor warn until mneme scrub finishes it.

The CLI exits 3 for every status except erased and erased_sources_kept. The receipt also names what an erase cannot remove. Audit rows written earlier still name the erased rows by content-derived id, and such an id confirms a guessed text when its source turn id is known. The disk blocks SQLite released, and those of deleted snapshots, can hold old bytes until they are reused. Exports, backups, other copies of the database file, and text already sent to a model provider are out of its reach.

update edits a memory's text while keeping its provenance, and supersede closes a fact while keeping it for history. From schema 5 their audit entries hold salted commitments to the old and new versions instead of plain hashes. The salts sit in the store under the memory they describe, so the history can be checked while the memory lives. An erase deletes the salts with the rows, and the commitments then open to nothing. The entries keep the memory's content-derived id, which the receipt counts. Entries written before schema 5 keep plain hashes, and the erase receipt counts them. Tamper any entry and the chain breaks.

In 0.4.2 and earlier, forget deleted the memory row only and left the raw turn in the store. mneme status counts those forget entries and the turns no memory cites, and mneme forget <turn_id> --turn erases such a turn.

Agents plug in over MCP

mneme mcp          # JSON-RPC 2.0 over stdio; MNEME_STATE points at the DB

The released v0.5.0 wheel exposes the MCP memory, recall, drift, provenance, origin recheck, forget, audit, status, doctor, Crucible export, and Crucible replay tools.

From 0.5.0, mneme.forget takes two steps. A call with only memory_id returns the plan (targets, row ids and counts, the number of users but not their names, with text previews only when include_previews is set) and deletes nothing. A second call with confirm_plan_sha256 applies exactly that plan and returns the receipt, without the commitment openings. When the plan lists collateral or duplicate rows, the second call also needs allow_collateral: true. The model that asked for the plan can send the digest too, so a host that launches mneme for an agent should require an owner-granted step for this tool.

From 0.5.0, mneme.doctor also returns the database's absolute path (home written as ~), whether it came from MNEME_STATE, the replay snapshot directory and any warnings, without opening the database.

MCP tools mneme.to_crucible and mneme.replay_crucible reuse the same replay library boundaries as the CLI. A recall through MCP returns the same re-derivable receipt, so the agent (or its operator) can see and re-check why a memory was surfaced; the accountability travels with the tool result. mneme.to_crucible returns the existing mneme.crucible-export/2 object from the server-bound MNEME_STATE; MCP callers must pass either user to select one tenant inside that configured state, or all_users: true to deliberately export every tenant visible to the server. Optional session filters must be non-empty strings; layer is limited to L1, L2, or L3. The user value is a selector, not an authentication boundary; the host still owns which state DB the server may open. Crucible export and replay fail when MNEME_STATE is unset or empty. mneme.replay_crucible consumes a decoded crucible.replay-template/1 object and returns crucible.replay-pack/1 from the same explicitly configured state. State paths and executable commands stay out of the untrusted recheck descriptors. All-row templates (skipped_count: 0) are checked against the assessment measurement seal. Mixed templates with skipped rows are refused until the template carries a verifier-enforced full denominator that binds the disclosed descriptors, skipped count, and undisclosed rows to the assessment; Mneme cannot authenticate undisclosed rows from an external assessment from a caller-recomputed binding alone.

Benchmark you can re-run

Token-reduction benchmarks are more useful when paired with answer-retention checks. Mneme reports both for the included benchmark.

mneme bench
# token_reduction: 76.6%   (full history 125 tok -> avg recalled 29 tok)
# answer_recall:   100%    (5 probes, every needed fact survived the reduction)

The included reduction is reported alongside answer recall, so a run that forgets required answers is visible in the result. The receipt carries the per-probe detail and the exact token estimator, so a third party can re-run the measurement over the same conversation and compare the number. Point it at your own conversation with --turns convo.json --probes probes.json.

Scenarios (L2)

mneme scenarios alice     # cluster the session's atoms into scene blocks

Atoms sharing a theme cluster deterministically into L2 scenarios; each scenario cites its atoms, so it is drift-checkable too (a scenario whose atom is gone is UNVERIFIABLE, never silently kept).

Guarantees

  • Zero runtime dependencies (stdlib sqlite3). pytest is the only dev dep.
  • Deterministic core. Memory rows, their hashes and default rankings are derived from the supplied turns, so the same input rebuilds the same memories. Some values are random on purpose: the store id that names the replay snapshot directory, the refs and salted commitments in the audit entries an erase writes, and the salted commitments in update, supersede and row-level forget entries. The erase entries cannot confirm a guess of what was erased. The other entries keep a content-derived memory id, which can, and the erase receipt counts them.
  • Tests are the contract. The core workflows above have regression coverage with false-success controls for recall, drift, audit, and ingestion.

Development

For a local development checkout:

python -m pip install -e ".[test]"
python -m pytest

Use synthetic SQLite state for tests and examples. mneme mcp reads MNEME_STATE; do not point examples, demos, or interop checks at a live user database. Release publication remains gated by DELIVERY.md, CI, version/tag alignment, and explicit operator action.

License

Mneme is fair-source: open to read, run, and build on, with commercial use reserved so the project can fund its own development. See LICENSE.

What this believes

This tool is one lane of a family that holds a single belief steady across every surface: knowledge open to anyone who can attain the means; acceptance decided by external checks, never reputation; every result re-runnable; honest nulls first-class; ownership earned by comprehension; learning woven into the work. The full text lives in CREDO.md. The long form of this belief: The Unbundling.


Zentropy Labs · order out of entropy. An independent lab building evidence-first tools that leave a re-checkable artifact behind. Built by Zain Dana Harper in Seattle. The full workbench is at Project Telos.

Metadata

Release files for flywheel-mneme 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flywheel-mneme 0.5.0
File Size Uploaded
flywheel_mneme-0.5.0.tar.gz 177.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flywheel-mneme 0.5.0
File Interpreter ABI Platform
flywheel_mneme-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 299.5 kB

Release files / flywheel_mneme-0.5.0.tar.gz

Download URL flywheel_mneme-0.5.0.tar.gz
Size 177.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c7664405b730e65b9a0dba73616ef3b1e4aae4b0023b443d690c8790bbbdc4f1
BLAKE2b-256 checksum
How to use checksums
3c9fc1be01e51c1c29ecf458c878c3d28974118ae2ea4c1827dac35ee5bc8374
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / flywheel_mneme-0.5.0-py3-none-any.whl

Download URL flywheel_mneme-0.5.0-py3-none-any.whl
Size 121.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c71d74d9aa23707c02fec19e95b82bf383bb4fd411c8d053d26484dfbd94b88c
BLAKE2b-256 checksum
How to use checksums
6a0963ce12a397ae2086befbc52702bf4c175649efac17851c5a46d689f0b41f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

This release

0.5.0 This release

2 release files

0.4.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page