A memory ledger that can prove nothing was edited and still forget
October 15, 2026
By Bike4Mind Engineering · also on app.bike4mind.com

An AI that remembers things about you has two obligations that pull in opposite directions. It has to be auditable: you should be able to see what it believed, when it started believing it, and be sure nobody quietly edited the record afterward. And it has to honor "delete my data", which means the fact is actually gone, not hidden behind a flag. A log that nobody can alter and a log you can remove things from are, on first look, the same log wanting two different things.
This post walks through how the memory layer in the open-source Bike4Mind core reconciles them: a hash-chained event ledger where a single fact can be destroyed and the chain still verifies.
Where this stands today
Mementos 2.0, the ledger-backed memory described here, is in the product but opt-in. It is an experimental per-user setting (enableMementosV2) with no workspace-wide switch yet, so it is not on for anyone who has not turned it on. For a user who has, chat recall reads from the ledger, new memories are written to it, and the memory endpoints under /api/memory can read, append to, and delete it. Data lakes also keep a separate lake memory in the same ledger, enabled per lake. The older V1 mementos still exist next to it, and for a V2 user the two are read together.
Why the obvious designs fail
Start with the plain version: each event holds a fact, and each event's hash covers its contents and the previous event's hash. Change anything, or remove or reorder an event, and every later hash stops matching. That gives you tamper evidence for free.
Now a user asks to forget one fact. If the hash covers the fact, removing the fact changes the event's hash, which breaks the next event's link, which breaks everything after it. You can rewrite the chain from that point, but then the chain no longer proves anything: "nobody edited it" and "we edited it to delete your fact" are indistinguishable.
The other option is to keep the fact and mark it deleted. That is not deletion. The text is still in the database and in every backup of it.
A tempting middle path is to hash the fact and have the chain cover only that hash, so the fact can go and the hash can stay. This is closer, but a bare hash of a short, guessable sentence can be confirmed offline. If the fact was a name, a diagnosis or a city, an attacker with the chain tries candidates and compares hashes. The deleted fact is recoverable by anyone who can guess it.
Salted commitments
The ledger does the middle path with two changes. Each event gets its own random salt, and the chain covers a commitment, sha256(salt + fact), instead of the fact. This is the code, trimmed:
const commit = (salt: string, fact?: string): string =>
sha256(`${salt} ${JSON.stringify(fact ?? null)}`);
const chainCanonical = (e: MemoryEventInput, commitment: string, prevHash: string | null): string =>
JSON.stringify([
e.principal.kind, e.principal.id, e.kind, e.subject,
commitment, // stands in for the fact
e.evidenceTier ?? null, e.salience ?? null, e.at,
(e.sources ?? []).slice().sort(),
prevHash,
]);
The canonical form is an array, not an object, so field order is fixed by construction. An event's hash is the sha256 of that array, and it doubles as the event's id. The fact never enters the hash, only its commitment does.
Shredding removes the fact and the salt, and nothing else:
export function shredEvent(event: MemoryEvent): MemoryEvent {
const { fact: _fact, salt: _salt, ...rest } = event;
return { ...rest, shredded: true };
}
Dropping the salt matters. The salt is stored in plaintext next to the commitment, so if it stayed, a guessed fact could still be checked against it. With both gone, what remains is a hash of something that no longer exists anywhere in the record. The commitment, the hash and the prevHash link all survive, so the structure of the ledger, including when each event happened and what evidence tier it carried, is unchanged.
verifyChain walks the events in order. For each one it checks that prevHash matches the previous event's hash and that the event's hash matches a recomputation over its stored commitment. If the fact is still present, it also checks the fact against the commitment, so a tampered plaintext is caught. A shredded event has no fact to check, so that step is skipped and the commitment stands on the chain hash alone. The check needs no keys.
Destroying the key, not just the text
Clearing a field in the live database is not enough if copies of that database exist. So in production the fact is never stored as plaintext. It is encrypted with AES-256-GCM under a data key that belongs to one principal (a user, an agent, or a data lake), and the key lives in its own collection. The subject of each fact, which would otherwise reveal what the fact was about, is stored as an HMAC derived from that same key. So is the embedding vector, because an embedding can be inverted to approximate the text it came from.
"Delete my data" does the irreversible step first: it destroys the principal's key. Every ciphertext sealed under it, in the live database and in any older copy, can no longer be decrypted. Then it clears the payloads and marks the events shredded. Deleting one memory instead of all of them clears just that belief's events and leaves the key alone, so the user's other memories stay readable.
The race that recreating a key would cause
Destroying a key opens a problem. The next write for that principal looks for a key, finds none, and mints a fresh one. Suppose a background job was already running when the user pressed delete. It finishes after the shred, mints a new key, and writes its facts under it. They decrypt normally, nothing marks them shredded, and the next chat reads them back. The delete reported success while work already in flight put the data back.
The fix is a timestamp, not a lock. Destroying a key does not delete its row. It removes the key and leaves a tombstone with a destroyedAt time. Anything that wants a key passes the moment its own unit of work began, and getOrCreate refuses if the last shred is not strictly older than that moment. Work that started before the shred gets nothing and writes nothing. Work that started after it is an ordinary rebuild of an erased principal, and is allowed to mint a new key.
The comparison is strict on purpose. Timestamps collide at millisecond resolution, and the two errors are not the same size: letting an ambiguous case through could resurface erased data, while refusing it costs one run that writes nothing and gets retried. The fence applies on the fast read path as well, so a caller whose work predates a shred cannot be handed a key that something else minted after it. Concurrent first writes are settled by a unique index on the principal, with the duplicate-key error resolved by re-reading the winner.
Beliefs are a fold over events
The ledger stores events: assert states a claim, affirm re-witnesses it, retract removes it from view. What the system currently believes is not stored. foldEvents computes it, as a pure function of the event stream, so the same ledger always folds to the same beliefs. When an agent tidies its own memory, that is a recomputable projection anyone can audit, not a private edit. Each belief carries the hashes of the events behind it in derivedFrom, so a recalled fact traces back into the chain. A shredded belief folds to the placeholder [shredded], with its provenance intact.
Old memories cool without a cleanup job
Salience, whether a belief is hot, warm or cold, is also computed in the fold, using ACT-R's base-level activation. Every assert and affirm of a belief is a presentation. With dt the days since each one:
B = ln( sum over presentations of dt^(-d) ) d = 0.5
Recent presentations count for more, and more presentations add up to more. With the defaults, a belief is hot above 0 and warm above -1.2. A belief presented once a day ago sits right at the hot line, one presented once a week ago scores about -0.97 and is warm, and one a month ago scores about -1.70 and is cold. Present it twenty times and it climbs back. Nothing runs to demote old beliefs; the fold takes a read time and the score falls as time passes. Time is floored at half a day so a just-written event does not blow up the sum. A re-assert of the same subject appends to the presentation history instead of resetting it, so frequency counts, and a retract clears the history.
Data lakes override one number. Their facts come from reference documents that are often months old, so they use a decay of 0.1 and stay warm for years; corroboration by many documents, rather than recency, moves them up.
What the tests check
The same inputs give a byte-identical chain; the genesis event has a null prevHash; reordering sources does not change an event's identity; altering a fact makes verifyChain report the exact index where it breaks; and deleting a middle event breaks the link at the event after it.
The shred tests build a three-event chain, shred the middle event, assert the fact is gone and the event is marked shredded, and assert verifyChain still returns { ok: true, brokenAt: -1 }. Another folds a shredded belief and checks it reads [shredded] while both event hashes remain in derivedFrom. A third seals the same fact under two different salts and checks the commitments differ and neither contains the text.
The key-store tests cover the fence directly. After a destroy, a caller whose work began before the shred gets null, and nothing has been minted by the failed attempt. A caller that began after the shred can re-key. A key re-minted after a caller started is refused to that caller. A shred stamped in the same millisecond as the start is refused. Two concurrent lifts of one tombstone produce exactly one key, and an unscoped destroy is rejected instead of shredding an arbitrary tenant's key. The fold's activation tests check that a recently affirmed belief outranks a stale one, that the same ledger goes colder when the read time advances, and that twenty re-asserts score higher than one.
Limits
Shredding is final by design. Once the key is destroyed there is no recovery path, for us or for the user. The chain also keeps the shape of a shredded memory: that an event happened, when, under which evidence tier and from which source ids. It does not keep what was said, and the subject is stored as an opaque HMAC tied to the destroyed key, but the record that something was remembered remains, because that is what lets the chain prove it was not rewritten. And as above, the whole system is opt-in today.