Self-Consistent Agent Memory: Refuse to Remember a Contradiction
Objective
Every agent framework has the same failure: memory is append-only trust. A user misspeaks, a scraped page lies, another agent hallucinates — and the falsehood is stored next to the facts, recalled with equal confidence forever. This is memory poisoning, and the usual answer ("review your memories sometimes") doesn't scale.
SynapCores co-locates agent memory (MEMORY_STORE / MEMORY_RECALL) and an
NLI verifier in the same process, so the fix is structural: before a
candidate memory is stored, verify it against what the namespace already
believes. A contradiction is rejected and logged; everything else is
stored. The whole check is one statement over the recall set.
Step 1: Establish what the agent knows
MEMORY_STORE auto-creates the namespace on first write — no schema step.
SELECT MEMORY_STORE('recipe_consistent_mem',
'The customer prefers to be contacted by email, never by phone.') AS m1;
SELECT MEMORY_STORE('recipe_consistent_mem',
'The customer is on the Aurora plan, which includes a 14-day return window.') AS m2;
SELECT MEMORY_STORE('recipe_consistent_mem',
'The customer manages procurement for a mid-size logistics company.') AS m3;
Step 2: Ground truth — what does recall surface for a contact question?
SELECT content, similarity
FROM MEMORY_RECALL('recipe_consistent_mem', 'how should we contact the customer', 3);
Expected: the email-preference memory ranks first. This recall set is exactly the evidence the gate will verify against.
Step 3: The gate — verify a candidate memory against recalled beliefs
A candidate arrives (from a user message, another agent, a scraper): "The customer prefers phone contact." Verify it against the namespace before storing:
SELECT NLI_VERIFY('nli-default',
'The customer prefers phone contact.',
NLI_ANCHORS(content)) AS gate
FROM MEMORY_RECALL('recipe_consistent_mem', 'The customer prefers phone contact.', 5);
Expected: verdict LIE (observed confidence ≈ 0.999), with
violated_anchor = the stored email-preference memory. The recall query uses
the candidate itself as the probe, so the memories most likely to
conflict are exactly the ones retrieved.
Step 4: Route on the verdict — store, or log the conflict
CREATE TABLE IF NOT EXISTS recipe_consistent_mem_conflicts (
id INT PRIMARY KEY,
candidate TEXT,
verdict TEXT,
violated_anchor TEXT,
confidence DOUBLE,
seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
The contradicting candidate goes to the conflict log, not to memory:
INSERT INTO recipe_consistent_mem_conflicts (id, candidate, verdict, violated_anchor, confidence)
SELECT 1,
'The customer prefers phone contact.',
JSON_UNQUOTE(JSON_EXTRACT(gate, '$.verdict')),
JSON_UNQUOTE(JSON_EXTRACT(gate, '$.violated_anchor')),
CAST(JSON_EXTRACT(gate, '$.confidence') AS DOUBLE)
FROM (
SELECT NLI_VERIFY('nli-default',
'The customer prefers phone contact.',
NLI_ANCHORS(content)) AS gate
FROM MEMORY_RECALL('recipe_consistent_mem', 'The customer prefers phone contact.', 5)
) checked
WHERE JSON_UNQUOTE(JSON_EXTRACT(gate, '$.verdict')) = 'LIE';
SELECT candidate, verdict, violated_anchor FROM recipe_consistent_mem_conflicts;
Expected: 1 row — the phone-contact candidate, verdict LIE, violated
anchor naming the email-preference memory. A human (or a supervising agent)
now sees that there is a conflict and which belief it collides with —
instead of two contradictory memories being served with equal confidence.
Step 5: A compatible candidate passes the same gate
"The customer works in logistics procurement." restates an existing belief:
SELECT JSON_UNQUOTE(JSON_EXTRACT(NLI_VERIFY('nli-default',
'The customer works in logistics procurement.',
NLI_ANCHORS(content)), '$.verdict')) AS verdict
FROM MEMORY_RECALL('recipe_consistent_mem', 'The customer works in logistics procurement.', 5);
Expected: TRUTH — the gate lets it through, and your application
follows with the MEMORY_STORE (or MEMORY_UPSERT with a dedup policy, so
restatements merge instead of piling up).
Step 6: Decide your policy for UNVERIFIED_PLAUSIBLE
A genuinely new fact — "The customer attended the October product webinar." — contradicts nothing and is entailed by nothing:
SELECT JSON_UNQUOTE(JSON_EXTRACT(NLI_VERIFY('nli-default',
'The customer attended the October product webinar.',
NLI_ANCHORS(content)), '$.verdict')) AS verdict
FROM MEMORY_RECALL('recipe_consistent_mem', 'The customer attended the October product webinar.', 5);
Expected: UNVERIFIED_PLAUSIBLE. This is the policy fork, and it is
yours, not the model's: store it (new knowledge is usually unverified — the
sane default for memory), store it flagged with lower confidence metadata, or
quarantine it. What the gate guarantees is only this: nothing that
contradicts established memory gets in silently.
Cleanup (Optional)
DROP TABLE IF EXISTS recipe_consistent_mem_conflicts;
SELECT MEMORY_FORGET('recipe_consistent_mem', id) AS forgotten
FROM MEMORY_RECALL('recipe_consistent_mem', 'customer', 10);
Use it from your app
- Wrap the gate in your agent's memory-write path: recall-with-the-candidate → verify → route on verdict. One extra round trip, ~tens of milliseconds on CPU since v2.1.2.
- Pair with
MEMORY_UPSERTpolicies: the NLI gate handles contradiction, upsert dedup handles repetition — different failure modes, both covered. - Why in-DB: memory, recall, and the verifier share one engine; there is no window where another writer slips a contradiction in between your check and your store (run both in one transaction).
Key Concepts Learned
- Memory poisoning is a write-path problem; filtering at recall time is too late because retrieval ranks by similarity, not truth.
- Probing recall with the candidate itself retrieves exactly the beliefs most likely to conflict with it.
- Contradiction precedence makes
LIEa reliable rejection signal, whileUNVERIFIED_PLAUSIBLEis a policy decision — most memories are new facts, and rejecting everything unverified would make the agent unable to learn. - The conflict log is as valuable as the gate: it turns silent corruption into a reviewable queue with the exact violated belief attached.