Bi-Temporal Claim Store
Overview
Section titled “Overview”mempill’s persistence layer is an append-only, bi-temporal claim store. Every claim is written once and never mutated. Supersession is recorded as a new bounded assertion that links back to the original — the history is never destroyed.
This design is not a coincidence of implementation. It is the only data model that can satisfy all of I1 through I4 simultaneously: you cannot have both non-destructive history (I1) and read-time-canonical belief (I3) if you allow in-place mutation.
The two time axes
Section titled “The two time axes”Every claim carries two independent temporal dimensions:
| Axis | Stamped by | Reliability | Question answered |
|---|---|---|---|
Transaction-time (tx_time) |
Engine | Reliable, monotone, non-negotiable | When did the engine record this claim? |
Valid-time (valid_time) |
Caller | Fallible, confidence-tagged, optional | When was this claim true in the real world? |
Transaction-time is assigned by the engine at write time and cannot be overridden by the caller. Valid-time is supplied by the caller as an optional bounded window [valid_from, valid_until) along with a valid_time_confidence score (0–1). When valid-time is omitted, the engine defaults the confidence to 0.0 and falls back to transaction-time ordering for belief ranking.
The two axes are kept separate because they are produced by different processes with different reliability. Conflating them — treating “when I wrote this” as “when this was true” — is invariant I2’s error case.
Core record types
Section titled “Core record types”An immutable triple (subject, predicate, value) with additional fields:
| Field | Type | Notes |
|---|---|---|
agent_id |
string | The agent whose memory this claim belongs to |
subject |
string | The entity the claim is about (e.g., "user", "project:123") |
predicate |
string | The property being asserted (e.g., "city", "status") |
value |
JSON | The claimed value |
provenance |
ProvenanceLabel |
Which of the three channels wrote this — immutable |
confidence |
{value_confidence, valid_time_confidence} |
Two independent quality scores |
cardinality |
Functional | SetValued | Unknown |
Single-valued or multi-valued belief |
criticality |
Low | Medium | High | Critical |
How important is this claim for ranking |
valid_time |
optional {start, end} |
When the claim was true in the real world |
derived_from |
list of ClaimRef |
Provenance chain links for derivation-depth tracking |
Claims are INSERT-only. There is no UPDATE or DELETE method on the claim store. The Rust type system enforces this.
Validity assertion
Section titled “Validity assertion”A separate immutable record that closes the valid-time window of an existing claim. Written by the Supersession component (C4) when a claim is superseded. The original claim remains in the store; only its temporal window is bounded by this assertion.
There are two assertion types:
Bound— closes an open-ended claim at a specified transaction time. Written by oracle-driven supersession or explicit invalidation.Reopen— reopens a bounded claim. Written by theReinstatedpath when a previously closed claim is revalidated.
Ledger entry
Section titled “Ledger entry”An immutable audit record created for every disposition outcome. The AuditLedger (C8) maintains this table. Every ingest_claim call produces at least one ledger entry. Ledger entries are queryable via query_audit by agent_id, claim_ref, and transaction-time range.
Belief is derived, never stored
Section titled “Belief is derived, never stored”Invariant I3 is the most important architectural consequence of this design: there is no “current belief” row. Belief is recomputed on every read by the TruthEngine (C2) performing a canonical valid-time fold over the full claim and assertion history.
The fold proceeds as follows:
- Load all claims for
(agent_id, subject, predicate). - Apply liveness filter: use validity assertions to determine which claims are live as of the query instant (controlled by
as_of_tx_time). - If all live functional claims have non-overlapping, trusted valid-time windows, select the one whose window contains the query instant (succession resolution — see Valid-Time Succession).
- If multiple live functional claims remain and their windows overlap (or lack trusted valid-time), compute
has_conflict = true. - Rank by: valid-time-covers-now → tx-recency (provenance-independent) → criticality.
- Return a
BeliefProjectionwith statusResolved,TimingUncertain,Contested, orNoBelief.
TimingUncertain vs. Resolved
Section titled “TimingUncertain vs. Resolved”These are the two non-conflict statuses a query can return:
Resolved— the fold found exactly one live claim whosevalid_timewindow contains the query instant withvalid_time_confidence >= 0.7. The engine has high confidence in both the value and when it applied.TimingUncertain— the fold found a single live claim, butvalid_time_confidenceis below the threshold (or no valid-time was supplied). The value is the best available answer, but the engine is not confident about when exactly it applies.
A claim with TimingUncertain is not wrong — it is the honest signal for “we know what was asserted, but we are not sure when it was true.” This is why the quickstart shows TimingUncertain for a claim submitted without a valid-time window: no valid-time was given, so the confidence is 0.0.
Key invariants
Section titled “Key invariants”- I1 — Non-destruction: writes are INSERT-only. Supersession never deletes. History is retained forever.
- I2 — Bi-temporal by trust: transaction-time is engine-stamped; valid-time is caller-supplied.
- I3 — Belief derived, never stored: recomputed via canonical fold at read time.
- I4 — Provenance immutable: set at injection time; no operation can rewrite it.
- I8 — Read-time canonical: the fold is authoritative; materialized links are a subordinate cache.
- I9 — Atomic commit unit:
{claim + bounding assertion + ledger entry}commits as one indivisible unit scoped to oneagent_id.
Bi-temporal queries
Section titled “Bi-temporal queries”The primary read operation is query_memory:
result = engine.query_memory({ "agent_id": "my-agent", "subject": "user", "predicate": "city", # Optional: as_of_tx_time restricts which ValidityAssertions are visible. # It does NOT filter claims by their valid_time window. "as_of_tx_time": "2025-01-01T00:00:00Z",})The as_of_tx_time parameter filters which validity assertions are visible at the query instant. It implements the transaction-time axis of the bi-temporal read: “what did the engine know, as of this transaction instant?” The valid-time axis is consulted separately for succession resolution and conflict detection.
Next steps
Section titled “Next steps”- Temporal Validity Problem — why this model exists
- Key Invariants — the full invariant set
- Valid-Time Succession — how clean temporal chains fold
- Contested and Dispositions — the 12-state outcome model