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 NoConflict (succession). The gate (C7) routes to the cheap path: 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.
Known limitation: as_of_tx_time is not a valid-time as_of
Section titled “Known limitation: as_of_tx_time is not a valid-time as_of”The as_of_tx_time parameter in query_memory restricts which ValidityAssertion records are visible — it does not filter claims by their valid_time window against a specified valid-time instant. The fold always selects the claim valid at now (or the transaction time of the query call).
True bi-temporal point queries combining as_of_tx_time AND as_of_valid_time are a planned future refinement. Today, to query “who was CEO at a past valid-time instant?”, you can use as_of_tx_time to see the store as it was at a past transaction instant, but the fold’s instant-selection uses the transaction time as the valid-time proxy.
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"}, "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"}, # 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