Pick any AI memory system you have worked with. Check how it stores facts.
You will almost always find a schema like this:
CREATE TABLE memories (
id INTEGER PRIMARY KEY,
content TEXT,
created_at TIMESTAMP
);
Missing from this schema is the thing that separates a database of facts from a museum of past beliefs: a way to say "this used to be true, and now it is not."
The problem, concretely
An agent stores: "Martin works at Acme Corp" on 2026-01-15.
Six months later, Martin joins a different company. The agent is never told directly, but it picks up the new employer from a recent email and stores: "Martin works at Beta Ltd" on 2026-07-02.
Now the agent has two facts. Both are in the table. Both will be retrieved when asked "where does Martin work?" The vector store does not care about recency. The answer is coin-flip, depending on which phrasing the agent queries with.
This is not a rare edge case. It is the default behaviour of every flat fact store. Titles change, prices change, features change, people change jobs, projects change owners, codebases change conventions. The half-life of a fact in a long-running agent is weeks at best.
What "bi-temporal" means
The term comes from data warehousing (Kimball, 2002). A bi-temporal record tracks two time axes, not one:
- Valid time — when the fact was true in the real world (from → to)
- Transaction time — when we learned about it / stored it
Most systems collapse these into created_at. That is sufficient for an audit log, but it is not sufficient for truth.
What you actually need is more like:
CREATE TABLE memories (
id INTEGER PRIMARY KEY,
content TEXT,
observed_at TIMESTAMP, -- when we observed this
valid_from TIMESTAMP, -- earliest point at which content was true
valid_to TIMESTAMP -- point at which it stopped being true (NULL = still true)
);
Now when the agent queries "where does Martin work?", the query becomes:
SELECT content FROM memories
WHERE content LIKE '%Martin%works%'
AND (valid_to IS NULL OR valid_to > datetime('now'))
ORDER BY observed_at DESC;
The old Acme fact stops being surfaced the moment valid_to is set. No deletion, no overwriting, no lost audit trail — just a one-field change that flips the fact from "current" to "historical."
Invalidation, not deletion
Setting valid_to is different from deleting. You still have the fact. You can still ask "what did we believe on 2026-04-01?" with:
SELECT content FROM memories
WHERE content LIKE '%Martin%works%'
AND observed_at <= '2026-04-01'
AND (valid_to IS NULL OR valid_to > '2026-04-01');
This is not a party trick. It is what you need when an AI decision from last quarter is being audited and you have to show exactly what the agent believed at the time. The alternative — having to explain "we used to think X but the row is gone now" — is a compliance disaster waiting to happen.
The hard part: detecting transitions
Adding the columns is easy. The hard part is knowing when to set valid_to on an old row.
There are two reasonable patterns:
Pattern A — explicit invalidation API
Expose an endpoint:
POST /api/memories/{id}/invalidate
{ "valid_to": "2026-07-02T00:00:00Z", "reason": "role change" }
The agent (or a human) calls this when they learn about a change. Simple, auditable, no ambiguity.
Pattern B — auto-detect on write
When a new fact is written that contradicts an existing one (same entity, same relation, different value), auto-set the old fact's valid_to to the new fact's observed_at.
This is more magical but also more failure-prone. "Contradicts" is a hard semantic judgement. You need a conflict-detection layer that knows (Martin, works_at, Acme) and (Martin, works_at, Beta) are contradictory in a way (Martin, works_at, Acme) and (Martin, lives_in, Berlin) are not.
We recommend starting with Pattern A, adding Pattern B selectively for the few relation-types where auto-detection is cheap (job, role, version, price).
The small-schema-big-impact category
A lot of AI-memory engineering ends up being this: a one-column change that, combined with a query-time filter, dramatically changes behaviour. You do not need graphs. You do not need embeddings with temporal weights. You need valid_to NULL filters and a disciplined way to set them.
This is also one of the handful of things that distinguishes a memory system you can trust under compliance pressure (EU AI Act Art. 12 documentation requirements, ISO 42001 traceability) from one that is "just" a vector store with a wrapper.
Where EON fits
EON stores every memory with observed_at, valid_from, valid_to. Historical queries (as-of) and explicit invalidation are first-class endpoints. The agent writing code never has to think about it — but the auditor has everything they need.
If you are rolling your own, the shortest path to the same behaviour: add the three columns, default-filter active rows in your search, and expose an invalidate-endpoint. Half a day of work, twelve months of saved compliance pain.
More on memory architecture at EON Docs.