action="create" fact whose value
differs from a fact already active for the same predicate, it does
not choose on the user’s behalf: it opens a conflict.
The principle: hide both, never guess
In the Context Assembler, any fact whose id appears in aConflictSet
with status="open" is excluded from scoring and blocked with
reason_code = conflict_open — including facts still candidate (which
would never have been served anyway, but are now explicitly traced as
blocked). The returned packet then carries a warning:
ConflictSet model
Observability
GET /v1/conflicts returns, alongside the list, two counters designed
for monitoring without parsing the list: open_count and
oldest_open_seconds (age of the oldest still-open conflict, null if
there is none). Nothing auto-resolves a conflict — that is intentional
(“hide both, never guess”); this counter is the guardrail against silent
accumulation.
Resolving a conflict
POST /v1/conflicts/{id}/resolve with {project_id, keep_fact_id}:
- the kept fact moves to
active(transitioncandidate/disputed → activeif needed); - every other fact of the set moves to
superseded, withsupersedes_idpointing at the kept fact; - the set moves to
resolved(+resolved_at).
/v1/context serves the kept fact normally — it is no
longer blocked by conflict_open.
Typed errors: conflict_not_found (404 — including a conflict from
another project, same error as an unknown id, no leak),
conflict_already_resolved (409), fact_not_in_conflict (422, if
keep_fact_id does not belong to the set).
API reference — Conflicts
GET /v1/conflicts, POST /v1/conflicts/{id}/resolve: exact schemas.
