| name | brain-conflict |
| description | How to detect and resolve conflicting beliefs in the INITE Brain knowledge graph — the COMPETING fact status, get_competing_facts, detect_contradiction preflight, and the human-in-the-loop adjudication workflow. Use when the timeline shows two facts disagreeing on the same predicate, or when an agent needs to decide what to record without making the disagreement worse. |
brain-conflict
Brain's conflict resolver runs on every record_fact. Most of the time it picks a winner cleanly — the new fact either INSERTED (no overlap), SUPERSEDED (clearly beats the prior), or REJECTED (too unconfident). When it can't pick — when two facts overlap in valid-time, are too semantically close, and score within margin — they BOTH stay alive in COMPETING status. This skill is how you handle those.
What COMPETING means
The conflict resolver writes status='competing' when:
- The predicate is
bitemporal (status, intent, address, …) — single_active predicates always supersede.
- A prior fact's valid-time interval overlaps the new fact's.
- The two facts are cosine-similar above
CONFLICT_SIMILARITY_THRESHOLD (semantically close enough that one might be a rephrase of the other).
- The score gap between them is ≤
CONFLICT_MARGIN_SUPERSEDE — neither dominates.
The resolver refuses to pick. Both rows stay searchable but are surfaced as a known disagreement. The dreams loop's resolver pass (/v1/dreams/run) periodically wakes up and asks an LLM judge to break ties for pairs older than the min-age gate; multi-way conflicts (3+) escape auto-resolution and stay for human adjudication.
Detecting disagreements — get_competing_facts
get_competing_facts({
entityId: "knowledge_entity:01HXYZ...",
predicate: undefined,
asOf: undefined,
})
Returns groups keyed by (entityId, predicate):
{
"entityId": "knowledge_entity:01HXYZ...",
"groups": [
{
"key": "knowledge_entity:01HXYZ...::status",
"predicate": "status",
"facts": [
{ "factId": "...", "object": "active", "recordedAt": "...", "source": ... },
{ "factId": "...", "object": "churned", "recordedAt": "...", "source": ... }
]
}
]
}
- A group of 2 facts is a pair the resolver left for adjudication — the dreams loop will try to break it, or a human will.
- A group of 3+ facts is a multi-way disagreement the resolver escalated. Dreams resolver won't touch these; they wait for an operator.
asOf filters to what was competing at that moment — useful for postmortems ("what was this customer's status disagreement on April 1?").
Preflight before recording — detect_contradiction
Before you record_fact on a contentious predicate, ask brain what would happen:
detect_contradiction({
entityRef: { vertical: "rent", id: "cust_42" },
predicate: "status",
object: "active",
validFrom: "2026-05-01T00:00:00Z",
confidence: 0.9,
sourceVertical: "rent",
})
Returns:
{
"wouldOutcome": "COMPETING",
"reasoning": "bitemporal predicate, strongest similar prior (cosine 0.91) score 0.612 too close to candidate 0.624 (gap 0.012) within margin; both would remain active in COMPETING status.",
"opposingFacts": [
{ "factId": "...", "object": "churned", ... }
],
"predicatePolicy": { "semantics": "bitemporal", "decayHalfLifeDays": 60, "piiClass": "none" }
}
The reasoning string names the rule that fired and the numerical gap. Read it before deciding what to do.
Adjudication workflows
LLM auto-resolution (dreams loop)
For low-stakes pairs aged past the min-age window, the dreams loop's resolver does this:
- Pulls competing pairs (
status='competing' AND age ≥ DREAMS_RESOLVE_MIN_AGE_DAYS)
- Walks neighbouring context facts on the entity
- Asks an LLM judge to pick
a_wins | b_wins | unsure
- On confident verdict, marks the loser superseded; on
unsure, leaves the pair
This is fully automated and writes through retract_fact with reason: "dreams_resolver:a_wins" style. The audit row stays — operators can always see "this was resolved by automated judgement".
Human-in-the-loop (your job as the agent)
For high-stakes pairs or 3+ multi-way groups:
get_competing_facts to find the disagreement.
- Present the user the COMPETING pair with timestamps + sources. Phrase it neutrally — "brain has two records for this; which do you want to keep?"
- On user verdict:
- Loser →
retract_fact({ factId: <loser>, reason: "operator verdict: <text>" })
- Winner stays as-is. The retract revives the supersede chain if needed.
- Optional: record a fresh fact reinforcing the user's verdict with
human_declared source — that source class has the highest trust weight and tilts future conflicts the same way.
Recording a new fact that touches the disagreement
When the user wants to record something new on a predicate that's already in COMPETING, run detect_contradiction first. If the dry-run says the new fact would also land COMPETING, surface that to the user before you write — "this would create a 3-way disagreement; would you rather pick one of the existing values first?"
What NOT to do
- Don't auto-pick by recency. "Newer = right" is the resolver's recency weight, not a UI rule. If the resolver decided not to pick, your agent shouldn't override silently.
- Don't forget to surface the source. A competing pair where one source is
human_declared and the other is inbox_extraction is almost always resolvable by the user; the disagreement is between a person and an LLM. Surface that.
- Don't retract one side of a 3-way without picking the winner. Retracting one of three competing facts leaves a 2-way pair the resolver still can't break. Either retract two (leaving a single active) or convert the survivors to a single
human_declared fact via record_fact.
- Don't ignore
asOf when adjudicating historical conflicts. Two facts that competed on April 1 may have been resolved by May — call get_competing_facts({ asOf: "2026-04-01..." }) to see the historical state, not the current one.
Pitfalls
single_active predicates never produce COMPETING by overlap alone. They auto-supersede. If you see COMPETING on a single_active predicate, the resolver is in an unusual state — check predicatePolicy from detect_contradiction to confirm.
- The 2-fact min and 3+ max are by-design. Dreams resolver only touches 2-fact pairs; the 3+ escalation is the safety valve when the conflict is too tangled for an LLM judge to break confidently.
- A retract doesn't always revive the supersedee. Revive only happens for facts retracted via the
supersedeChain (retractionReason === 'superseded'). An explicit operator retract is treated as independent; the predecessor stays superseded.
Companion tools
record_fact / retract_fact — the write side of adjudication (see brain-write)
detect_contradiction — preflight before write
get_entity_timeline — full history including superseded chain (see brain-recall)
search_knowledge with includeContested: true — surfaces COMPETING facts in search results (see brain-search)