Key Invariants (I1–I11)
Overview
Section titled “Overview”mempill’s correctness is defined by 11 invariants. These are not conventions or guidelines — they are structural guarantees enforced by the Rust type system, the append-only database schema, and engine logic. Violating any invariant is a bug, not a configuration issue.
Each invariant maps to at least one component (C1–C8) and is covered by end-to-end tests. See The Eight Components for the full component reference.
I1 — Non-destruction
Section titled “I1 — Non-destruction”Constraint: Claims write once; supersession and invalidation never delete. History is retained in the append-only store.
Enforcement: The schema has INSERT-only semantics on the claims, validity_assertions, and ledger_entries tables. The Rust API exposes no delete() or UPDATE method on the claim store.
Why it matters: If supersession deleted the incumbent claim, an agent could not audit what it previously believed. Replay — reconstructing historical belief from the stored record — would be impossible. I1 is the foundation that makes the audit trail meaningful. A superseded belief is never gone; it is merely bounded in time.
Failure mode if violated: A claim can be forgotten; history is lost; bi-temporal replay fails. A superseded belief could be mistaken for an active one if its bounding assertion is lost.
I2 — Bi-temporal by trust
Section titled “I2 — Bi-temporal by trust”Constraint: TransactionTime is reliable (engine-stamped, monotone). ValidTime is fallible (host-extracted, confidence-tagged). These are two distinct axes, separated by trust level.
Enforcement: Rust: TransactionTime and ValidTime are separate types; only the engine can produce TransactionTime (the host cannot override it). Schema: two distinct timestamp columns with trust metadata. Logic: when valid_time_confidence ≤ 0.7 (the trust threshold), the engine falls back to transaction-time ordering for belief ranking.
Why it matters: The valid-time of a claim is an extraction from natural language or an agent’s observation — it is inherently uncertain. Treating it as reliable when it isn’t means an uncertain temporal boundary can disguise itself as fact, causing unreliable belief ranking. The confidence tag makes the uncertainty explicit and propagates it into the TimingUncertain status.
Failure mode if violated: A stochastic extraction can disguise an uncertain boundary as a reliable fact. Temporal staleness returns. Ranking becomes unreliable.
I3 — Belief derived, never stored
Section titled “I3 — Belief derived, never stored”Constraint: Belief is recomputed at read time via the canonical valid-time fold. No “current belief” row is authoritative.
Enforcement: Rust: query_memory() returns a BeliefProjection derived by the TruthEngine fold (C2); it never loads a pre-computed “current” row. Every read triggers the fold fresh.
Why it matters: If a “current belief” row existed, it could diverge from what the full history says — silently, without an error. Staleness is not signaled; it silently accumulates. By deriving belief at read time, changing the fold logic is immediately reflected in the next query, and there is no cache consistency problem. Bi-temporal replay (replaying history to reconstruct belief at any past instant) works correctly because the fold is a pure function of stored claims.
Failure mode if violated: Materialized belief becomes stale. Cache consistency bugs are invisible. Replay fails if the cache is the source of truth.
I4 — Provenance immutable, injection-assigned
Section titled “I4 — Provenance immutable, injection-assigned”Constraint: ProvenanceLabel is set at write time (injection) and never rewritten. Model output defaults to ModelDerived. The 3-channel enum is immutable after commit.
Enforcement: Rust: the provenance field on Claim is private with no setter method. Schema: provenance_label is an immutable column. Write path: all ingestion operations (C1) assign provenance; no later operation can change it.
Why it matters: Provenance determines routing through the gate (C7). A claim labeled External(UserAsserted) takes the cheap path; a claim labeled ModelDerived takes the down-weighted path. If provenance could be retroactively rewritten, a model-derived claim could be re-labeled as external to bypass the gate’s safety checks — a backdoor to manufacture ground truth from stochastic output.
Failure mode if violated: A false claim can be retroactively relabeled as “external” to bypass safe routing. Write-path amplification loops become uncontainable.
I5 — Stochastic proposes, never commits
Section titled “I5 — Stochastic proposes, never commits”Constraint: Extractor and oracle ports return proposals; the deterministic core (C3/C7) makes all commit decisions. Cardinality is a proposal (default Unknown), gated like other proposals.
Enforcement: Rust: ExtractorPort::extract() returns a ClaimProposal; it carries no commit flag. Every proposal passes through C3 (reconciler) and C7 (gate) before any write. The engine embeds zero model libraries.
Why it matters: If a stochastic model could write directly to the claim store, it could silently assert a cardinality bound (marking a belief as single-valued when it is set-valued), overturn an external belief without oracle authority, or inflate confidence by re-ingesting its own output. The gate is the firewall that separates “a model said so” from “this is committed truth.”
Failure mode if violated: A model can unilaterally assert cardinality, commit a destructive bound without gating, or overturn external beliefs. Contradiction handling fails; information loss occurs.
I6 — Idempotent append under identity
Section titled “I6 — Idempotent append under identity”Constraint: Identity-equivalent re-entry (recall loop, amplification) corroborates the existing claim (currency refresh only if provenance-independent); never creates a duplicate. Same-extractor multiple statements cannot accrue independent corroboration (the mem0 #4573 defense).
Enforcement: Write logic includes an identity check: (subject, predicate, value, ProvenanceLabel) + external anchor = identity. Corroboration-by-identity returns the same ClaimRef. Currency refresh only occurs if the new entry’s provenance differs from the existing claim’s provenance (provenance-independence check).
Why it matters: An agent that reads its memory, extracts facts from what it read, and re-ingests them would amplify every belief by the number of recall turns. 808 recall turns → 808 copies of the same claim → artificially inflated confidence. I6 collapses all identity-equivalent re-entries to one claim with a single corroboration record. The mem0 #4573 amplification loop cannot happen.
Failure mode if violated: The 808-copy amplification loop recurs. Write-path error multiplies unbounded. Memory becomes an echo chamber.
I7 — Conflict/Contested first-class
Section titled “I7 — Conflict/Contested first-class”Constraint: Unresolved conflicts and contested beliefs are first-class dispositions, surfaced explicitly in BeliefProjection. Never silently returned as the incumbent value.
Enforcement: Rust: BeliefProjection.status includes Contested as an explicit variant; query_memory() always returns the status. Logic: when oracle is absent and an external contradiction arrives, the gate marks Contested — it never defers or silently keeps the incumbent.
Why it matters: A memory system that silently picks a winner when it faces a conflict is manufacturing certainty it does not have. Downstream agents act on the returned value with false confidence. By surfacing Contested explicitly, the agent (and its operator) can see that the memory is in an ambiguous state and decide what to do: provide an oracle, surface the conflict to a user, or wait for more information.
Failure mode if violated: A contradicted belief is silently returned as if resolved. Agent acts on contested state with false confidence.
I8 — Read-time canonical authoritative (arrival-independent)
Section titled “I8 — Read-time canonical authoritative (arrival-independent)”Constraint: The canonical valid-time fold (C2) defines belief; materialized supersession links are a subordinate cache, overridden on disagreement. Belief is arrival-order-independent.
Enforcement: Rust: query_memory() re-folds the subject-line on every read; SupersessionLink cache is advisory only. Logic: on read, the fold is computed fresh; if it disagrees with cached links, the fold wins and the cache is reconciled.
Why it matters: If arrival order determined belief, backfilling — learning a prior claim with an earlier valid-start after the fact — would produce different belief than the “true” temporal ordering. I8 means that a claim ingested late (because the information was received late) is treated as if its valid-time position is what matters, not when it happened to arrive. Temporal history is reconstructed correctly regardless of ingestion order.
Failure mode if violated: Arrival-order-dependent belief violates replay. Backfill scenarios produce different belief than the true ordering. Belief varies between two reads of the same fixed history with no intervening write.
I9 — Atomic commit unit
Section titled “I9 — Atomic commit unit”Constraint: {claim + bounding assertion + ledger entry} for one agent_id commits together or not at all. No torn or partial state is observable. Single-writer-per-agent_id is enforced.
Enforcement: Persistence: begin_atomic(agent_id) / commit() / rollback() scopes all three tables. Rust: PersistencePort::begin_atomic() returns a Txn handle scoped to one agent_id; all appends use that handle. SQLite: synchronous=FULL prevents partial writes on power loss.
Why it matters: If a supersession were committed without its bounding assertion (because the process crashed between writes), the claim would appear live and the superseded belief would be indistinguishable from an active one. If a claim were committed without a ledger entry, the audit trail would have a gap. I9 means that the store is always in a consistent state: either all three records are present, or none of them are.
Failure mode if violated: A supersession commits but its ledger entry is lost (audit trail broken). A claim is visible but its bounding assertion is not (belief inconsistent). Replay fails.
I10 — Fixed-history monotonicity
Section titled “I10 — Fixed-history monotonicity”Constraint: Belief is monotone with respect to a fixed committed history. A new committed claim (including reinstatement, rival, or supersession) may change belief — this is a legitimate append, not a regression. Forbidden: belief regression without a new committed claim.
Enforcement: Rust: query_memory() reads the committed store as-is; no background async re-ranking without a new commit. Logic: if belief moves Resolved→Uncertain between two reads with no intervening write, the store is corrupted — this triggers an error, not a silent inconsistency.
Why it matters: If belief could fluctuate between reads without any write, an agent cannot trust its own recall across turns. Two reads of the same question in the same session could produce different answers for no observable reason. I10 guarantees that belief is stable over a fixed history: the only thing that can change belief is a new committed claim.
Failure mode if violated: Estimators (model outputs) can flip belief between reads without a new commit. Confidence in replay is false.
I11 — Currency decay, never deletes
Section titled “I11 — Currency decay, never deletes”Constraint: Currency decays with age, including set members. Decay surfaces an AgingUnconfirmed marker. Only an explicit negative assertion (validity bound) yields Invalidated. Decay never deletes.
Enforcement: Rust: CurrencySignal includes a decay_since: TransactionTime field; projection (C5) computes decay as a function of (now - decay_since). Logic: aged set members surface an AgedSetMember marker; no background cleanup deletes them. Only explicit negative assertion (validity bound) produces Invalidated.
Why it matters: Facts that were true once but have not been reconfirmed are not the same as facts that are actively false. I11 distinguishes these: an old unconfirmed fact decays gracefully and surfaces an aging signal so the host can decide whether to reconfirm or close it. A fact closed by explicit negative assertion (e.g., an oracle says “this is no longer true”) becomes Invalidated. Automatic deletion without operator decision would lose information that could be relevant for historical queries or audit.
Failure mode if violated: Temporal staleness recurs: an aged fact never signals it is stale; it is silently kept as if current. Set members become immortal until a retraction the user never explicitly states.
How invariants map to components
Section titled “How invariants map to components”| Invariant | Primary enforcement components |
|---|---|
| I1 | Schema (INSERT-only), C4 (Supersession), C8 (AuditLedger) |
| I2 | C1 (Gateway), C2 (TruthEngine fold ordering) |
| I3 | C2 (TruthEngine), application read path |
| I4 | C1 (Gateway), Rust type system |
| I5 | C7 (Gate), ExtractorPort/OraclePort traits |
| I6 | C6 (Firewall/AmplificationGuard) |
| I7 | C7 (Gate), C5 (Projection) |
| I8 | C2 (TruthEngine), application read path |
| I9 | PersistencePort adapters (SQLite WAL + synchronous=FULL, PostgreSQL transactions) |
| I10 | C2 (TruthEngine), application read path |
| I11 | C5 (Projection — currency decay), C4 (Supersession — bounding) |
Next steps
Section titled “Next steps”- Temporal Validity Problem — what problem these invariants solve
- The Adjudication Gate — I5 and I7 in action
- Bi-temporal Claim Store — I1, I2, I3, I8, I9
- Provenance Firewall — I4, I6