This is not an established best practice. 51 systems of 555 carry it, and almost no two arrived the same way: one invented it under adversarial pressure, one adopted it from the first, one arrived at a weaker form independently, one was driven to it by a regulation, several built it only after their report named its absence — one of them adding it in the same release that closed a scope hole the same report found — one has it as a side effect of a lookup that forgot to exclude the rejected row, one made that same collision deliberate, one began as that side effect and was rebuilt as an explicit refusal, one built it as ordinary plumbing in its write gate, and one hardened it against key rotation. Sorted by mechanism rather than by mark, twenty-one refuse the write — the table below says which, and what the rest do instead. There is no consensus behind this page and no library that provides the mechanism. There is now a vocabulary: arXiv:2605.26252 states it as a correctness condition — no superseded value becomes current — and proves that append-only storage cannot satisfy it, which is this page's argument in one line and from the database side. One vision paper with a prototype is not adoption. Everything below is still an argument, and the provenance is traced under Seen in the atlas so you can weigh it as one.
Intent
Remember not only what the system currently believes, but also which values were deliberately rejected. Use that negative knowledge during future writes.
The name collides with two others, and the difference is the whole pattern
A reader arriving from distributed systems or from ordinary CRUD will recognise the word and read the wrong thing, so it is worth separating three mechanisms that share it:
| Keyed on | Lifetime | Purpose | |
|---|---|---|---|
| Cassandra-style tombstone | a row or cell | until compaction garbage-collects it | propagate a delete across replicas |
| Soft delete | a row, via deleted_at |
usually forever, but invisible to queries | hide a row while keeping it recoverable |
| Rejected-value tombstone | the value, normalised | outlives the row it came from | refuse the value when something tries to write it again |
The first two are keyed on a record and exist to make a deletion take effect. This one is keyed on the content and exists to make a deletion stay in effect against a writer that has never seen the old row — a re-extraction from a retained transcript, a sync from an unchanged upstream file, a model rediscovering the claim in a later conversation. Deleting the row does not help there, because the new write creates a different row saying the same wrong thing.
If a better name exists, this atlas has not found it, and the term is used here with that collision acknowledged rather than hidden. What matters is the key, not the word.
A third collision is inside the corpus rather than outside
it. PLUR1BUS ships
tombstoned as one of seven record statuses, reachable by an
authorized human command, and a record in that state scores
-Infinity and never reaches recall. That is a well-built
soft delete under this pattern's name: it is keyed on the record, so
re-capturing the same sentence produces a new card in
candidate that nothing checks against what was tombstoned.
The system that uses the word is not thereby the system that has the
mechanism, which is the reason this table exists.
The problem
Deleting or superseding a wrong memory removes it from normal recall, but it does not stop the same value from returning. A later conversation, stale document, model extraction, or synchronization pass can rediscover the old claim and create it as if it were new.
This is memory laundering: history that the system already judged wrong re-enters through a different write path.
The pattern
Store a durable tombstone keyed by the semantic identity of the rejected value:
scope + subject + predicate + normalized value
The tombstone records why, when, and by whom the value was rejected, plus the claim or evidence that triggered the decision. Normal retrieval suppresses it. Every automated write checks it before activation.
Diagram source
%% caption: the tombstone is keyed on the normalized value and consulted before dedupe, so a rejected value is blocked at the next extraction rather than re-asserted
flowchart TD
A["Candidate value"] --> B["Normalize key and value"]
B --> C{"Rejected tombstone exists?"}
C -- "No" --> D["Run dedupe and conflict policy"]
C -- "Yes" --> E["Block, quarantine, or require review"]
F["Human rejection or correction"] --> G["Write tombstone"]
G --> H["Reject or supersede active claim"]
H --> CWhy it works
A tombstone changes correction from a point-in-time mutation into a durable constraint. It prevents repeat failures across extraction runs and preserves enough history to explain why a write was refused.
It is stronger than a soft-delete flag on the old claim because the check is value-oriented. The new proposal may have a different record ID or arrive from a different source.
Tradeoffs
- Normalization mistakes can block a legitimately different value.
- Truth can change; some tombstones need expiry or explicit reactivation.
- Scope matters. A rejection in one project or user context may not apply globally.
- The tombstone itself can contain sensitive data and must follow deletion policy.
- Model writes should normally be blocked, while a trusted human correction may be allowed to override with an auditable action.
Do not use tombstones as the sole conflict model. A new competing value may deserve a candidate state rather than immediate rejection.
Cost to adopt
Build: a normalized form for values so "Berlin" and "berlin" hash the same; a tombstone table keyed on (subject, predicate, normalized value, scope); a check on the write path of every ingestion route, including background ones.
Forces elsewhere: every extractor and background job must consult the check, so a system with several write paths pays this per path. Normalization is where the real work is — too strict and the tombstone never fires, too loose and it blocks legitimate updates.
Ongoing: tombstones accumulate and need their own retention policy, and a user who changes their mind needs a way to lift one.
Skip it if nothing re-derives memory automatically. A store written only by explicit user action cannot resurrect a value on its own, and supersession is enough.
Seen in the atlas
51 systems of 555 in the atlas have this. That is still the most striking negative result in the atlas, and it is the reason this page exists.
Verel uses rejected memory records
as a correctness mechanism and protects rejected states from ordinary
pruning. RainBox stores
MemoryRejectedValue rows when claims are rejected or
superseded, and model writes check them before asserting. Daimon is the third and the weakest,
and it is instructive precisely because of how it is weaker. Provem is the fourth and the first to
arrive at it from regulation rather than from a failure, discussed
below. memsem is the fifth, refuses
the write like the first two rather than filtering the read like the
middle two, and puts the mechanism behind a human decision — discussed
below. Perseus Vault is the
sixth and the only one that refuses a value without storing it —
discussed below. Universal Memory
Engine is the ninth and the plainest — a suppression table the write
gate consults at four points, discussed below. Noosphere is the most rigorous — its
tombstone is keyed on an HMAC subject hash and checked across every
retained key version, so rotating the key cannot resurrect a revocation,
at the price of a ninety-one-one-day expiry discussed below. Wenlan puts one on the
suggestion layer rather than the fact layer: a dismissed
mind-map node keeps its row so its fingerprint stays occupied, and every
insert is ON CONFLICT … DO NOTHING. breadcrumbs is the smallest — a
JSON file of rejected values that the fact setter raises on — and it is
the only one in a repository that also argues, correctly, against
applying the same mechanism to its other store. Its commit describes the
mechanism as closing "the two remaining code-shaped gaps from the
Agent Memory Atlas round-2 evaluation". Discussed at length
below.
Where it came from: an adversary, not a designer. Verel's git history dates the mechanism to 28 June 2026, inside a numbered red-team sequence, and the commit that introduces it describes the attack it closes:
"rejection wasn't durable across supersede-then-restate: reject paris → supersede with london (rebuilds CANDIDATE, erasing the verdict) → restate paris + attest → VERIFIED.
write()now carries a durablerejected_valuesset forward across supersessions, and the gate refuses to promote any value that was ever rejected for that key."
Nobody set out to build negative memory. A red team walked a rejected value back to verified in three steps, and the tombstone is what the fix turned into.
The next three rounds are the more useful part, because each one defeated the previous fix — and they map onto the tradeoffs listed above:
| Round | What got past the tombstone | The fix |
|---|---|---|
| 8 | TTL pruning deleted the tombstone, "reopened the supersede-then-restate launder after ~90 idle days" | REJECTED made prune-exempt, like
VERIFIED |
| 9 | NFKC divergence — the gate compared
fact.text.strip().lower(), so unicode look-alikes slipped
through |
NFKC-canonical rejection |
| 12 | key collisions and an unbounded ledger | injective make_key, bounded rejection ledger |
One detail about how it reached this atlas is worth keeping, because
it is the clearest argument for reading code rather than documentation.
When the survey that became this atlas read Verel, the mechanism was
about half a day old and the README did not mention it
once — that file advertised "trust + provenance, consolidation,
and a held-out, attested promotion gate". The survey found the tombstone
in the source, along with make_key(), and
canonical_text() "shared by recall rendering and rejection
comparison" — the normalization seam round 9 had hardened hours earlier.
A README-based survey would have missed the atlas's most-quoted finding
entirely.
Round 9 is empirical confirmation of the first tradeoff on this page. Normalization really is where the work is, and it was found by attacking the mechanism rather than by reasoning about it.
Memori is the same seam reached from the opposite direction, and it is worth reading as a warning about the positive case. It builds a careful content-addressable key — implemented twice, in Rust and Python, with a comment requiring the two to agree, and unit-tested for case and punctuation insensitivity — and uses it to deduplicate facts rather than to reject them. The normalization keeps ASCII alphanumerics only, so any fact written in a non-Latin script reduces to the empty string and every such fact hashes identically. Nobody attacked it; the tests simply never passed it a non-ASCII string. Whatever a content key is for — deduplication here, refusal in Verel — normalization is the part that decides whether it works, and it is the part that looks finished long before it is. Round 8 is the same for the fourth: a tombstone that expires is not a tombstone.
The two systems are not independent inventions, and the count should be read accordingly. RainBox's git history dates its tombstone to 29 June 2026, the same day as the comparative survey that later became this atlas — a survey whose RainBox report stated plainly that "it does not implement Verel-style rejected-value tombstones", and whose recommendations listed "keep rejected tombstones". So the field has produced this mechanism once, in Verel, and copied it once — into the system belonging to the person who ran the survey.
That makes the negative result stronger rather than weaker. Two of 555 would suggest a hard idea that a few teams reach independently. One of 555, plus one adoption by a reader who went looking, suggests an idea that is not being reached at all — and that the way it spread was somebody reading another project's source.
The third is an independent arrival, and it stops short in
the two places this page predicts. Daimon's daimon forget
deletes the item from the live checkpoint and appends a
forgotten:<content-hash> event to an append-only log.
Because item ids are sha1("<field>:<text>"),
that event is keyed on the value, not on a row: an identical
re-extraction in a later session lands on the same id, is withheld from
the briefing, is not carried forward, and is deleted from the search
index across every historical checkpoint on the next rebuild. There is
no evidence the author had read Verel or RainBox; the mechanism falls
out of content-addressed ids rather than from a red-team finding.
Two differences matter, and both are on the tradeoff list above.
It is mostly suppression at read, not refusal at write. Verel and RainBox refuse the write; Daimon lets the extractor re-assert the value into a new checkpoint on disk and stops it reaching the agent. The observable behaviour is the same and the failure surface is not: every future read path has to remember to consult the fold, and the store itself holds content a user asked to forget. One write path is an exception — the supersede-candidate emitter skips values already in the ledger, which is a consultation the systems in the read-only class do not have.
Daimon's key is canonical, not literal.
normalize.canonical_text folds NFKC, strips invisible
characters, collapses whitespace, casefolds and translates
confusables, and content_key truncates a digest
under a docstring naming the direction it fails in: "a prefix
collision over-blocks, the fail-safe direction for a deletion
guarantee". That is the round-9 lesson implemented, not missed.
What still separates Daimon from Verel and RainBox is the write path,
above — not the key.
Recorded rather than silently edited, because the failure is instructive: a pattern page argues from reports, the reports get re-read, and nothing connects the two. This one was caught by re-deriving the strong-form subset, which is a thing nobody does on a schedule.
And the same system runs a second negative store beside it, which
is the sharpest illustration on this page of what the tombstone property
actually buys. Daimon's refutations.jsonl records an
approach that lost — subject, verdict, scope, cited
evidence — keyed on sha1(subject, scope), folded through
candidate, active and overturned, and activated only through a write
channel the process observed to be human. By every measure of care it is
the more elaborate of the two: adversarial tests, an authority model
that cannot be spoofed by a flag, a revision rule that demotes its own
record. And it is not a tombstone by this page's
definition, because nothing consults it on any path a value travels:
refute guard is a command an agent chooses to run, and the
skill text shipped to hosts says a hit is "advisory, not a command
veto". The forget ledger, which is far simpler, is the
one wired into the fold that every read crosses. Elaboration is not the
property. Being on the path is.
Everything else stops at supersession, archival, or deletion — mechanisms that remove a value from view without recording that it was judged wrong:
The fourth arrival came from a regulation, and it has the
most forgiving key. Provem's
forget(term, scope) deletes the matching records, appends
the term's token set to a per-tenant
erased_terms registry, and writes an erasure certificate
into the audit log. Every later recall computes the record's tokens and
excludes it with reason "erased" if any registered set is a
subset.
Two things separate it from the three above. Its key is a normalized token subset rather than an exact string or a canonicalised value, so a later record that restates the erased term inside different surrounding text is still caught — the round-9 lesson on this page, that normalization is where the work is, taken further than Daimon's canonical key takes it, at the cost of a false-positive surface nobody has measured. And it is tenant-scoped, so an erasure for one customer cannot silently censor another — the only instance here that treats the scope of a rejection as part of its identity.
It shares Daimon's limitation exactly: suppression at read, not refusal at write. A re-ingested erased value still lands in the backing store and is stopped on the way out. For a system whose stated purpose is GDPR Article 17 that is the sharper version of the same criticism — the regulation is about what you hold, and this mechanism governs what you serve.
The fifth is the only one here whose tombstone a person has
to arm. memsem parks an
uncertain fact in a memory_candidates table that no read
path joins; rejecting it writes a row into
memory_suppressions keyed on the normalised subject,
predicate, object and project, and blockedBySuppression()
is the first effective statement in add(). A suppressed
value is refused outright — no row written, no rival faded — and
memory_add_many, the tool the extraction sub-agent is
instructed to call, inherits the same check. Lifting it takes an
explicit memory_unsuppress, which is audited. The key is
trim().toLowerCase(), which is the case-and-space class
rather than Verel's NFKC canonicalisation, and expires_at
exists in the schema with nothing writing it, so round 8's lesson — a
tombstone that expires is not a tombstone — is passed by omission rather
than by decision.
What it shows is that the check and the trigger are
separable, and that building the check is the easier half.
memsem's other correction path is supersession by attenuation: a
contradicting write multiplies its rival's confidence by 0.6 and
archives it below 0.25. That path writes no suppression. Archiving a
value because it lost an argument and rejecting a candidate because a
person said no are two judgements about the same sentence, and only the
second is recorded as a rejection — so the write path a background
extractor uses reaches the store through the door with no lock on it.
Measured against the project's own milk/lactose example: an ordinary
correction is archived at the third re-assertion of the value it
corrected, and a pinned correction, whose confidence fade()
never touches, stops being the top search result at the sixth.
That is this page's argument in one system. A discount on re-entry is
paid off by repetition; a check that refuses is not. memsem carries both
mechanisms in the same file, which makes the comparison unusually clean
— and leaves the open question of whether an automatic judgement should
be allowed to write a tombstone at all, or whether a rejection is by
definition something a person does. The sixth stores the digest
and not the value, which answers a tradeoff on this page rather than
inheriting it. Perseus
Vault's rejected_value_tombstones is keyed on
(workspace_hash, subject, predicate, value_sha256) and
carries a reason, an evidence reference, an author id and an optional
expiry. The value is never written — only its SHA-256, over a form that
is JSON-canonicalised when it parses, whitespace-collapsed and
lower-cased, so a re-indented body, a re-ordered object and a case
variant all land on the same tombstone.
That directly addresses the Tradeoffs entry above — "the tombstone itself can contain sensitive data and must follow deletion policy" — which is a real problem for every other holder: a rejection record for a leaked secret is a copy of the secret. A digest refuses the value without retaining it, and the project states the property in those terms.
And it closes the hole a digest leaves, which is the one this page has not had an answer for. A digest refuses the value it was written for. A consolidation pass that summarises the rejected source writes a body with a different digest and the check never fires — so the strongest tombstone in the corpus was defeatable by the system's own maintenance loop. The fix shares one suppression check between ordinary reads and maintenance scans, on the ground stated in the code: "a derived body is not safe merely because its own digest differs from a rejected source body's digest." Derived bodies carry source references in three JSON fields, described as "intentionally hash-free provenance references, not a new authority mechanism" — so suppression propagates without the tombstone ever holding the rejected value. The walk is recursive with a cycle set and a depth cap of eight, and it fails closed at both edges: hitting the cap suppresses, and so does a source whose encrypted bytes cannot be authenticated, because "serving a derived record whose evidence cannot be checked would recreate the same bypass."
This is a further property the strong form needs, and unlike the others it is not optional for any system that derives. If summaries, consolidations or promotions write back into the same store, a value-keyed tombstone protects the original and nothing downstream of it. The limit is honest and worth stating with the mechanism: propagation reaches exactly as far as the provenance is declared, so a derived writer that omits its source ids escapes.
Three further details. It refuses at the write path
rather than filtering at read, like Verel and RainBox and unlike Daimon
and Provem, enforced in remember_impl with a comment
claiming the reach — agent remember, capture, ingest, connectors,
derived writers. Its override is an audited act, not a
bypass: a deliberate write passes allow_rejected=true and
is journaled into the same hash-chained ledger, which is the shape the
tradeoff list above asks for when a trusted human correction has to win.
And the lookup matches on predicate and digest without the
subject, so a rejected value is refused under any key in scope
— deliberate, named in a test, and the most aggressive reading of the
normalization tradeoff any holder here takes, with a correspondingly
larger false-positive surface.
The open edge is reach rather than existence: the comment names connectors and derived writers, and no committed test walks the background consolidation passes, which is the condition under which record-keyed removal stops holding and the one this page cares most about.
The seventh nobody appears to have built. Mnemosyne gives every extracted fact
a primary key of compute_fact_id — a SHA-256 over the
NFC-normalized, length-prefixed subject, predicate and object. A fact
that loses a conflict gets superseded_by set, and every
read filters superseded_by IS NULL. The part that makes it
a tombstone rather than a supersession is one clause that is
not there: the lookup at the top of
consolidate_fact matches on subject, predicate and object
without excluding superseded rows, so a later extraction of the same
value finds the dead row and updates it — raising its
mention_count and its confidence, and leaving
superseded_by exactly where it was. Nothing in the tree
ever clears that column. The rejection outlives every re-assertion, at
the write path, keyed on the value.
It is the same route Daimon took — a content-addressed id makes the
key the value — reached with none of the intent. No comment claims the
behaviour, no test pins it, and the docstrings around
consolidate_fact are entirely about concurrency. Add the
obvious-looking AND superseded_by IS NULL to that lookup
during a tidy-up and the guarantee is gone, silently, in the direction
this page says failure always looks like success. Which is the argument
for the test bullet below rather than against the mechanism: a
tombstone you got for free is one you can lose for free.
Two limits on the reach. The property covers the extracted-fact
layer, not the memory rows — there, _find_duplicate matches
exact content within a session and the dedup update preserves an
existing valid_until, so re-asserting a memory you
invalidated does not revive it, but the same text written from a
different session is a new row with no expiry. And the confidence on the
tombstoned row keeps climbing with each re-mention, so anything that
ever did clear the flag would resurrect the value stronger than it was
rejected.
The eighth is the seventh's mechanism in a different
language, and it is the better instance. Nova AI is a symbolic concept graph
with no model anywhere in it. weerleg — refute — sets
status = "rejected" on a sense, with the reason and
timestamp in that record's own audit log; the reasoning query and the
disambiguation candidate list both filter it out, while
get_senses() deliberately keeps showing it, so a person
inspecting the graph sees exactly what the reasoner refuses to use. That
split — invisible to inference, visible to audit — is the cleanest
expression of this pattern's intent in the corpus.
And it is the one case in the corpus where the accident
became a design, which is the strongest evidence this page has for its
own advice. Until August 2026 Nova held the property the way Mnemosyne still does: the
deduplication loop in add_sense matched an incoming
definition by definition text and did not exclude rejected rows, and the
status branch below it promoted only when source == "user".
Automatic re-derivation landed on the refusal and left it standing — but
a person re-teaching the same sentence set confirmed and
lifted it with nothing recorded and nothing asked.
core/semantic.py:204 now tests the rejected status
first and returns {"blocked": "rejected", …}
without touching the stored sense. The signal is carried up to the layer
holding the chat bus, where an automatic source is reported and never
applied, and a person's re-assertion becomes a spoken "I had already
rejected this — do you really want to confirm it again?" whose
ja writes a sense_reactivated audit entry. Six
cases in tests/test_tombstone.py hold each link, and the
one covering re-assertion says in its own name that it replaced a test
pinning the previous behaviour.
That is the whole argument of this page, run once by somebody who had
the shape already: the property falls out of writing dedup
against content and rejection against the same record, and it is
destroyed by the tidy-up that adds AND status != 'rejected'
to the lookup. Nova's author reached the opposite tidy-up.
Mnemosyne still claims the behaviour nowhere and pins it with no test.
If you have this shape, write the test before someone helpfully filters
it.
The ninth is the write gate itself, and it is the plainest
instance on this page. Universal Memory
Engine extracts candidates and resolves them through a gate before
anything becomes a node. Rejecting a candidate with
suppress_similar calls addSuppression with its
canonical_key; memory_suppressions is a real
table indexed on (user_id, kind, canonical_key) with an
optional suppressed_until; and the gate consults it at
four points, a hit producing
reject(obj, "suppressed_blocked"). Nothing about it is
subtle or accidental, which is the interesting part — it is what this
page asks for, built as ordinary plumbing by a project that does not
appear to have read the argument.
The cleanup pass writes suppressions too, so a deletion binds the future rather than waiting to be re-derived — the answer to the failure the MemoryOps AI entry below describes, in the same paragraph of the same kind of system.
OpenMake LLM is the
smallest instance, and it arrived by removing a filter. Its
memory is one table of sentences with an is_active flag; a
delete flips the flag and leaves the row. The duplicate check that the
regex extractor, the per-message LLM extractor and the CLI backfill all
run before writing reads the user's newest 500 rows through a query with
no is_active clause, so a sentence the person deleted
matches its own tombstone and the write is refused — by normalised text,
by containment, or by a token-overlap threshold of 0.75 whose measured
scores are in the config comments. The settings tab, where the person
types a sentence by hand, does no check at all, which is the right
boundary: the tombstone binds the machines and not the author. The test
that pins it mocks the old active-only query to an empty list on
purpose, with a comment that the previous code path must stay runnable
so the case cannot pass vacuously — the same worry this page raises
about Nova AI, written down as a test fixture. The reach is 500 rows and
the match is the dedup's, so a paraphrase below the threshold is a new
sentence.
Argos keeps two of them
and checks both on both write paths. A hard delete writes
deletion_tombstones, keyed on a case- and
whitespace-insensitive hash of the content with the category and user
scope; a review decision of rejected, and every conflict
resolution that keeps the old value, writes
rejection_ledger, keyed on the claim slot
(subject, predicate, user_scope) so a paraphrase of a
rejected claim is caught where a content hash would miss it.
remember, the direct memory_save path, and
save_candidate, the proposal path, each call
tombstone_check and rejection_check before
writing and return nothing on a hit, so a re-fed fact neither lands as
memory nor reaches the reviewer; a purge is explicit and scoped. The
schema comment records where the mechanism came from — the resurrection
its own observational test found when it ran this atlas's deletion
sequence — and the suite that pins it asserts the re-feed is blocked,
the block is case-insensitive, other content and other categories pass,
a purge lets the value back, and one user's tombstone does not bind
another. The one softness is INSERT OR REPLACE: a second
deletion of the same value overwrites the first row's reason and
time.
Sorted by what actually stops the value
Counting holders of the mark conflates four different mechanisms. Sorted by the one question that separates them — does anything read the rejection before the write completes? — and re-derived report by report in this note:
| Kind | Systems | What happens on re-assertion |
|---|---|---|
| Consulted — the form this page argues for | memsem, Perseus Vault, Universal Memory Engine, RainBox, Verel, Noosphere, breadcrumbs, Memory Compiler, Agent Memory Doctrine, Hippo Memory, Memmy, plur1bus, Sonder Runtime, Open Second Brain, Nova AI, remem-mcp, aimee, fireweed-mcp, NexusMem, RCK, Veracium, OpenMake LLM, Argos, no_human, SAGE | The write is refused. No row, or no activation |
| Collided — the key stays occupied | Mnemosyne, Wenlan, memoir-cli | The write lands on the rejected row, which stays rejected. Accidental in Mnemosyne, held in place by a missing filter and pinned by no test; deliberate in Wenlan, where the unique key is the value and the no-op is a named outcome the caller handles |
| Suppressed — the read path hides it | Provem, OmniMem | A copy enters the store and is stopped on the way out — in OmniMem by a suppression set matched as a substring of content, written by hand or by an effort-4 abandonment |
| Hybrid | Daimon | All three at once: collided by content-addressed id, suppressed on every read, consulted by one emitter |
Twenty-four of the thirty, then, implement the strong
form — value-keyed, normalized, consulted before the write,
refusing activation. The collided form is the rarer one, and Nova AI
shows why the distinction is worth drawing: its refusal held by a
missing filter, which its author then replaced with an explicit check, a
blocked return value and six tests. A property nothing
claims is one a tidy-up can delete. The mark is still broader than this
page's argument, and a reader deciding what to build should use the
table rather than the count.
Memory Compiler
is the cheapest complete instance, and the one that shows where the cost
actually lands. Its store is four Markdown files; the tombstone
is a table with a Rejected value column; the consultation
is tombstone_collision_check(), thirty lines that scan the
other canonical files for that value verbatim and emit a blocking
finding. The session cannot be sealed while a hit stands. No database,
no normalization pipeline, no embedding — the Cost to adopt
section above is answered here with a substring search and a chokepoint,
which is worth knowing before anyone concludes the pattern needs
infrastructure.
What it also demonstrates is that the hard half is the comparison,
exactly as this page argues. The scan skips any rejected value shorter
than twelve characters, commented "ignore short/generic strings, too
noisy" — and both tombstones in the project's own worked example
are ten characters: a superseded go-live date and a superseded hex
colour. The automatic check cannot see either. Two hand-written
must_not_return cases in its test file cover them instead,
so the example is correct and the mechanism that makes it correct is the
one an adopter has to remember to write. A length threshold is the
simplest possible answer to "when is a match meaningful", and it fails
on precisely the values — dates, prices, versions, identifiers — that
get corrected most often. Scope the comparison to a field or a whole
cell instead of to a length.
MemoryOps AI is the
closest any system here comes without arriving, and it is the
best argument on this page that the expensive half of the pattern is not
the hard half. Its records carry normalized_content,
computed on write, persisted on every row, and already compared — the
normalization this page's Cost to adopt section calls "where
the real work is" is simply done. Deletion is a soft delete setting
deleted_at, followed by a compaction pass.
Then the lookup that would catch a returning value reads:
MemoryRecordORM.status == _ACTIVE,
MemoryRecordORM.normalized_content == _norm(content),
A value that was deleted and is later re-asserted matches nothing,
and lands as a new active memory with an audit entry that looks like a
legitimate creation — because on its own terms it is one. The
gap is one predicate wide: the same comparison run without the
active filter, or a rejection table keyed on
(tenant_id, user_id, normalized_content) consulted before
activation.
Its own benchmark measures the half of deletion that works,
which is the distinction this page exists to draw.
benchmark/COMPARISON.md runs two deletion-leakage probes —
a deleted memory must not resurface on an exact probe, nor on a
paraphrased one — and MemoryOps passes both, as do a plain vector
baseline, Mem0, and its own governance-disabled ablation twin. Every one
of those cases is a read after a delete. None of them writes
the deleted value again, which is the event this pattern is about, and a
reader taking the four green scores as "deletion is settled" would be
reading a result that was never measured. The document is careful about
this in general terms — it says the probes leave "deletion lineage"
unexercised — and the specific case that would exercise it is: delete,
re-assert the same value through the ordinary write path, and assert
that what comes back is a refusal rather than a new active record.
AIMAOS is the other
near-miss, and it stores the value it rejected. When its
duplicate detector decides a new statement contradicts an existing
belief rather than restating it, the row adopts the newer wording and
keeps the old one in previous_content — the rejected value,
retained, keyed to the belief it was replaced in. Nothing reads that
field. Re-assert the old value and the same contradiction logic fires in
reverse: it supersedes back, the evidence trail resets again, and the
store oscillates between two values with no record that either was ever
judged wrong. The field this pattern asks for exists; the lookup
does not — which is the same one-predicate gap as MemoryOps AI above, reached from
the opposite direction, by a system that kept the value rather than one
that normalised it.
Worth holding beside the rest of that system, because it makes the page's central claim concrete. This is a project that enforces tenancy through Postgres row-level security, chains its audit per tenant so it cannot fork, holds sensitive writes for human approval, and commits eval cases that plant a cross-tenant memory before asserting it is unreachable. It is more careful than most of the corpus about what may enter. Admission and rejection are different problems, and building the first extremely well does not build the second.
- Gini has a
rejectedstatus on a unit, which is closer than most, but nothing keyed on the value: an equivalent claim can be retained again under a new id. - OpenSRE is where the
re-extraction this page warns about runs on a timer.
Its automatic extractor reads the last thirty turns after every recorded
turn;
forgetunlinks the file and records nothing. So a user who asks the agent to forget a fact they stated earlier in the same session has removed the row while leaving the sentence that produced it inside the window the next pass reads — and the grounding gate that admitted it will admit it again, for the same reason. The prompt's only defence is an instruction not to extract what an existing memory already covers, which is exactly false for a memory that was just deleted. Everything else in that write path is gated carefully, which is what makes the omission legible. - SESA is the case with the strongest evidence for a tombstone and none of the machinery. A skill card is deleted only after it has been retrieved at least three times and its measured net usefulness has gone negative — a rejection backed by more observation than any judgement on this page — and then the row is dropped, the duplicate check compares only against the live bank, and the next similar failure regenerates the card at score zero. The system pays three rollouts to learn the value is harmful and forgets that it learned it.
- Atomic Agent deprecates lessons and retains the row — good for history, silent on re-distillation from the same cluster.
- Mercury has a
dismissedboolean on the record. - Magic Context, MetaClaw, Redis Agent Memory Server, nanobot, CowAgent, Holographic, OpenClaw, Hermes Agent, and LlamaIndex have supersession, archival, or exact deletion and no value-level negative memory at all.
The absence matters most where it co-occurs with automatic
re-derivation, which is now the common case. CowAgent
re-distils MEMORY.md nightly from retained daily files.
Atomic Agent re-clusters. Magic Context and Redis Agent Memory Server
both extract on a schedule from retained history. OpenClaw's
auto-capture can restore content a user deleted. In each, "forget that"
is a statement about the present that the next background pass is free
to undo.
llm-wiki-memory states the limit plainly: operational supersession can archive an old leaf but cannot prevent the same rejected content from being distilled again.
Memanto shows that a
resolution is not a tombstone either. Its conflict workflow
ends in a human choosing remove_both, which is a
deliberate, reasoned, human judgement that two memories are wrong — and
it deletes them. The next night's extraction pass runs over the same
sessions with nothing to consult, so the most carefully made correction
in the atlas can be undone by a scheduled job. The lesson generalizes:
the quality of the decision does not matter if the decision
leaves no trace the write path can check.
Memora comes closest to the shape
without arriving at it. Its supersession pass classifies memory pairs
into a defined vocabulary — including contradicts as an
edge between two named memories — and hides rather than deletes the
superseded row, so the decision is reversible. But the edge is between
two ids, not keyed on the rejected value, and Memora
ingests documents and images: re-ingesting the same source produces a
new row that nothing blocks. Rich relation modelling is not a substitute
for negative memory.
Memora Engine is
both near-misses at once. Its deduplication is MemoryOps AI's
lookup — lowercased, whitespace-collapsed text compared against memories
with status: 'active' — and its conflict pass is AIMAOS's
reversal: a model classifies a new memory as superseding an older one,
and a guard allows that only when the new memory is the newer. A
superseded value repeated in a later conversation is not active, so it
is stored as new, and being newer it is eligible to supersede the
correction. What it keeps is better than either: a
SUPERSEDES edge with the model's stated reason and the
loser's row intact, which is the record this page asks for, keyed on
memory ids rather than on the value. And its committed evaluation is the
MemoryOps benchmark's shape — a superseded memory must not reach the top
five, asserted over full result sets — which proves the read half and
never re-asserts the value.
Memory Garden consults its rejection and keys it on the wrong thing. A person who denies a proposed change in their own views leaves a reviewed discovery that every later scan checks before writing the same pair again — the consulted form this page argues for. The key is the pair of source-atom ids, and an atom's id hashes its note's whole-content revision, the importer version and its position. Editing any line of either note reissues every id in the file, and the denied pair is eligible again. A living notes vault is edited constantly, so the rejection decays exactly where it is needed. Keying the check on the normalised text of the two statements would make it the value.
One sighting outside the corpus, because of where it was
found. os-factory/har is a harness for running
coding agents in isolated worktrees — no memory in it, no report, recorded as an exclusion —
and its Mission Control dashboard carries an
UnregisteredRepository table whose schema comment reads
"Paths removed via unregister — blocks auto-sync re-registration
until force register." The delete path writes the path into it
before dropping the row; the register path consults it on every write
and refuses with a 409 unless the caller passes
force: true, which deletes the tombstone as the same act
that overrides it. That is the consulted form, with the audited-override
shape this page's tradeoff list asks for. It goes one step further than
any holder above: the client handles the 409 by dropping the path from
its own local registry, so the sync loop stops re-asserting
instead of failing at the gate forever — a tombstone that quiets the
writer rather than only refusing it.
Two things to take from that and one not to. The failure it closes is
the one named at the top of this page — a periodic pass re-reading an
unchanged source and restating what a person deleted — which is why a
deleted_at on the row would not have worked and somebody
noticed. It is keyed on a filesystem path, so it never meets the
normalization problem that defeated Verel's round 9 and Memori's key;
this is the pattern on easy mode, and the easy mode is where it gets
built. And nothing tests it, in a tree with 87 test files, beside a
commit gate tested across nine cases — so even here, the negative half
is the half nobody covered. The mechanism is not hard. It is reached
when a concrete re-assertion loop makes the need undeniable, and memory
systems have exactly that loop and mostly have not noticed.
Noosphere is the strongest implementation of the form this page argues for, and it answers a question the others never had to. Its key is an HMAC of the capture, not a plain digest — so rotating the secret would make every stored key uncomputable from new input and silently readmit every value the system had ever refused. The check therefore computes the candidate's digest under every retained key version and matches the tombstone against the whole set, with the reasoning in the comment: "A tombstone from any retained key version blocks recreation. Historical keys remain in the bounded keyring until their tombstones and source TTLs have expired." The check runs inside a serializable transaction after the lineage rows are locked, and the write is refused with a 409.
If your value key is derived under a secret, this is a fifth property the strong form needs, and it is invisible when it fails: the tombstone table stays full, the check keeps running, and nothing ever matches again.
It also introduces the first deliberate expiry in this atlas's tombstones. Retaining the historical keys is what makes the check work, so the keyring is bounded by the tombstone's ninety-one-one-day TTL. That is a defensible trade for a privacy revocation whose source data expires anyway, and it means the guarantee is not again for ninety-one-one days rather than never again — a distinction worth making explicitly wherever this shape is copied.
The tombstone that must not retain what it protects
Every implementation above keys the tombstone on the value, and stores the value to do it. For a rejected fact that is fine. For an erased person it is a contradiction: a record whose purpose is to prove someone was removed, holding their name.
Fireweed MCP keys on a
digest instead. name_fingerprint is a SHA-256 over the
whitespace-normalized lowercased name, written into the durable
ERASE ledger event as subject_name_hash, and
the write gate hashes the names in an incoming claim to compare. The
refusal still works — an identical claim naming the erased subject is
turned away with previously_erased — and the store holds no
copy of the name to do it. The cost is the ordinary cost of a digest
key: it matches exactly or not at all, so a spelling variant walks
straight past, which is the same limitation every value-keyed tombstone
on this page has and is merely more visible here.
The second half is what makes it a tombstone rather than a
ban, and it is the part to copy.
acknowledge_erasure=true admits the claim anyway. The
stated reason is specific to the subject matter and worth quoting,
because it is the argument against the strongest form of this pattern in
the one domain where the strongest form is wrong:
"erasure is not a permanent ban on a person ever being mentioned again — someone may lawfully re-consent, or the same name may be a different person. The requirement is that re-admission be a DECISION SOMEONE MAKES, recorded as such, rather than something that happens quietly because nothing was looking."
Two committed tests hold both sides: one asserts the identical claim is refused after an erasure, the other asserts it is admitted with the acknowledgement. A suite carrying only the first would pass on a store that had stopped accepting writes at all — the same trap this atlas keeps finding in exclusion tests that never assert anything is still returned.
The refusal that reaches the inference
RCK is the instance that closes the
loop this page has been leaving open. Its denial is an ordinary row —
deny(kb, s, r, o) writes (X, NOT_R, Y) into
the same substrate as any assertion, so it survives, merges and replays
by machinery that already exists, with no second schema and nothing to
keep in sync. The module draws the distinction the design turns on
before it writes a line of code: a negative fact is "positive
certainty about non-membership", which is "structurally
different from 'we don't know'" and is handled by a separate
epistemic state.
What is new is where the lookup runs. Every
tombstone above guards a write path or filters a read. RCK does both and
then checks the same denials on the two paths that manufacture
facts: chain_induction.py calls
denied_pairs_for before an induced fact is accepted, and
rule_instantiation.py calls it at a score floor before a
rule fires. So the refusal is not defeated by the system's own
derivation — the answer you rejected cannot be re-derived from the facts
you kept.
That is the gap most of this page's instances have and none of them names. A store with any inference step — a consolidation pass, a rule engine, a summariser — can regenerate a rejected value from material that was never itself rejected, and a tombstone consulted only at ingest will not see it. Perseus Vault reaches the same problem from the other direction and solves it by lineage, suppressing a derived record whose source was rejected; RCK solves it by predicate, refusing the derivation before it produces the record. Lineage catches what was already built; the predicate check stops it being built. A store that derives wants one of the two.
The committed test is the shape this page asks for —
deny, then assert the denied object is absent while a
legitimate one is present, with a separate passthrough case establishing
the filter returns candidates when nothing is denied, so neither half
can pass vacuously.
The one that answered the question twice, differently, in two stores
breadcrumbs is the only project in the corpus that reaches the value-keyed question, answers no for one store on grounds this page has to take seriously, and yes for another. Both answers are in the same repository and neither is a compromise.
Take the no first, because it is the argument. The
conclusions ledger's correction model is ordinary supersession: a newer
JSONL line names the older one through obsoleted_by, keyed
on the record. What is not ordinary is that it ships a committed test
asserting a superseded entry must not win retrieval — and a second one
pinning why that test keys on the supersession marker rather than on the
value. From the fixture comment:
"The revert case: a value flips A -> B -> A. Both earlier entries carry obsoleted_by; the final entry RESTATES the original value as a new current entry."
A value-keyed tombstone would suppress that final entry, which is legitimately current. The reasoning is correct for the store it is defending: entries are hand-authored, so a value only reappears because a person decided it was true again, and that decision should win.
The reasoning stops holding the moment anything re-derives
entries, and the same repository documents that case. Its
provenance doc describes a backfill pass mining facts out of git
history, and its schema doc records what happened when one ran: the
backfill "swamped the session-verified entries and wrecked lookup
precision." A re-mining pass that re-derives a fact somebody
already retired writes a fresh line with a new date and walks past every
obsoleted_by in the file — which is exactly the laundering
path Verel's red team found, minus the adversary.
Its engine tier gives the yes, and it is a clean
instance.
reject_fact(category, key, value, reason) writes
.memory/tombstones.json keyed by category/key
and then by the rejected value itself, deletes the fact entry if that
value is the one held, and logs a REJECTED episode.
store_fact() reads the tombstone file before it reads
facts.json and raises when the incoming value matches —
"a rejected value may not be silently re-asserted" — and
lift_tombstone() is the deliberate way out, requiring its
own reason and logging its own event. An empty reason is refused on
either call, on the stated parallel that "an unexplained rejection
is as unauditable as an unexplained verification". Five committed
cases pin the behaviour, including that a different value under the same
key still stores.
Note what the two answers do not do: reconcile. The engine
and the ledger are separate stores with separate write paths, and
nothing in the ledger tooling consults tombstones.json. So
the backfill hazard the project documents — PROVENANCE.md's
mining pass, CONCLUSIONS_TEMPLATE.md's record of one that
"swamped the session-verified entries" — sits on the side that
answered no.
The general lesson survives the split intact, and it is sharper for having both halves in one repository. The value-keyed form is unnecessary while every write is a human decision, and that condition is a property of the write path rather than of the store. Any system here that adds model-driven extraction to a hand-curated ledger crosses that line without the schema changing, and nothing signals the crossing. That is a better argument for the pattern than a prevalence count, and it came from the project that stated the objection to it best.
And then there is the case that argues this page's thesis
better than any system carrying the mark, and does not carry
it. memoir publishes a
format spec whose merge section derives the mechanism from first
principles, for the reason this page states in its intent: under
union-by-identity across replicas, removal cannot be an
absence, because any replica still holding the item re-unions it on
the next merge. So a removal must be a record — and the spec gets the
hard half right, which most implementations do not. The record
must be monotonic and date-independent. If either side of a
merge carries hidden: true, the merged copy carries it
regardless of which copy has the newer date, because
tombstoning legitimately does not touch the item's date and the
tombstoned copy therefore usually loses the newest-wins
comparison. "Suppression must be monotonic or it is not
suppression." The implementation matches: unionByText
resolves by date, then makes a second pass re-applying the tombstone
from the losing copy onto the winner, and partitions tombstones out of
the visible cap so a suppression cannot evict a live memory while
enforcing itself.
It then splits the mechanism in two, which nothing else here does and
which the tradeoff list above implies. A suppressed decision gets an
absolute tombstone because the text is junk
permanently; a completed action gets a temporal one,
suppressed only against copies whose added predates the
done_at, because "'Fix the flaky test' can legitimately
be finished and later added again." "Implementations MUST
NOT substitute one class for the other." That distinction is
the answer to the objection that a value-keyed tombstone is too blunt
for anything that can recur.
The mark is withheld because no shipped surface can create
the absolute one. The only assignment of
hidden = true outside the merge function is a dated one-off
script whose own header says it is not wired into any command, whose
match strings are placeholders, and which the package manifest excludes
from publication. Fourteen MCP tools write memory and none retracts it.
Everything downstream of the writer is built — three read paths filter
it, the validator enforces its timestamp by spec section number, a test
asserts its exclusion at three surfaces including a live protocol call —
which makes this the most complete instance of the pattern in the atlas
and an unusable one. Read it as the strongest available
evidence that the idea is reachable by reasoning rather than by a red
team, and as a reminder that a mechanism is only as real as its
narrowest surface.
Open Second Brain is
the newest independent arrival and the first with a scope
dimension, which is this page's standing objection answered in
code. Its nightly consolidation pass promotes repeated corrections into
preferences; o2b brain reject --reason <text> retires
one and writes user_rejected_reason into the retired file —
set only for a user rejection and left undefined for the
automatic retirements beside it. On the next pass that retired rule
becomes a suppressor: signals on its topic are
swallowed before candidate planning, with the reasoning in the source —
"the user explicitly rejected the rule — re-growing it from fresh
signals is exactly what they were asking us not to do."
Three details are worth taking. The block is scoped rather than blanket: an unscoped suppressor swallows every signal on the topic, a scoped one only signals sharing its scope, and a signal carrying no scope matches an unscoped suppressor but never a scoped one — so a rejection in one project does not silence the same topic everywhere, which is the tradeoff this page lists third. Non-matching signals fall through and stay eligible for candidate planning, so the tombstone narrows rather than closes the topic. And every suppression emits an event naming the retired rule and the reason, so a refusal is visible to the user whose rejection caused it — the answer to a tombstone that silently swallows input and is indistinguishable from a bug.
What it does not have is a lift. user_rejected_reason
arms the suppressor permanently and nothing found removes it short of
hand-editing the retired file, which is the expiry problem this page's
tradeoff list names and which most implementations here also leave
open.
Velantrim
Crystal shows where the boundary of the pattern runs, because it has
two records that look alike. Its erasure writes a content-free receipt
with a hash of the erased claim, and no write path reads that hash: a
deletion receipt, which proves removal and cannot say what may not
return. Its immune memory (core/immune.py) is the
tombstone: a table of rejected claim patterns keyed on the normalized
text, recorded by a curator or by the strict ingest path, and consulted
before the truth gate by ingest, import and the review diagnosis, so a
claim containing a recorded pattern is refused with the reason
Immune: and only a force approval with a named actor and a
reason gets past. The two tables have no edge between them, which is the
first tradeoff on this list in its plainest form: erasing a claim for
being wrong does not record it, and recording it does not erase it.
Scope Recall
declines the pattern in writing. The store's dedupe comment says an
archived row must not suppress a distinct new candidate, the digest's
match reads only visible rows, and a review's reject is the same
archived that forgetting produces. Its privacy purge writes
tombstones keyed on scope and row id and stores a
content_hash no write path reads. Re-assertion is handed to
the candidate review instead: a refused fact said again returns as a new
candidate, and stays out of recall only as long as the digest default
stays candidate and nobody promotes it.
Uteke asserts the opposite of a
tombstone in a committed test. Its dedup gate checks each index hit
against the database and skips rows that are deprecated or gone, which
is right for the stale-index bug it fixed;
test_dedup_skips_deprecated_stale_index_entry then asserts
that writing a soft-deleted memory's text again creates a new live
memory. The rejection is recorded, as a superseded_by edge
and a reason, but keyed on the row, and thirty days later the server
prunes the row, the edge and its events.
Utopia writes the triple
rather than the row: rejecting a queued fact inserts
(kb_id, subject_id, predicate_id, object_id) into
rejected_facts, and pending::propose runs that
lookup — after checking whether the claim is already asserted or already
queued — before creating any new proposal. Because entity resolution
maps the same name to the same row, a re-extraction of the same sentence
hits the key rather than re-asking. It is also the clearest illustration
on this page that a tombstone's value is bounded by where it is
consulted: only documents belonging to the implicit memory source reach
propose at all, so an uploaded PDF restating a rejected
triple writes it straight to the ledger, and the table has no
object_value column, so a rejected salary blocks nothing.
Both limits are argued in the source — keying attribute facts on
(subject, predicate) would turn this salary is
wrong into never mention salary again.
Signet AI ships a table named
for tombstones and a route named /tombstone, and neither is
this pattern. The table is keyed on agent and session with removed paths
— a record of what the rolling memory-file window dropped, so a privacy
removal survives a re-index — and the route forces a soft delete.
Nothing is keyed on the rejected value: the uniqueness index and the
dedup lookup both filter out deleted rows, and the retention worker
hard-deletes the row after thirty days, taking the content hash with it.
The consequence is sharper here than in most systems that lack the
pattern, because the same store runs a background pass that re-derives
beliefs from evidence: a value the user forgot is accepted as a new
memory the next time it is said, and the next dreaming pass may cite it
and re-assert the claim it was removed from.
GrayMatter builds the durable half and skips the value-keyed half. Its supersession marker excludes a fact from recall immediately and unconditionally, before any signal is scored, and the update path latches it — when the stored fact is retired and an incoming snapshot is not, the write is dropped, so a consolidation cycle holding a pre-retirement snapshot cannot resurrect a corrected value. That is more retirement durability than most systems here manage. What is absent is anything keyed on the value: the write path performs no exact-text lookup, no similarity query and no history check, so a forget followed by the same sentence stores a second live fact at full weight. The pattern's distinction is visible in one repository — a record-keyed retirement engineered carefully, and the re-assertion it cannot see.
marm-memory arrives at the pattern from a place most of the corpus does not: not a claim extractor that might re-assert a corrected fact, but a derived graph its own product tells you to throw away and rebuild. Removing a concept writes its name into a suppressions table before it deletes the entity row, so the record is keyed on the name in its scope; name resolution follows the alias chain and then returns nothing for a suppressed name, and the build engine skipping that result is the only place the reading is used. What makes it a clear worked example is the table list in the reset: a full rebuild drops entities, relationships, code links, build runs and schema metadata, and leaves aliases, suppressions and dismissals standing. The committed test does the thing the pattern is for — remove, reset the whole graph, assert the name still resolves to nothing. The limit is worth stating beside it: the mechanism covers the derived layer only. A memory row is hard-deleted with nothing left behind, so the same text logged again is stored as new, and the entity gets suppressed on its way back out while the sentence that produced it does not.
Lobu calls its mechanism a tombstone and it is the record-keyed kind. A delete appends an empty event whose supersedes pointer names the target, a partial unique index permits one superseder per target, a trigger blocks a top-level delete on the events table, and the current-records view removes the target from every recall arm — a clean, durable retraction of a row. Nothing is keyed on the text, so the same content saved again becomes a new live head. The nearest thing to the value-keyed form is one layer up: a field-controls column marks a field human-owned and the merge returns an automation's write to an owned field as blocked rather than applying it, which is a durable refusal that survives re-proposal indefinitely — but it refuses the field, not the value, so it prevents silent application and not re-assertion.
create-context-graph's session connector gets most of the way and stops at the key. A corrections pass reads a user turn matching one of eight patterns, writes the preceding assistant turn as an alternative carrying a not-chosen flag and a reason, and links it as rejected from a decision — a durable, reasoned record of a value a person rejected, which is more than most of the corpus has. Its name is derived from the session id and the message's index in the transcript, so importing a second session in which the model proposes the same thing creates a second node and consults neither. The distinction this page draws — keyed on the record versus keyed on the value — is the whole difference here, and it is one hash function wide.
Tests to require
- Run the laundering sequence: reject a value, supersede the claim with a different value, then restate the original and corroborate it. Verel's round-7 finding is that this walks a rejected value back to verified in three steps, and it is the concrete attack this pattern exists to stop.
- Age the store past every TTL and prune you have, then run the laundering sequence again. Verel's round 8 was exactly this, at ninety-one-one idle days.
- Attack the key normalization with unicode look-alikes and case and
whitespace variants. Verel's round 9 was an NFKC bypass of
strip().lower(). - Reject a value, rerun extraction, and prove it stays inactive. Every
system in the atlas that carries this mechanism should have this test;
Daimon, which has 4,388 others, does not, and neither does Mnemosyne,
with 51,407 lines of them. Both hold the property by accident. Nova AI
is the counter-case and the cheapest one to copy:
tests/test_tombstone.pyis 246 lines, isolates a store in a temporary directory, and asserts that a re-asserted rejected definition changes no status and creates no second sense. A tombstone is the one mechanism whose silent failure looks exactly like success. - Run the lookup while the candidate is already in the
table. A tombstone consulted after the row is inserted and
before it is admitted can match the candidate itself. SAGE's v10.1 did:
the dedup predicate was
status != 'deprecated', the voter ran with the candidate sitting there asproposed, and on a single-validator chain every memory was rejected as a duplicate of itself and deprecated on arrival. The first repair narrowed the lookup to committed rows and deleted the tombstone with the bug; the second, on 12 September 2026, excludes the candidate's own id and ignores other in-flight candidates, so two identical proposals cannot veto each other. Assert all three: own row never matches, another proposed row never blocks, a deprecated row always does. - Correct A to B, then try to reintroduce A through a different source.
- Verify scope isolation between users, projects, and agents.
- Verify trusted override and tombstone reactivation are audited.
- Exercise normalization variants without conflating materially different values.
- Propagate privacy deletion to tombstones when policy requires true erasure.
Run these as a matrix rather than a checklist — see the contradiction test for the case shapes and the four outcomes worth scoring separately.