Skip to content

Oracle Resolution Loop

The oracle resolution loop is the mechanism by which a Contested or QueuedForAdjudication claim moves to a resolved state. The OraclePort is a pluggable trait — you supply the implementation; mempill supplies the loop semantics, the durable pending store, and the resolution write path.

The oracle is the only mechanism that can flip a contested belief. No internal signal — corroboration count, recency, frequency — substitutes for oracle judgment (delivery constraint DC-4, invariant I7).

Without an oracle configured, conflicts route to Contested (invariant I7). With an oracle configured (open_oracle / open_oracle_in_memory), conflicts route to QueuedForAdjudication instead — the engine calls request_adjudication() and stores the handle durably.

The oracle loop operates in two halves:

Request side (at ingest time):

  1. A belief-overturning conflict is detected by the gate (C7).
  2. oracle_present = true (oracle is configured) — gate routes to heavy path.
  3. Engine calls oracle.request_adjudication(agent_id, request).
  4. AdjudicationRequest carries: {subject_line, incumbent, challenger, criticality, reason}.
  5. Oracle returns an opaque handle_id (UUID) immediately. It does not block.
  6. Engine stores handle_id → (agent_id, challenger_claim_ref, incumbent_claim_ref) in the durable pending_adjudications table (survives restart).
  7. ingest_claim returns disposition: QueuedForAdjudication.

Submit side (when oracle has a verdict):

  1. Host calls engine.submit_adjudication(response).
  2. AdjudicationResponse carries: {handle_id, verdict, evidence_provenance}.
  3. Engine looks up handle_id in the pending store.
  4. Engine acquires the per-agent write lock (I9, DC-2).
  5. Engine applies the verdict atomically (I9).
  6. Engine removes handle_id from the pending store (idempotency guard).
  7. Returns AdjudicationOutcome {handle_id, disposition, claim_ref}.
Verdict Disposition after submit_adjudication What the engine writes
Affirm Challenger → CommittedCheap (or Reinstated if reopening a bounded claim). Incumbent → Superseded. Bounding ValidityAssertion on incumbent + ledger entry for both. Resolution carries External(ExternalFirstHand) provenance.
Deny Incumbent stays active. Challenger → Superseded. Bounding ValidityAssertion on challenger + ledger entry.
Unknown No disposition flip. Both claims remain Contested. Handle consumed. Ledger entry recording the abstain.
Host (ingest) EngineHandle Oracle (host impl) Host (submit)
│ │ │ │
│──ingest_claim()─►│ │ │
│ │ conflict detected │ │
│ │ oracle_present=true │ │
│ │──request_adj(req)───►│ │
│ │◄─handle_id:Uuid──────│ │
│ │ (stored in DB) │ │
│◄─QueuedForAdj────│ │ │
│ │ │ │
│ [async gap — milliseconds to days] │ │
│ │ │ │
│ │ Oracle decides verdict │
│ │ │ │
│ │◄─────────────────────│─submit_adj(resp)───│
│ │ acquire write lock │ │
│ │ apply verdict (I9) │ │
│ │ write ledger+bound │ │
│ │ remove handle │ │
│ │──────────────────────│──AdjudicationOutcome►│

The pending_adjudications table is the source of truth for outstanding oracle requests. It persists across engine restarts. This means:

  • If the process crashes between request_adjudication and submit_adjudication, the pending row is still in the database.
  • On restart, list_pending_adjudications() returns all outstanding items. The oracle implementation can reinspect them and re-queue if needed.
  • Claims stay QueuedForAdjudication in the ledger until a verdict arrives.

The engine supports a configurable default_adjudication_ttl (optional; None = indefinite). When a handle expires:

  • The engine sets the pending row’s status to expired.
  • sweep_expired_adjudications() reverts all expired-but-unresolved claims to Contested and writes ledger entries for each.
  • The host is responsible for calling sweep_expired_adjudications() periodically (or at startup) — the engine does not run a background sweep.
# Sweep on startup to revert any claims whose TTL elapsed while offline.
n = engine.sweep_expired_adjudications()
if n > 0:
print(f"[startup] Reverted {n} expired adjudication(s) to Contested.")
import mempill
class MyOracle:
"""Minimal oracle: always abstain."""
def request_adjudication(self, agent_id: str, request: dict) -> str:
import uuid
# In a real oracle: store (handle_id → request) in your queue; return handle_id.
# The engine stores the full request in pending_adjudications; you just need the ID.
return str(uuid.uuid4())
oracle = MyOracle()
engine = mempill.open_oracle("/path/to/agent.db", oracle)
# Later, when you have a verdict:
outcome = engine.submit_adjudication({
"handle_id": handle_id,
"verdict": "Affirm", # or "Deny" or "Unknown"
"evidence_provenance": {"type": "External", "kind": "ExternalFirstHand"},
})
print(outcome["disposition"]) # CommittedCheap (challenger won)
# OracleEngine exposes list_pending_adjudications().
pending = engine.list_pending_adjudications(agent_id="my-agent")
for item in pending:
print(item["handle_id"], item["predicate"],
item["incumbent_value"], item["challenger_value"])

What happens if the oracle never responds?

Section titled “What happens if the oracle never responds?”
  • With no TTL configured: claims stay QueuedForAdjudication indefinitely. They are safe and auditable; they do not affect other beliefs.
  • With a TTL configured: sweep_expired_adjudications() reverts them to Contested after the TTL elapses.
  • In either case, the engine never silently picks a winner. Absence of oracle response = Contested, not incumbent-wins.
  • I7 — Contested first-class: oracle absent or UnknownContested, never silent incumbent-wins.
  • I9 — Atomic commit unit: submit_adjudication applies verdict atomically (bounding assertion + disposition flip + ledger entry in one transaction).
  • DC-4 — Oracle is the only correctness pressure: no internal signal substitutes for oracle judgment.
  • I4 — Provenance immutable: oracle-resolved claims carry External(ExternalFirstHand) provenance, written at resolution time and immutable thereafter.