Intent
Every contradiction a memory system finds must end in a disposition: a named outcome, chosen by someone, recorded where the write path can see it. Detection without a disposition adds a status field and a growing backlog.
The problem
Contradiction detection is the easy half and the half everyone builds. Across the systems reviewed here, the same shape recurs: a detector runs, a memory gets a flag, and nothing in the codebase says what clears it.
The failure is not that the flag is wrong. It is that a flagged memory is still retrievable and now also ambiguous — the system knows two things disagree and still hands both to the model, which is strictly worse than not knowing, because the cost of detection was paid and none of the benefit collected.
Three specific traps:
- A binary resolver forces wrong answers. "I work at Acme" and "I work at Globex" conflict only if you assume one employer. Systems that can only supersede or reject cannot say both are true, so someone eventually resolves a non-conflict incorrectly, or leaves it forever.
- A blocking queue makes memory useless. Quarantining every flagged memory until a human clears it means the store degrades whenever nobody is looking.
- A resolution that deletes gets undone. If the pipeline that produced the contradiction runs again — nightly extraction, re-ingestion, re-clustering — and the resolution left no trace it can consult, the next pass recreates what was just decided against.
The pattern
detect ──▶ typed finding ──▶ queue (non-blocking) ──▶ disposition ──▶ record
contradiction pending items keep_old so the
update stay retrievable keep_new write path
duplicate keep_both can consult
conflict remove_both it later
manual(content)
Five requirements:
- Type the finding. "Contradiction", "update", "duplicate" and "semantic conflict" deserve different dispositions; collapsing them into one flag discards the information the detector just produced.
- Offer a non-binary disposition set, including both are valid and a human writes the replacement.
- Keep pending findings retrievable. The flag is a signal, not a gate. Only a disposition removes.
- Name the actor and the reason. A resolution with neither is indistinguishable from a bug.
- Record it where the write path looks. Otherwise the next extraction pass undoes it — see rejected-value tombstone.
Keep the detector's recommendation vocabulary narrower than the resolver's action vocabulary, so the model's framing does not bound the operator's options.
Diagram source
%% caption: a detected conflict becomes a typed finding in a queue that stays retrievable while pending, and every disposition writes something the next extraction consults
flowchart TD
D["detector"] --> F["typed finding:<br/>old id, new id,<br/>description"]
F --> Q["queue — pending<br/>stays retrievable"]
Q --> R{"disposition"}
R --> S["keep_old/new:<br/>supersede"]
R --> B["keep_both:<br/>both stay"]
R --> T["remove_both:<br/>tombstone"]
R --> H["manual:<br/>human writes"]
S --> W["write path consults<br/>on next extraction"]
T --> WWhy it works
It converts a standing ambiguity into a decision with an owner. Retrieval stops handing the model two conflicting memories and hoping; the store gains a record of why it believes what it believes; and the detector's precision becomes measurable, because kept-versus-dismissed is now data.
The keep_both option carries more weight than it looks.
Most detected "contradictions" are two true statements about different
times, scopes, or aspects, and a resolver that cannot express that will
either corrupt the store or stall.
Tradeoffs
- It needs a human, or a policy that stands in for one. A queue with no drain rate is a slower version of the original problem.
- The detector's false-negative rate stays invisible. Nothing that never reaches the queue can be counted, and no system here measures this.
- Dispositions can be wrong, and unless the finding and its resolution are both retained, a bad resolution is unreviewable.
- It is only as durable as its record. A carefully
reasoned
remove_boththat leaves no tombstone is undone by the next scheduled extraction.
Cost to adopt
Build: a typed finding record, a queue with a non-blocking read semantic, a disposition enum, and one surface — CLI or UI — where a person acts on it.
Forces elsewhere: retrieval must know what a pending finding means, and the write path must consult resolutions or they decay. Bounding the detection scan matters too: comparing everything against everything re-surfaces settled pairs forever.
Ongoing: somebody has to drain the queue, and the detector needs its precision watched or the queue fills with noise and stops being read.
Skip it if nothing automatically writes memory. A store only a human edits resolves conflicts at the keyboard.
Seen in the atlas
Memanto types the finding and
offers a non-binary disposition set. A scheduled local LLM pass writes a
dated JSON report typed as
contradiction | update | duplicate | conflict with old and
new ids, and a person resolves each entry through the CLI or web UI as
keep_old, keep_new, keep_both,
remove_both, manual with content they write
themselves — a model validator refuses manual without it —
or expire_old, expire_new,
expire_both, which retire the losing memory reversibly
where the others delete it. The detection prompt requires at least one
side to be new, keeping the pass linear and stopping the queue refilling
with pairs someone already dismissed. What it lacks is requirement 5:
remove_both deletes permanently and leaves no tombstone, so
the next night's extraction may restore it.
Core Memory has the governance half. Its approval workflow states the non-blocking rule explicitly — "pending beads stay retrievable… rejection is the only state that removes" — distinguishes rejected (not memory-worthy, excluded unconditionally, retained with rejecter and reason) from superseded (once true, surfaced on request), and makes a reason mandatory on every governance action. It is aimed at review rather than at contradiction specifically, and its approve and reject verbs are also MCP tools the writing agent holds, recording the approver as a string rather than verifying it.
Daimon solves requirement 5,
which Memanto misses, while placing the decision somewhere neither of
the others does: inside the artifact the user is already
reading. A detected supersession renders in the next briefing
as a flagged item with the confirm and reject commands printed beside
it, so the disposition is chosen at the moment the stale claim is
encountered rather than in a queue nobody opens. Confirming appends a
resolution that withholds the item from later briefings while search
ranks it down; the forget path appends a content-keyed
tombstone, which is the trace Memanto's remove_both
lacks.
breadcrumbs contributes the
requirement none of the three above states, and it is the one that goes
wrong after everything above is built correctly: the resolution
has to reach the read path, and something has to check that it
did. Its supersession is ordinary — a newer JSONL line names
the older one through obsoleted_by — and its schema doc
tells adopters their boot matcher should exclude a superseded
entry from current knowledge. What is unusual is
run_forbidden_check() in
templates/ledger-tools/retrieval_exam.py, which replays a
configurable model of that matcher against simulated session-start
conditions and names any superseded entry that still wins an injection
slot, naming the probe that surfaced it, with
--fail-on-forbidden turning a hit into a red exit. The
docstring is the argument: "Correction that stops at the ledger row
and never reaches the retrieval lane is not correction; the descent has
to complete." A store can fail exactly that way on its main
retrieval path with no test that would catch it.
The check is also careful about its own negative result: when every
superseded entry is unreachable it reports unexercised
rather than clean, because a lane that never had the chance to make the
mistake proves nothing. Other suites here defend against a vacuous pass
by designing the fixture so it cannot pass vacuously; this one puts the
distinction in the verdict vocabulary, where a later fixture edit cannot
quietly remove it.
Three rules make Daimon's briefing surface safe, and all three are
transferable. A machine suggestion is live by
construction — the liveness fold refuses to let a
supersede-candidate suppress anything, so a wrong guess
costs a line of noise and never a memory. Rejecting a guess
needs no evidence, re-opening a resolved item does, because
overruling a machine suggestion is not the same act as vouching for a
claim, and the code refuses the second without either a live code anchor
or an explicit --evidence string. And a human verdict
silences re-detection permanently, so the queue cannot
refill with something a person has already answered.
The absence is the more common finding, reached independently in
several reports. Gini declares
rejected and conflicted as states that no
native code path writes, so there is nothing to resolve. Magic Context's
flagged "marks a problem without an operator surface". OpenViking has merge operations but
"no operator-facing review queue for contradictions". Holographic surfaces
contradictions as an ordinary query and only reports them.
MateClaw stops one step later:
it has the queue and the actor without the effect. A
ContradictionDetector fills a queue of unresolved
contradictions, and POST /contradictions/{id}/resolve,
gated on a workspace member role, records
KEEP_A, KEEP_B, MERGE or
IGNORE with resolvedAt and a
resolvedBy read from the authenticated principal. No code
path reads the verdict to retire, merge or down-trust a fact, so recall
returns both sides before the verdict and after it — a disposition with
an owner and a record, and no consequence.
Nova AI answers detection
without a queue. find_contradictions checks a word's
is_a parents against three hardcoded incompatible category
groups and returns a reason per conflict, and
modules/knowledge/contradiction_checker.py calls it from a
periodic sweep on the background loop, raising each conflict with the
user with a concrete weerleg: proposal, so the refusal is
one typed line away. A contradiction_state.json keyed on
the word plus its sorted conflict list remembers what has already been
raised, so an unresolved conflict is mentioned once rather than every
cycle — the bounded scan this page's Cost to adopt asks for.
The disposition is the refutation itself: weerleg sets
status = "rejected" with the old status in the concept's
audit log (the reason parameter has no caller that fills
it), reasoning filters it out, and add_sense refuses to
re-add a refuted definition, which is requirement 5 met by the rejected-value tombstone rather
than by a separate resolution record.
Memora sits between the two
groups: it classifies pairs into a defined vocabulary including
contradicts as an edge between two named memories, and its
correction pass defaults to a dry run, though dry_run is a
parameter on an MCP tool the model calls — and the dispositions
available are supersede or nothing.
AIMAOS resolves rather than flags, and shows that resolution alone is not the whole of the pattern either. Its detector is deterministic and unusually cheap — a phrasing-skeleton match where the value tokens swap — and the disposition is fixed: the newer statement wins, the evidence trail restarts, and the replaced wording is kept on the row. Nobody chooses, nothing records that a conflict occurred beyond the overwritten field, and no write path consults the outcome. So the same value can be re-asserted and win the reverse decision immediately. A disposition that is always the same one and is never written where a later write can see it leaves the store in the state this page's first group ends in — ambiguity resolved, and nothing durable to show for it.
Ouroboros is the
version where the disposition ladder is complete, and it is worth
reading even though its subject is a specification rather than a
knowledge store. resolve_conflict on a same-key
contradiction returns one of five outcomes: identical normalized values
are SAME_VALUE and not a conflict at all; a blocked entry
is a human-decision surface in both directions; otherwise a
fixed ten-entry source-priority ladder decides, then
confidence, and CONFLICTING is returned only on an exact
tie of both. No model is consulted anywhere in that function. The
outcome is then written where the next write can see it — the loser's
status becomes WEAK and its rationale field
gains a sentence naming the reason, so the store carries both the
surviving value and why the other one lost. The residual decision is the
exact-tie case, which the ladder is designed to make rare, and it blocks
the interview driver rather than sitting in a queue — an interview
ouroboros_pm_interview answers from its parameters as
readily as a person at a terminal. Two properties this page argues for
and rarely finds together: the automatic disposition is
deterministic and free, so it does not degrade when nobody is
looking, and a blocker is retirable — an earlier
transient block is cleared by a later non-blocked same-key answer rather
than needing a human to remember it exists.
Claude
Self-Reflect has four of the five requirements and deliberately
declines the fifth. Its csr_resolve tool writes
resolved, still_open or regressed
into an append-only resolution_ledger with mandatory cited
evidence, latest row wins, and a later regressed row
re-opens a settled chunk — a named disposition, an actor, a reason and a
durable record. The schema comment even reasons about who may write:
task-derived candidates land in resolution_proposals,
invisible to search and annotation, because "automatic writes to
resolution_ledger would be indistinguishable from human
verdicts at read time". Two gaps follow. The source
column that would record that distinction has one production writer,
passing the literal "agent". And the disposition never
removes anything: resolved chunks are sorted to the tail of the page and
annotated, so the verdict is advice to the reading model rather than a
state that withholds a memory.
Resonant Mind resolves at
write time with one fixed disposition. Each mind_write
observation looks up same-entity neighbours, discards candidates between
0.80 and 0.85 cosine unreported, and retires anything at 0.85 or more as
keep-new by setting valid_until and
superseded_by, with no type, no actor, no reason, and a
reply that names neither row. The disposition then reaches two of seven
read paths: mind_search and graph_look hide
the retired row, while the dream pools, the wake ritual's orphan pick
and the HTTP search return it. It misses requirements 1, 2 and 4 by
construction, and shows a failure the list does not name: a disposition
stored as a per-query predicate holds only on the queries that remember
to apply it.
Kagura Memory Cloud
gives a detected supersession a disposition and keeps the queue from
refilling. At embedding time the nearest same-context memory at cosine
0.85 or more is stored in a server-only supersede_candidate
column and surfaced on every recall until the agent accepts it with a
supersedes edge or rejects it with
update_memory(dismiss_supersede_candidate=true). The
rejection records the similarity it was made at, and the detector
re-proposes the pair only when a recomputed score moves by 0.02, so a
mechanical reindex stays suppressed while a content edit earns a new
judgement. The actor on both sides is the agent, and the disposition set
is binary: nothing records a reason.
Octop Memory is the
counterexample for having a disposition in three places. Its rule-only
promotion worker parks a candidate whose negation polarity flips against
a live atom as conflict, and three surfaces resolve the
queue: the JSON-RPC approve supersedes the contradicted atom, the CLI
approve writes the new atom and leaves the old one live, and the source
dashboard's approve sets the status column and writes no atom at all. A
separate fallback pass promotes anything left in
needs_review for seven days, including values it re-queued
because they had been rejected twice. One resolver function called by
every surface would have closed all three gaps.
Dynamics-memory meets
requirements 1 and 3 and shows a disposition that does not reach the
read path. Near-duplicates at write and suppressed pairs at read enter a
tension backlog that an LLM judge resolves after 20 turns as
synonym, update, contradiction or
collision; until then the rival is injected beside the
selected memory as a conflict line. But an update only
moves the loser to an archive pool that retrieval still scores at a
penalty, every contradiction becomes an aggregate flagged
pending_review that no code path clears, no verdict records
an actor or a reason, and a re-extracted old value is compared only with
visible rows, so it returns as a new candidate. breadcrumbs's forbidden-item check
is the test that would catch the first of these.
Lint-AI detects conflicts
automatically and gives them nowhere to go. At every refresh a regex
claim chain marks a document Conflicted when a newer claim
on the same subject disagrees without a correction cue or a same-source
date, and whenever an inferred replacement hits a document holding more
than that one claim. The status is recomputed from content, so no actor
can resolve it; it clears only when a later document or an explicit
supersedes_id changes the chain. MCP search returns it per
hit, while the hook that injects memories automatically drops it, so the
flagged record reaches the model unmarked.
CTX Cognitive Version Control
shows detection with the disposition decided before anyone looks. Its
branch merge compares every entity present on both sides, records a
DivergentChange for each difference, and then keeps the
incoming copy of every shared id and saves the merged working context
before returning the list, whose summary calls the conflicts
"requiring review". There is no keep-current, keep-both or
refuse outcome, and goals are merged without a conflict check at all.
The comparison is C# record equality over list-valued fields, so after a
round-trip to disk it should also flag entities that did not change — a
queue that is both pre-drained and over-filled.
Counterparts meets
requirements 1, 3 and 4 and misses 5. A pair a dream flags or a write
notices stays live with an "Unsettled — may be out of date"
label, and a settle picks changed, corrected
or open, each with its own effect on the losing memory, an
actor, a reason and an exact undo on the
contradiction_settles trail. What the settle does not do is
reach the write path: a corrected memory is archived by id,
so the same claim written again in other words mints a new live row, and
the write-time neighbour check leaves archived rows out.
Minta is the counterexample where
detection and resolution were both written and the edge between them was
not. Its lifecycle scanner finds stale, redundant and conflicting
objects, and findings_to_inbox_items hands each one to the
inbox as its suggestion sentence alone, dropping the object id, so
confirming a finding stores the sentence as a new
lesson_learned memory and leaves the stale object active.
The resolver that would have acted, resolve_and_persist,
marks the loser stale and records the winner, and nothing
in the tree calls it. A detector whose output cannot name its target can
only ever add notes about the store.
Agent Memory Engine
detects and never disposes. ConflictService flags a
conflict on a confidence gap of more than 0.15 or on five keyword pairs
such as must/may, and
PromotionService then sets the existing node to
needs_review and records the candidate with the same
status. No MCP tool, CLI command or HTTP route lists, approves or
rejects either. _fill_bucket does not skip the status, so
the flagged memory keeps reaching the agent — labelled
non-authoritative in its trace provenance — while the
README lists it as excluded; the problem statement above, verbatim.
Marvis is the case where the
disposition exists and has no hand to carry it out. Write-time and
nightly passes would file a supersede_candidate into an
approval queue, and the queue has a proper lifecycle — pending,
approved, rejected, applied, reverted — with an append-only state table.
Approval returns guidance reading "manual Edit" and a target path the
proposal never sets, and nothing on the approval path calls the one
function that retires the old learning, so an approved supersession
changes nothing a reader sees.
MGI-Mind resolves every
contradiction automatically, and shows what happens when the resolver's
inputs have no producer. A fact on a Single or
TemporalSingle predicate duels the strongest visible rival
and ends active, contested or
quarantine_candidate, with the loser retired
stale or superseded under a cross-process
lock. Entrenchment's dependants term is written only by a migration
command, so on the live path the disposition is decided by age, and any
rival under about nine months old loses. It also misses requirement 5:
re-adding a retired triple rebuilds its row with a fresh duel, so the
old value can win the rematch.
IHMT disposes of a
contradiction on the wrong record. Thirteen regexes put six attributes
on a dated timeline, and a newer value demotes the older fact to
HISTORICAL, closes its validity interval, and writes a
persisted notice into the old fact's source leaf. The leaf itself stays
ACTIVE and the navigator never reads the fact timeline, so
the old and new statements rank on their words alone. A question about
the current value then returns AMBIGUOUS between them,
which the project's roadmap records. A disposition that lands on
the derived fact and not on the text the reader retrieves is a label,
not a resolution.
Harness Evolution as
Learning shows the disposition being chosen by list position. Its
ACE-style updater detects near-duplicate bullets by character-trigram
cosine above 0.85 and always keeps the earlier one, so when a newer
bullet negates an older one — "Text messages should not end with my
first name." scores 0.931 against the positive form — the
correction is the one discarded, and the history still records the add
as applied. The TEPA baseline in the same repository resolves the same
situation the other way: one active precedent per attribute key, the
newer write revokes the older, and the revoked entry stays archived with
revoked_at and revoked_by.
Graft wires detection to
resolution and then lets one disposition fall out of both. A scan caches
candidates with deterministic ids, maintain resolve applies
one verb per candidate with the transition, a dismissal keyed on an
evidence digest and an audit row in one transaction, and
restore inverts every verb. Two edges are missing. The
highest-priority kind, contradiction, reads
CONTRADICTS edges that only a test fixture writes. And
stale, the disposition the skill recommends when the agent
is unsure, leaves the note in every search while every scan selects
state = 0, so a stale note is never offered for resolution
again.
Tests to require
- Detect a contradiction, resolve it every available way, and assert retrieval changes correspondingly for each.
- Resolve, then re-run the pipeline that produced the finding, and assert the resolution survives.
- Assert a pending finding is still retrievable and a resolved one is not ambiguous.
- Feed the detector known non-conflicts (same fact, different phrasing; two facts about different periods) and count false positives.
- Assert every resolution records an actor and a reason.
- Leave a finding unresolved for the retention period and assert something sensible happens.
- Measure kept-versus-dismissed over a real queue; that ratio is the detector's precision and nothing else reports it.