Bi-temporal validity
Bi-temporal Validity
Bi-temporal validity is the time model underneath evidence_refs and locked_facts. It's the foundation of Mnemix's audit guarantee: facts are never deleted or silently overwritten — they are bounded in time. This lets Mnemix answer two different questions precisely.
Forgetting is failure. A contradiction bounds the old fact; it never erases it.
Two timelines, not one
Every governed fact tracks two independent timelines, stored as range columns (tstzrange, GiST-indexed for fast as-of queries):
| Timeline | Column | Answers |
| :--- | :--- | :--- |
| Valid time | valid_range | When was this true in the real world? |
| Transaction time | tx_range | When did Mnemix believe / record it? |
On evidence_refs, the ranges are generated from two scalar timestamps: event_time (when the event happened — caller-supplied) and ingestion_time (when Mnemix processed it). Keeping the timelines separate is what makes the model bi-temporal: a fact can be valid in the real world before the system ever recorded it (you backfill an opening balance dated August 1 on August 15), and a fact can be recorded but already bounded (you log that a value was true last quarter).
Scope (locked decision D3): bi-temporality lives on
evidence_refsandlocked_factsonly. Memory objects — the embedded memories your agent recalls — are not bi-temporal; they record ingestion time as information, nothing more. As-of reconstruction answers from the two governed tables.
The two questions it answers
- What is true right now?
SELECT fact_value FROM locked_facts WHERE entity_id = $entity AND fact_key = $key AND valid_range @> now(); - What did the agent believe was true at 2:00 PM last Tuesday?
SELECT fact_value FROM locked_facts WHERE entity_id = $entity AND fact_key = $key AND valid_range @> $t::timestamptz AND tx_range @> $t::timestamptz;
That second query is the heart of the audit guarantee. Given any past moment, Mnemix reconstructs the exact set of facts that were active then — which is exactly what re-running a gate verdict requires.
Supersession instead of update
When a fact changes, the substrate runs a supersession transaction (via the evolve_fact procedure) rather than an UPDATE:
- Bound the old fact — close its
valid_rangeat the supersession moment. - Insert the new fact — its
valid_rangeopens where the old one closed. - Link them — the new row's
supersedes_ref_idpoints at the old row.
jest ┃━━━━━━━━━━━━━━━━━━━━┫ (valid_range closed) ◀──┐
2025-01-01 2026-05-29 │
vitest ┣━━━━━━━━━━━━━━━━━━▶ active supersedes_ref_id ───┘
The old row stays queryable forever. Nothing is lost.
Why this beats a vector store
A vector database stores the latest text and retrieves what's similar. It has no notion of "what was true before this was overwritten." Mnemix retrieves provenance-backed truth as of a point in time. When an enterprise asks "why did the agent approve this $5,000 refund three weeks ago?", Mnemix replays the exact evidence ref and locked fact that were active at that millisecond — not today's values.
Evidence is bi-temporal too
The same range pair lives on evidence_refs. Evidence can expire (a quote valid for 30 days) or be superseded (a corrected statement) without deleting the record that an agent once relied on it.
Out of scope (v1)
- Time-bounded locked facts authored as "valid until DATE" — a post-launch ticket. Today, bounding happens via supersession, not a pre-declared expiry.
See also
- Locked Facts — the supersession chain in depth.
- Evidence Refs — the other primitive carrying this time model.
- The Validation Gate — why historical reconstruction makes verdicts rerunnable.