Reported: a durable record keyed on the rejected value rather than on the row, and consulted on the write path, recurs across the corpus. 68 systems of 757 carry the mark, and the table below sorts the holders this page discusses by what actually stops the value. The idea's history runs from invention to independent arrival: Verel built it under adversarial pressure, RainBox adopted it from Verel, and the systems after them reached it by other routes — content-addressed ids, a regulation, ordinary plumbing in a write gate, a report that named its absence, a lookup that forgot to exclude the rejected row, a hardening against key rotation.
Argued: the form that survives the red-team walk — a normalised value, scoped where the rejection is scoped, consulted on every automated write path including background ones. Holders stop short of it on normalisation, on scope or on reach, and the rest of this page is the argument for closing all three. There is a vocabulary for it: 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, and no library provides the mechanism. The provenance is traced under Seen in the atlas.
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 uses the
word for both kinds in one system. tombstoned is one of
seven record statuses, reachable by an authorized human command, and a
record in that state scores -Infinity and never reaches
recall — a soft delete keyed on the record. Beside it,
/forget writes a SHA-256 fingerprint of the NFKC-normalised
text, never the plaintext, to an append-only registry, and
findBlockingTombstoneForCapture checks it as step zero of
both capture callsites, so the forgotten sentence is refused when it is
captured again. Only the second is this pattern. The word alone does not
say which one a system has, 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
Read these first
- Memory Compiler — the cheapest complete instance, and the one that shows where the cost actually lands.
- Noosphere — the form this page argues for, keyed by an HMAC and checked under every retained key version, so rotating the secret cannot readmit a refused value.
- Argos — two value-keyed records, both checked on both write paths.
- MemoryOps AI — a near-miss, and the argument that the expensive half of the pattern is not the hard half.
Every instance
68 systems of 757 in the atlas carry the mark. The entries below are the ones whose mechanism this page discusses.
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 arrived at it independently and
refuses at the checkpoint write, discussed below. Provem arrives at it from regulation
rather than from a failure, discussed below. memsem refuses the write like Verel,
RainBox and Daimon rather than filtering the read like Provem, and arms
the mechanism only on a candidate review — discussed below. Perseus Vault refuses a value by
its digest without storing it — discussed below. PLUR1BUS does the same with a SHA-256
fingerprint of the normalised text, checked as step zero of every
capture. Universal
Memory Engine keeps a suppression table the write gate consults at
four points, discussed below. Noosphere keys its tombstone on an
HMAC subject hash and checks it across every retained key version, so
rotating the key cannot resurrect a revocation, at the price of a
ninety-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 keeps a JSON file of
rejected values that the fact setter raises on, 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. Memora, memoir and Scope Recall are discussed
below with the systems they are most easily confused with.
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.
Verel and RainBox are not independent inventions. 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 idea's first two appearances are one invention and one adoption, by the person who ran the survey, and the way it first spread was somebody reading another project's source. The arrivals that followed came by other routes, and they are what makes this a recurring mechanism rather than one project's idea: Daimon through a canonical content key, Provem through a regulation, Universal Memory Engine as ordinary plumbing in its write gate.
Daimon is an independent arrival, and it lands on the strong
form. Daimon's
daimon forget deletes the item from the live checkpoint and
appends a forgotten:<content-hash> event to an
append-only log, keyed on the canonical value and not on a row.
There is no evidence the author had read Verel or RainBox; the mechanism
came out of the project's own deletion-durability issue rather than from
a red-team finding.
It refuses at the write. store.write_checkpoint
runs every checkpoint through policy.admit_checkpoint,
whose drop_forgotten removes any item whose canonical text
hashes into the forgotten set before the item is stamped, signed,
indexed or mirrored. The docstring names the alternative it replaced:
"not merely a render-time withhold, which leaves the value sitting
on disk". A committed test asserts the re-extracted sentence is
absent from the checkpoint file itself, beside a never-forgotten twin
that must be present. The read side repeats the check — withheld from
the briefing, not carried, never written to the index on rebuild — and
the supersede-candidate emitter skips values already in the ledger. An
events ledger the write cannot read refuses the admission instead of
admitting through an empty set. And a reopen on the item cannot lift a
forget, because the forget also stands on a random key ref of its own;
only daimon unforget, at a terminal and with no agent
channel, writes the lift.
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 separates Daimon from Verel and RainBox is reach: its gate guards
the checkpoint write, and a reworded restatement produces a different
key.
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.
Provem arrived from a regulation, and its key is a token
subset. 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 Verel, RainBox and Daimon. 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 — scope is part of the rejection's identity, as it is in memsem's project key and Argos's user scope.
Its limitation is one Daimon does not share: 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.
memsem's tombstone is armed by a candidate review, and the
review verb is an MCP tool the agent holds too. 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 on review
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. Perseus Vault 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 any holder that stores the value: 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 a digest-keyed tombstone 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, RainBox and Daimon and unlike
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 an aggressive reading of the
normalization tradeoff, 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.
Mnemosyne has one
nobody appears to have built. It 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.
Nova AI began as Mnemosyne's mechanism in a different
language, and it is the better instance. Nova AI is a symbolic concept graph
with no model on any running path. weerleg — refute — sets
status = "rejected" on a sense, with the old status and a
timestamp in the concept's 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 a clean expression
of this pattern's intent.
And in Nova the accident became a design, which is good
evidence for this page's 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 typed "I had already
rejected this — do you really want to confirm it again?" whose
ja writes a sense_reactivated audit entry.
Seven cases in tests/test_tombstone.py hold each link, and
the one covering re-assertion says in its docstring that it replaced a
test pinning the previous behaviour. The check lives in
add_sense, and TeachEngine.teach calls
upgrade_unknown_sense before it: on a concept that also
holds an unknown placeholder, a refuted definition typed
again fills the placeholder as confirmed without the
question, and no test covers that order.
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.
Universal Memory Engine puts it in the write gate itself, as
plain plumbing. 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 a
small 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.
PMB shows why a closure reason has
to be read by every path that can promote. A user negation archives the
current keyed value and writes closed_reason, which nothing
in the tree reads. The plain fact that first stated the value stays
active, and pmb repair-keyed --apply promotes it back into
a keyed value and archives the negation as obsolete. The record exists;
it is keyed on the row rather than the value, and the repair pass walks
past it.
MeMesh carries a narrow instance
that still fits the definition, and its scope is exactly where its
rejections arise. The Stop hook restates three per-session snapshot
entities from the transcript on every turn, which is a re-extraction
from a retained source by construction; removing one observation from
such a snapshot writes the SHA-256 of its exact text into
metadata.forgotten_observation_hashes in the same
transaction, and captureEntity drops any restated line
whose hash is listed. Untrusted writes filter it as well, and only a
trusted explicit write clears it. There is no normalisation, the list
lives on one session's entity, and entity-level forget is
ordinary archival that a plain remember reverses.
Recollect consults its rule at
every write and suppresses on the read, and the read half reaches past
the claim store. A reject, correct or withdraw review writes an
assertion_rules row keyed on the normalised subject,
predicate and value; a later proposal of that value from any writer is
stored as blocked_by_rule and cleared from every
eligibility gate, and recall withholds any raw source chunk whose text
contains all three keys, so a copied note repeating the corrected value
stays out outside history mode. Its committed case asserts exactly that
after a positive lexical control, a REINDEX and a rebuilt
router. The two limits are the text key, which a paraphrase walks past,
and erasure, which blanks the rule's keys with the claim it came
from.
TideMind has the
value-keyed form on tag assignments and nothing on content. A
person rejecting a tag on the desktop flips the node-to-tag link to
rejected_by_user rather than deleting it, and the
background annotator drops any model-proposed tag whose case-folded name
matches a rejected one for that node; the existence check keeps
tag-promote from re-linking it, and decay and pending-link GC never
touch the row. The re-extraction the tombstone guards against is live,
because a recall-time rewrite resets the node to unannotated. Content
has no equivalent: a corrected value re-digested later dedup-merges into
the corrected node under a prompt that says the newer text wins, and a
link the evaluator rejects is soft-deleted and can be proposed
again.
FERNme puts its tombstone on the
suggestion layer, as Wenlan does, and derives the key from the content:
suggestion_id is the SHA-256 of site, user, kind and the
canonical-JSON payload, and upsert_suggestion leaves a
rejected row untouched, so the refresh generator and the agent's
propose_* tools collide with it. The key is only as stable
as the payload. An alias merge names its pair as
canonical_attr and alias_attr, ordered by
current weight, so a reinforcement that flips the order mints a new id
and the rejected merge returns as pending; the committed test holds
weights fixed. And remember writes the rejected tag
straight to the graph, which the tombstone never consults.
Particles keeps no separate
rejection table: the retired particle is the tombstone. A row retired
with EXPLICIT_RETRACTION,
EXPLICIT_SUPERSESSION or a review's
CONFLICT_RESOLVED stays in the store, and its identity —
normalised text, resolved subject set, stance holder — is looked up on
both the extraction and the agent-assert path before the conflict ladder
runs. A match is stored quarantined behind an INCONSISTENCY
record that only a review lifts, so the source's restatement is on
record and not believed. Two edges are deliberate: a review
DISCARD retracts with a reason outside the judgment set, so
a discarded value re-mints, and the match is exact, so a paraphrase or a
different subject resolution walks past.
vir keys its tombstone on meaning, and only on its mined rules. The connect pass proposes rules that recur across sessions, and a person rejecting one keeps the row and its rule embedding. Every later run compares each candidate's embedded rule with the rejected vectors for the active model and skips any at a cosine of 0.85 or above, so a rewording drawn from other sessions is not written; a model change re-embeds the rejected rules first. The comparison runs after the paid model call, because the candidate's wording exists only then, and a failed embedding skips it. A rejected session note has no equivalent: its rejection is keyed on the session, and the same lesson from another session returns as a new note.
Stella puts the tombstone on its
proposal loop and keys it on the wording. A rule or skill proposal mined
from the agent's reflections carries the lineage
prp_<kind>_<slug>-<hash8> over the
cluster's representative text, stella proposals ignore
appends a Rejected promotion event to a ledger whose update
and delete are refused by SQLite triggers, and both halves of the next
induction skip a lineage that folds to Rejected before the eligibility
gate and before any file is written. A later Keep on the same lineage
lifts it, and a test pins each direction. The key is exact wording, so a
lesson that re-clusters under a different representative mints a new
lineage; and what the codebase itself calls a tombstone, its
node-suppression marker, is keyed on the row and lifted by the next
upsert of the same node.
NEOTH keys its tombstone
on an erased topic, and reach is the open half.
forget_by_topic writes
_tombstone.<topic>, lowercased, into the profile
redaction registry with never_recreate, in the same
transaction that deletes the topic's rows, and profile extraction
rejects any claim naming the topic in its field or nested value — at its
Stage 5 guard and again inside the apply transaction. Peer gossip and
vault promotion check it too. The module's own comment says every ingest
that can lift text into recall must consult it; the ground-truth insert,
document claims and episodic ingest do not, so
neoth groundtruth add of a sentence naming the topic lands
verified.
Sorted by what actually stops the value
The mark covers four different mechanisms. The table sorts the holders this page discusses — a selection, not every system carrying the mark — by the one question that separates them: does anything read the rejection before the write completes? The method is set out 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, OpenMake LLM, Argos, no_human, SAGE, Memora, Scope Recall, MeMesh, Recollect, TideMind, Utopia, Velantrim Crystal, marm-memory, vir | The write is refused. No row, or no activation |
| Collided — the key stays occupied | Mnemosyne, Wenlan, memoir-cli, ShellBrain | 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 or effort-5 abandonment |
| Hybrid | Daimon | Consulted at every checkpoint write, where
drop_forgotten removes the value before it reaches disk;
also collided by content-addressed id, suppressed on every read, and
consulted by one emitter |
The Consulted row, then, is the strong form —
value-keyed, 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 seven tests. A property nothing claims is one a tidy-up can
delete. The mark is 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 comes
close 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 MemoryOps AI, because its gap 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.
Most of the near-misses below stop at supersession, archival, or deletion — mechanisms that remove a value from view without recording that it was judged wrong:
- Gini declares a
rejectedstatus on a unit that no native path writes — only a migration importer reaches it — and nothing is 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.
- RRSI states the refusal as a property of the method — its README says the proposer is conditioned on the full edit history so a falsified hypothesis is not redrawn — and enforces it only as prompt text. The ledger keeps every rejected edit with its hypothesis and measured loss, and the proposer sees the 40 most recent records under an instruction not to redraw them unchanged. Nothing compares a new hypothesis or diff with a rejected one, the derived tried and prune sets are keyed on component rather than value, and the ledger does not survive into the next run.
- FARM is the robotics form of the
gap. A person can delete an object in its Viser UI, and the delete
clears the row's
activebit — the same bit a merge, a keyword prune and an unclear-caption drop clear. Neighbour search masks inactive rows out, so the next time the robot sees the object the detection finds no match and opens a new row, which the caption worker describes and the retriever returns. The only value-keyed exclusion in the tree is a configured prune keyword list no correction writes to; the rejection a perception memory needs is keyed on what the next observation carries — a place and a category — not on the row. - open-bridge has the lookup
a tombstone needs and none of the refusal.
learning-ledger.py prior-rejectionslists rejected proposals on exactly the sametarget.path, and five skills are told to cite what it returns or drop the candidate — but it is keyed on a file path rather than a value, its docstring says it "never blocks a write", and the one writer that proposes memories,/archivePhase 5, does not call it. Below it, an inline archive reject records nothing, andmemory-location.py migratecopies back any legacy fact missing from the target, so a deleted fact returns on the next run. - MegaMemory refuses the right
re-creation for the wrong reason. Concept ids are slugs of the name
under the parent,
remove_conceptkeeps the row withremoved_atand a reason, andcreateConcept's existence check skips removed rows, so re-creating the concept passes the check and then fails on the removed row's primary key with a raw SQLiteUNIQUEerror. The name is blocked for good, with no message saying why, while the same claim under any other name is admitted and searchable. The refusal is keyed on the identifier, not the value. - Memoose is the case
where the write path undoes the retirement. A relation's id is
a hash of its source, name and target, and
upsert_relationon an existing id setssuperseded=0, superseded_by=NULL, so the triple that was replaced becomes current again the moment anything re-asserts it. The re-asserter is not a person deciding a value is true again, the case breadcrumbs defends, but a background Haiku keeper reading transcripts and an onboarding skill reading an old README. Where both values of a functional relation carryvalid_from, the ordering re-supersedes the arrival; where neither does, the revived row wins on write time and retires its replacement. - DreamGraph has the
value key and consults it on the write path, and lets it
expire.
deduplicateAndAppendEdgeskeys every incoming dream edge on the sorted endpoint pair and the relation with its qualifier prefixes stripped, and drops a duplicate of arejectededge without reinforcing it — the right key, checked at the right place. The key lives on the rejected row itself, which decays at double rate and is gone in about four cycles. Its key then moves into an in-processreinforcementMemorythat hands the next re-derivation the old reinforcement count and up to +0.20 confidence, so the store remembers the evidence and forgets the verdict. - Claude Engram
(mlapeter) shows the gap from the other side: its compression is
careful never to destroy, and that makes deletion incomplete. Gist
promotion overwrites an old memory's text and archives the verbatim
original under a fresh id with a
gist_ofpointer, and merges archive their sources withmerged_into, while theforgettool filters only the active file. The forgotten memory's original wording stays indeep_archive.json, wheredeep_recallfinds it by substring andreactivate: truereturns it to the active store. Nothing keyed on the text stops the extractor re-deriving it either. - Atomic Agent deprecates lessons and retains the row — good for history, silent on re-distillation from later episodes.
- 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 forget
tombstones the session rather than the claim, so the same sentence from
another session is learned again. In each, "forget that" is a statement
about the present that the next background pass is free to undo.
llm-wiki-memory shows the limit: 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 a carefully made correction 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 shows the two halves side
by side. 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; that edge is between two ids, and
rich relation modelling is not by itself negative memory. Retirement is
the value-keyed half. It writes a tombstones row keyed on
content_tombstone_hash(content), which normalises case and
whitespace, with no foreign key, so the schema comment's "Tombstones
survive the deleted row" holds literally; both automatic ingest
paths consult it — absorb_memory skips a tombstoned digest
and records action: tombstoned with the retirement reason,
and import skips the entry. memory_create is exempt by
design, so the gate binds the automatic paths and not the operator, and
a paraphrase has a different digest and passes.
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. Its tests came a release after the mechanism: one file asserts
the server's 409, another that the client drops the path on it. 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 implements the form this page argues for, and it answers a question a plain digest never has 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.
Its tombstone also carries a deliberate expiry. Retaining the historical keys is what makes the check work, so the keyring is bounded by the tombstone's ninety-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 days rather than never again — a distinction worth making explicitly wherever this shape is copied.
ShellBrain has the
collided form on one record type and not on the one that matters
most. add_claim looks up a concept claim by
(repo_id, concept_id, claim_type, normalized_text) with no
status predicate and returns the existing row, backed by a total unique
constraint, so a claim its librarian marked wrong stays
wrong when re-added — and the new evidence is attached to
it as supports; no comment claims this and no test pins it.
Relations, groundings and memory links use partial unique indexes on
active instead, and memories have no value key at all.
Worse for memories, the librarian's duplicate search is the same read
that hides wrong rows, so the rejection is invisible at the
moment it would be consulted.
HiveMind
(projectmentor) gets the collision from content addressing and the
suppression from a projection. Facts are identified by their
exact text: persist_fact maps a new entry whose
content matches an existing row onto that row, as
corroboration (hivemind/parts/04_projection.py:55-67). An
owner-signed retract names one entry, and
_content_evidence resolves it to that entry's text and sets
forget on the text, so every row and every later assertion
of the same string projects to FORGET_FLOOR, -1.0
(:1158-1201). Every default read keeps rows at or above
zero. No table holds the rejection: the journal entry is the record and
the projection is the consultation. Two limits come with that design.
The key is the literal string, so a trailing period makes a new fact.
And the journal is replicated and append-only, so the forgotten text
stays on every device, one --min-confidence -1 away from
the agent.
The tombstone that must not retain what it protects
A tombstone that stores the value in order to key on it is fine for a rejected fact. 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 limitation every exact-match key 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 offers a read
filter the caller has to apply — no KB query invokes it — and checks the
same denials automatically 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. 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 reaches the value-keyed question twice in one repository, 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". Seven selftest
checks pin the behaviour, including that a different value under the
same key still stores and that a rejection naming an alternative cites
it in the refusal.
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 derives this page's thesis
from first principles. 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 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 absolute one has a writer for a person and one for the model.
memoir forget "substring" [--purge] resolves the decision,
states that hiding cannot be undone, and calls hideDecision
to set hidden and hidden_at, with
--purge redacting the text while keeping a sha256 identity;
the MCP tool memoir_forget reaches the same
hideDecision. Downstream, three read paths filter it, the
validator enforces its timestamp by spec section number, and a test
asserts its exclusion at three surfaces including a live protocol call.
The limit is the key: retraction is keyed on the decision text, so a
paraphrase of a hidden decision is a different identity and is not
suppressed. Read it as evidence that the idea is reachable by reasoning
rather than by a red team.
Open Second Brain is
an independent arrival 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:. A force approval with a named actor and a reason
gets it past only as CURATOR_OVERRIDE, a status strict
grounding refuses, and a curator's immune forget lifts the
pattern under an audited event. 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 keys
its suppression on the claim rather than the row.
_inherits_suppression runs inside put_source
for every incoming source event and asks whether the text contains a
suppressed claim's subject, predicate, value_text and every
one of its conditions in the same scope; the claim's head version must
be active or disputed. If it does, the event
is written already suppressed, and a claim derived from it is suppressed
with it. So a statement suppressed once and said again in an unrelated
later turn does not return. The match is literal containment, so a
paraphrase passes. The one committed test restates the claim in a source
that also cites the suppressed source, which a separate rule,
link_source, suppresses on its own, so no test isolates the
value-keyed rule.
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 ten 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.
elizaOS is a near-miss that came
close to having built it by accident. It already computes the key:
normalizeFactTextKey canonicalizes a claim to lowercase,
letters and numbers only, whitespace collapsed, unicode-aware — and
findEquivalentFact looks that key up against the recent
fact window, scoped by room and entity, before every fact insert. It
also already has a retirement state that every read path respects. The
two are one continue apart: the dedupe loop opens with
if (!isActiveMemoryEvidence(candidate)) continue;, so a
retired row is skipped as a match candidate and the identical normalized
text is written again as a new fact with a new id.
That leaves the system where this page's problem statement starts.
Deletion is real — runtime.deleteMemory, an agent action
and a DELETE route — and so is evidence-linked retirement,
and both are keyed on the record, so "forget that" survives
exactly until the next message asserts the claim again. The change that
would close it is unusually small for this corpus: write the canonical
key the dedupe already computes into a rejected-values table, and
consult it in the loop that already skips inactive rows. The one
argument for the current behaviour is that fresh evidence arguably
should revive a claim — which is the tradeoff this page's
Tradeoffs section names, and nothing in the repository states
it as a decision.
pi-jarvis keys its
tombstone on the slot a fact occupies, and its tests assert that
choice. A note's id is a SHA-256 of its scope, project and
normalized title, and a forget writes that id into
tombstones. Every automatic save with the id returns
forgotten, and only a person's remember
command lifts it (memory-store.ts:90-95,
:491-499). A rename tombstones the old title in the same
transaction. The committed test saves a changed body under a forgotten
title and asserts the refusal
(test/memory-store.test.ts:181-182), so the record refuses
the slot rather than the value, and the same fact under a new title is a
new identity. Key the tombstone on what a writer will re-assert,
and decide whether that is the title or the body.
Hy-Memory stores a
forget request as an ordinary fact, and only derived artifacts read
it. An extracted fact such as "the user asked the assistant to
forget that they like hiking" is detected by regex, its target terms are
extracted, and in ultra mode the balanced-catalog and
specific-supplement builders suppress entries matching them while System
2 retires matching schemas
(hy_memory/pipelines/balanced_catalog.py:76-237,
hy_memory/pipelines/system2_agent.py:1956-2030). All three
are behind flags that default to false, and the System 1
extractor and reconciler that would re-assert the value never consult a
marker. MemHub, in the same repository, blocks the evidence ids a
person's delete rested on and asserts by test that the same fact from a
new conversation is remembered again
(memhub/apps/core/tests/test_hy_state_source.py:281).
A value-keyed record consulted only downstream of the write that
re-asserts the value is a filter, not a tombstone.
Cindy keeps a value-keyed
rejection for skill drafts and none for memories. When a person
declines a candidate skill, the nightly routine stores the candidate's
title in declined and drops any later observation with that
exact title before it reaches the candidate list
(apps/desktop/src/main/maker-ipc/botNightlyLearning.ts:553-560,
:646). It is consulted, which is the form this page argues
for, and it is narrow: keyed on a model-written title, capped at 40
entries, and read by no other skill writer. Memory has nothing of the
kind. memory_delete, consolidation and the nightly delete
unlink the shard, and the next memory_write of the same
claim lands as a new file.
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 about ninety 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;
Mnemosyne, with 65,228 lines of tests, does not, and holds the property
by accident. Daimon's
test_deletion_durability_protocol.pyruns it end to end: forget a value, re-feed the original transcript through the real serializer, and pair every step with a never-forgotten twin. Nova AI is the cheapest 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 did before
v10.1: 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, v10.1, 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.