Short answer: Find live memories that share a subject but disagree on the value, then flag them before prune or supersession.
Similarity often ranks a correction near the old fact, so distance is a poor contradiction detector. Engram transform-with-context resolves many conflicts on write and only commits finished changes. Bulk imports and parallel agents still need read-time scoped scans. Once flagged, agents should not cite both; prune or supersede the loser.
Contradiction detection is how an agent notices that two live memories cannot both be true. Pruning removes leftovers after a winner is chosen. Soft forgetting only demotes weak hits. Detection is the step that finds the conflict in the first place. Embedding similarity alone is a poor judge. A corrected fact often looks more like the old fact than a harmless paraphrase does. Weaviate Engram’s transform path resolves many conflicts on write. Your application still needs a read-time check when search returns competing values in the same scope.
This chapter covers what counts as a contradiction versus a restatement, why vector distance fails as a detector, how Engram’s TransformWithContext path handles updates, and how to add a scoped conflict scan before the agent answers. The goal is a clear signal that two memories share a subject and disagree on the object, so supersession or prune can act with confidence.
When do two memories contradict, and when do they only overlap?
After you prune stale rows, the remaining store can still hold two current-looking facts that fight. A contradiction means the same subject and relation assert incompatible values. “Preferred glaze is celadon” and “preferred glaze is tenmoku” cannot both guide the next firing. A restatement says the same thing in new words. “Uses celadon for tableware” restates the first fact. Overlap without conflict is also common. “Fires to cone 10” and “prefers reduction atmosphere” can both be true.
Implicit conflicts are harder. A later note that the studio switched to electric kilns may invalidate an older “always reduce for copper reds” rule without saying “never reduce.” Research on state-aware memory calls this out as a frequent failure mode. Agents retrieve the update, then still honor a query that assumes the old state. Detection therefore needs more than string negation. It needs a judgment that two retrieved lines cannot jointly advise the next action.
Scope the question tightly. Contradictions only matter inside one user_id and property boundary. Two users preferring different glazes is not a conflict. Two looms with different tension setpoints is not a conflict. Engram scopes keep those worlds apart. Your detector should search inside the same group and properties the agent will use at answer time.
Why can’t similarity scores tell you a contradiction from a duplicate?
Once you know what a conflict is, the tempting shortcut is to flag near-duplicate vectors as updates. That shortcut fails. A value flip is a minimal edit. The embedding for “preferred glaze is tenmoku” sits close to “preferred glaze is celadon.” A true paraphrase can sit farther away. Studies of evolving retrieval memory find that cosine similarity barely separates contradictions from duplicates. No safe threshold exists. An LLM asked only “are these similar?” will also confuse update with echo.
Structural keys work better. Treat each durable fact as subject, relation, and object. When a new memory shares subject and relation but changes the object, you have a candidate contradiction. Timestamps then decide which value is newer. Engram already surfaces created_at and updated_at on every memory. Your scan can pair high-overlap search hits, extract a cheap key, and list pairs where objects disagree.
Do not outsource freshness entirely to the model at answer time. Deterministic assembly of candidates, then a max-timestamp or max-serial pick, outperforms asking the LLM to track which fact is current across a long context. Use the model to label hard pairs. Use rules to choose the survivor when the key is clear.
How does Engram resolve conflicts on the write path?
Knowing that similarity fails leads to a better default: resolve on write when you can. Engram pipelines extract facts, then transform them against related memories already in Weaviate. A TransformWithContext step retrieves neighbors and asks an LLM tool call which action to apply. Typical actions are rewrite, keep, and delete. When a role changes from engineer to CEO, the older job memory can be rewritten to include history, and the duplicate extract can be dropped so two titles do not both persist.
Transform steps also cover bounded topics, where multiple extracts collapse into the single memory allowed for that topic’s scope. Commit is the only step that persists creates, updates, and deletes. Until commit finishes, intermediate drafts are not searchable. That keeps agents from retrieving half-merged states. Tune topic descriptions and transform instructions so preference changes are treated as updates, not as unrelated new facts.
Write-path resolution is not complete coverage. Bulk imports, pre-extracted dumps, and parallel agents can still leave siblings alive. That is why a read-time detector remains useful. Prefer sending corrections through memories.add so the pipeline can reconcile. Use an application scan when you must answer from whatever is already stored.
What does a read-time contradiction scan look like?
Suppose a ceramics studio agent on glaze-test-tile-rack-4 must recommend a liner glaze. Search may return both an old celadon preference and a newer tenmoku correction. Before drafting a reply, the app clusters hits and flags incompatible objects.
import os
import re
from datetime import datetime
from engram import EngramClient, HybridRetrieval
client = EngramClient(api_key=os.environ["ENGRAM_API_KEY"])
potter = "potter-elena-cho"
rack = {"rack_id": "glaze-test-tile-rack-4"}
CONFLICT_PROMPT = (
"Do these two studio memories contradict on a single durable preference? "
"Answer CONTRADICT or COMPATIBLE, then one short reason."
)
def parse_ts(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def glaze_key(text: str) -> str | None:
m = re.search(r"preferred\s+glaze\s+is\s+(\w+)", text, re.I)
return m.group(1).lower() if m else None
def seed_conflict():
client.memories.add(
"On glaze-test-tile-rack-4 the preferred glaze is celadon for everyday bowls.",
user_id=potter,
group="personalization",
properties=rack,
)
client.memories.add(
"Update for glaze-test-tile-rack-4: preferred glaze is tenmoku, not celadon.",
user_id=potter,
group="personalization",
properties=rack,
)
def find_contradictions(query: str = "preferred glaze bowls"):
hits = client.memories.search(
query,
user_id=potter,
group="personalization",
properties=rack,
retrieval_config=HybridRetrieval(limit=12),
)
by_value = {}
for m in hits:
value = glaze_key(m.content)
if not value:
continue
by_value.setdefault(value, []).append(m)
if len(by_value) < 2:
return [], hits
# Same relation (preferred glaze), different objects → contradiction candidates.
candidates = []
values = list(by_value.items())
for i in range(len(values)):
for j in range(i + 1, len(values)):
left = max(values[i][1], key=lambda x: parse_ts(x.updated_at))
right = max(values[j][1], key=lambda x: parse_ts(x.updated_at))
newer = left if parse_ts(left.updated_at) >= parse_ts(right.updated_at) else right
older = right if newer is left else left
candidates.append(
{
"older_id": older.id,
"newer_id": newer.id,
"older": older.content,
"newer": newer.content,
"label": "CONTRADICT",
"hint": CONFLICT_PROMPT,
}
)
return candidates, hits
seed_conflict()
conflicts, retrieved = find_contradictions()
for c in conflicts:
print(c["label"], "keep", c["newer_id"], "review", c["older_id"])
print(" newer:", c["newer"])
print(" older:", c["older"])
The regex key is intentionally narrow. Real systems replace it with a small extractor that emits subject-relation-object triples. Keep Engram as the store and search layer. Keep conflict labeling in a function you can unit test. When a pair is labeled conflict, feed the newer wording back through add so transform can rewrite, or delete the older id after archival if your policy allows.
What should the agent do once a contradiction is flagged?
Detection without a response policy only produces alerts. For user preferences, prefer the newer updated_at and withhold the older line from the prompt. For safety rules, escalate instead of auto-picking. For ambiguous pairs, ask the user one clarifying question and write the answer as a fresh memory. Log the pair ids so prune jobs can clear the loser later.
Measure how often top-k search returns incompatible objects for the same key. Measure how often the agent cites both in one reply. Those rates should fall after write-path transform tuning and after read-time scans. If they stay high, tighten topic instructions so updates rewrite instead of append.
Contradiction detection is the gate before supersession with history. Once you know which fact lost, you can rewrite the winner to preserve what changed, instead of silently erasing the past.
Our next chapter, How do you supersede memories without losing history?, shows how to replace a contradicted fact while keeping a readable trail of what used to be true.