Skip to content

Valid-Time Succession

Consider two claims about the same predicate:

  • Alice: acme:ceo / held_by = "Alice", valid [2020-01-01, 2024-06-01), valid_time_confidence = 0.9
  • Bob: acme:ceo / held_by = "Bob", valid [2024-06-01, ∞), valid_time_confidence = 0.9

These two claims describe the same predicate at different points in time. They do not conflict — they form a clean temporal succession: Alice held the position before June 2024; Bob holds it from June 2024 onward. Querying “who is CEO today?” should return Bob. Querying “who was CEO in 2022?” should return Alice.

Without succession-aware logic, the engine would see two live functional claims with different values on the same (subject, predicate) and route them to Contested. That is incorrect: there is no genuine ambiguity here, and no oracle is needed. The valid-time windows tell the whole story.

A claim pair qualifies as a clean temporal succession when all of the following hold:

  1. Same (subject, predicate), different value.
  2. Both claims have valid_time_confidence >= 0.7 (the trust threshold — both windows are trusted).
  3. Both claims have bounded windows: valid_from is present on the successor; valid_until is present on the incumbent.
  4. The incumbent’s window ends at or before the successor’s window begins: incumbent.valid_until <= successor.valid_from (non-overlapping, half-open interval semantics where valid_until is exclusive).

When all four conditions hold, the reconciler (C3) classifies the relationship as Succession — a distinct classification from NoConflict, but one that also routes to the cheap path. The gate (C7) treats Succession the same as NoConflict for routing purposes: CommittedCheap for the successor claim. No oracle is needed.

After both claims are committed with CommittedCheap, querying the belief uses instant-selection in the fold (C2). Given a query instant q (defaulting to now):

  • The fold checks whether all live functional claims on the subject-line form a non-overlapping trusted succession.
  • If they do, it selects the single claim whose valid_time window contains q.
  • Only that claim appears in the belief projection; has_conflict is false.
Query instant Result
q = 2022-03-15 (within Alice’s window) Resolved, value = "Alice"
q = 2024-06-01 (exactly at Bob’s start; Alice’s end is exclusive) Resolved, value = "Bob"
q = 2026-01-01 (within Bob’s open-ended window) Resolved, value = "Bob"
q falls in a gap between Alice’s valid_until and Bob’s valid_from NoBelief (gap)

If the windows overlap — even partially — succession does not apply. Two claims covering the period [2020, 2025) and [2023, ∞) genuinely conflict at the overlap [2023, 2025). The fold returns Contested for any query instant in that overlap. An oracle is needed to resolve which value was correct during the contested period.

Similarly, if one or both claims have valid_time_confidence < 0.7, the engine cannot confidently determine whether the windows overlap, so it falls through to Contested.

Succession is handled at two layers:

Write time (Reconciler C3): When a new claim arrives, the reconciler checks whether it forms a clean succession with the current incumbent. If it does, it classifies as NoConflict → cheap path → CommittedCheap. This prevents a spurious Contested disposition from being written to the ledger.

Read time (TruthEngine C2 fold): When query_memory is called, the fold selects the claim valid at the query instant. This handles:

  • Multi-claim succession chains (A → B → C → …) where the query instant falls in any window.
  • Claims whose succession was detected at write time (both stored as CommittedCheap).
  • Historical queries at different instants returning different beliefs from the same store.

Both layers must work together. The write-time reconciler prevents the ledger disposition from saying Contested when succession was detected; the read-time fold provides correct instant selection.

Combined bi-temporal point queries: valid_at + as_of_tx_time

Section titled “Combined bi-temporal point queries: valid_at + as_of_tx_time”

as_of_tx_time and valid_at are two independent parameters on query_memory (shipped in 0.3.0). as_of_tx_time restricts which ValidityAssertion records are visible — the transaction-time axis (“what did the engine know by this point in its own log?”). valid_at independently selects the claim whose valid-time window contains the given instant — the valid-time axis (“what was true in the real world at this point in time?”).

The two compose: the transaction-time filter is applied first, then valid-time selection narrows the result to a single claim. Supplying both answers the full bi-temporal question “what did the system believe, as of transaction time T, was true in the world at valid-time V?” See Query Patterns and Python (Advanced): Point-in-time valid-time query for worked examples, and the As-Of / Bi-Temporal Correctness Benchmark for a reproducible demonstration of the combined query.

To enable succession, supply a valid-time window with high confidence when ingesting claims:

import mempill
from mempill.types import ProvenanceLabel
engine = mempill.open_in_memory()
# First claim: Alice, CEO from 2020 to 2024-06-01.
resp1 = engine.ingest_claim({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
"value": "Alice",
"provenance": ProvenanceLabel.external_user_asserted(),
"cardinality": "Functional",
"confidence": {"value_confidence": 0.95, "valid_time_confidence": 0.9},
"valid_time": {"start": "2020-01-01T00:00:00Z", "end": "2024-06-01T00:00:00Z", "valid_time_confidence": 0.9},
"criticality": "High",
"derived_from": [],
})
print(resp1["disposition"]) # CommittedCheap
# Second claim: Bob, CEO from 2024-06-01 onward (non-overlapping).
resp2 = engine.ingest_claim({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
"value": "Bob",
"provenance": ProvenanceLabel.external_user_asserted(),
"cardinality": "Functional",
"confidence": {"value_confidence": 0.95, "valid_time_confidence": 0.9},
"valid_time": {"start": "2024-06-01T00:00:00Z", "valid_time_confidence": 0.9}, # open-ended
"criticality": "High",
"derived_from": [],
})
# Non-overlapping + trusted windows = CommittedCheap (not Contested)
print(resp2["disposition"]) # CommittedCheap
# Query at now (after 2024-06-01): Bob is selected.
result = engine.query_memory({
"agent_id": "my-agent",
"subject": "acme:ceo",
"predicate": "held_by",
})
print(result["belief"]["status"]) # Resolved
print(result["belief"]["primary"]["fact"]["value"]) # Bob
  • I1 — Non-destruction: Alice is not deleted when Bob is committed. Both remain in the store.
  • I2 — Bi-temporal by trust: succession is only recognized when valid_time_confidence >= 0.7. Low-confidence windows fall back to Contested.
  • I7 — Contested first-class: overlapping windows still produce Contested. Succession never silently resolves a genuine overlap.
  • I8 — Read-time canonical: the fold computes instant-selection fresh on every query_memory call. The result is deterministic: same claims + same query instant → same belief.
  • I10 — Fixed-history monotonicity: querying the same fixed history at the same instant always returns the same result.