# A memory ledger that can prove nothing was edited and still forget How an append-only, hash-chained memory ledger can still honor deletion. Memory That Forgets Bike4Mind Engineering A memory ledger that can prove nothing was edited and still forget How the memory layer keeps a tamper-evident, hash-chained event log and still honors "delete my data": salted commitments, per-principal keys that can be destroyed, a fence against in-flight writes, and beliefs that cool on their own. By Bike4Mind Engineering October 15, 2026 engineering · memory · security Opt-in today 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. A four-event ledger chain with one event shredded Four events, each with a hash, a prevHash pointing at the event before it, and a commitment. Event 3 has been shredded: its fact and salt are gone, drawn as a dashed empty slot, but its commitment, hash and prevHash remain, and the links between all four events still verify. event 1 hash h1 prevHash null commitment c1 fact + salt: kept event 2 hash h2 prevHash h1 commitment c2 fact + salt: kept event 3 hash h3 prevHash h2 commitment c3 fact + salt: gone shredded: true event 4 hash h4 prevHash h3 commitment c4 fact + salt: kept green: each prevHash still matches the hash before it h3 is computed from c3, never from the fact, so it is the same before and after the shred Shredding event 3 empties its fact and salt. Every hash and link in the chain still checks out. 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.