Valid-Time Succession
The problem succession solves
Section titled “The problem succession solves”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.
The succession rule
Section titled “The succession rule”A claim pair qualifies as a clean temporal succession when all of the following hold:
- Same
(subject, predicate), different value. - Both claims have
valid_time_confidence >= 0.7(the trust threshold — both windows are trusted). - Both claims have bounded windows:
valid_fromis present on the successor;valid_untilis present on the incumbent. - 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 wherevalid_untilis 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.
Read-time instant selection
Section titled “Read-time instant selection”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_timewindow containsq. - Only that claim appears in the belief projection;
has_conflictis 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) |
Overlapping windows are still Contested
Section titled “Overlapping windows are still Contested”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.
Write-time vs. read-time succession
Section titled “Write-time vs. read-time succession”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.
Supplying valid-time in practice
Section titled “Supplying valid-time in practice”To enable succession, supply a valid-time window with high confidence when ingesting claims:
import mempillfrom 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"]) # Resolvedprint(result["belief"]["primary"]["fact"]["value"]) # BobKey invariants honored
Section titled “Key invariants honored”- 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 toContested. - 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_memorycall. 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.
Next steps
Section titled “Next steps”- Claim Store: Bi-temporal — the two time axes and confidence scoring
- Contested and Dispositions — what happens when windows overlap
- The Adjudication Gate — how the reconciler feeds conflict classification to C7
- Key Invariants — I1, I2, I7, I8 in full detail