What the graph can know about contradictions (and what it can't)
When an agent files a memory that contradicts a standing rule, nothing should let that pass silently. Here's the design we landed on — and where the boundaries are.
When an agent files a memory that contradicts a standing rule, nothing should let that pass silently. That's a clean sentence to write. Getting to a design that actually delivers it turns out to be harder than it looks.
This post is about where the difficulty lives — two assumptions we had to discard, what we built instead, and where the limits are.
The gap
Recordari's contradicts edge lets you record disagreement explicitly. If you know two memories conflict, you file the edge and it's visible: in audit, in orient(), in the graph. That part works.
The problem is that it depends entirely on whoever is filing the memory. A careless agent, or a deliberately misaligned one, just doesn't file the edge. The graph then holds two things that disagree — a new filing and an existing standing rule — with no trace that anyone noticed. The contradiction is real. It's invisible.
The goal was to make the invisible visible. That required two design decisions we got wrong on the first pass.
Wrong assumption 1: contradiction severity falls out of trust weights
Recordari already has a trust model. Memories have intrinsic weights by node_kind. So the obvious approach is: use those weights to score how serious a contradiction is.
The problem: finding, decision, and standing nodes all carry the same intrinsic weight (1.0). From a pure information-theoretic view, they're equally known. But from a governance perspective, they are not equivalent at all.
Contradicting a standing node is categorically different from contradicting a finding. A standing node is something a human has deliberately written as a durable constraint on future behaviour — an architectural rule, a security policy, an explicit team decision. When agent-filed content contradicts one, that's the rogue agent drift scenario. It matters more.
So we needed a second axis: contradiction authority — how anomalous is a contradiction from an oversight perspective, based on the kind of node being contradicted. This is entirely distinct from epistemic weight, and it can't be derived from it.
The formula:
ContradictsAuthority(contradictor, contradicted)
= TargetAuthority(contradicted) × IntrinsicWeight(contradictor)
TargetAuthority values: standing=1.0, decision=0.8, finding=0.6, goal=0.4, option=0.3, issue=0.2, assumption=0.1, reference=0.0, transient=0.0.
The product means non-propositional nodes — reference, transient — produce zero. It's not meaningful to flag contradictions against them. A low-trust contradictor doesn't inflate a high-authority target's score.
One firm boundary here: this score measures governance risk, not real-world consequence severity. A finding about something with serious consequences has the same content gravity whether it contradicts a standing node (score 1.0) or a decision node (score 0.8). The formula can't see that. If you need content-level severity, you have to bring your own signal — the trust score from significance(mode=trust), the why_matters field, or a human review step. Those two things are not the same question, and we didn't want the implementation to pretend they were.
Wrong assumption 2: semantic similarity detects contradictions
The obvious detection approach: when you file a memory, search semantically against existing nodes and flag close matches as potential contradictions.
The problem: embedding similarity measures topical closeness, not agreement or disagreement. "Cap the pool at 20" and "cap the pool at 35" are nearly identical in embedding space — and they genuinely contradict. But "cap the pool at 20" and "raise the pool to 20" are equally close — and they agree. A similarity threshold cannot distinguish these cases.
This means the check inside remember() finds candidates, not confirmed contradictions. The server-side score is a retrieval filter, never a verdict. The response says possible_contradicts: true with candidate IDs — whoever is filing (or reviewing the filing) still adjudicates whether an actual contradiction exists.
We considered whether this limitation made the synchronous check not worth shipping. We landed on shipping it anyway, for one reason: detection and resolution are separable. The check finds candidates. The human or agent decides what to do with them. And critically — the server logs a contradiction_flagged audit event regardless of what the agent does next.
That last part is what makes this meaningful. Without the log, an agent that ignores a surfaced candidate leaves no trace. With it, "agent was shown this candidate and did not file a contradicts edge" becomes a detectable, attributable event.
What we built
Synchronous (inside remember()): When you file a new memory, Recordari runs a lightweight semantic search against a capped candidate set — standing nodes first, then recent in-domain. It reuses the embedding already computed for the write. If a candidate crosses the threshold, the response includes possible_contradicts: true, the candidate IDs, and their authority severity scores. The contradiction_flagged audit event is written regardless.
Asynchronous (audit(mode=conflicts)): A full-domain sweep that surfaces a conflict-candidate count at orient root — the same structural position as stale_count. This is the sixth drift mode alongside stale, orphan, and low-connection. It keeps the synchronous path cheap and catches nodes that became contradictory after both were filed.
Neither path asserts that a contradiction is confirmed. Both surface candidates for adjudication. The philosophy is the same as audit(mode=stale): structure surfaces candidates, humans and agents adjudicate. The system doesn't claim to reason about truth.
The collaboration model underneath
The contradicts edge that sits under all of this is worth naming explicitly. In Recordari, nodes belong to their creator — no one else can update them. If you disagree with a node you didn't file, you create a contradicts edge pointing to it. You record the disagreement without touching the original.
This is deliberate. The graph accumulates perspectives rather than converging to a single rewritten truth. Teams reason through disagreement structurally, not by one party silently overwriting the other. An Editor can override with a recorded reason, but the default is immutability.
The detection work described here sits on top of this model. The edge was always there for cases where someone knew about the contradiction. What's new is surfacing candidates for the cases where they didn't.
References: Wooldridge et al., Weighted Argument Systems (AIJ 2011) — OWASP Agentic AI Threats and Mitigations