How does memory stay true when life changes?
In this blog, we will learn how a memory system handles facts that change: a move, a new job, a broken habit. We will also see the two clocks every fact needs, and why "git for facts" is the right mental model.
The problem with overwriting
Maya lived in London. Now she lives in Paris. The simplest fix is to find the London row and rewrite it.
| Design | "Where do I live?" | "Where did I use to live?" |
|---|---|---|
| Update in place | Paris | gone |
| Keep both, with time stamps | Paris (the only open row) | London (closed in March) |
Overwriting answers the first question and destroys the second. LongMemEval has a whole category called "knowledge update", and BEAM has "contradiction resolution" and "event ordering". Overwriting loses all three.
Two clocks
Databases solved this decades ago with the word bitemporal. Every fact carries two clocks:
- Valid time: when the fact was true in the world. Maya lived in London from 2025-01 to 2026-03.
- System time: when the system learned it. We were told about London on 2025-01-14 and about Paris on 2026-03-02.
"Maya lives in London."
valid_from 2025-01 valid_to 2026-03 <- valid time
created_at 2025-01-14 superseded_by #7 <- system time / lineage
"Maya lives in Paris." (#7)
valid_from 2026-03 valid_to NULL
created_at 2026-03-02
Ask the store "as of today" and you get Paris. Ask "as of February 2026"
and you get London. Ask "what changed?" and you follow superseded_by.
Zep's Graphiti paper names the four timestamps on every edge: t_valid, t_invalid, t_created, t_expired. HydraDB describes the same thing as a Git-style, append-only temporal graph.
Git for facts
If you have used git, you already understand this. A commit never changes.
A new commit points at the old one. HEAD is the current truth, and
git log is the history.
#3 "lives in London" <---- #7 "lives in Paris" <---- HEAD
valid 2025-01..2026-03 valid 2026-03..
Superseding a fact is a new commit with a parent pointer. Nothing is force-pushed. Every memory mutation is traceable, which is also what an audit wants and what "the user can see and delete everything" needs.
Who decides that a fact is superseded?
The extractor. When it writes "Maya moved from London to Paris in March
2026", it also sees the 10 most similar existing memories, including
"Maya lives in London", and it returns supersedes: [id of the London row].
The store then stamps valid_to on the old row and links the two.
This is one LLM call, the same call that extracts the fact. Nobody deletes anything, and a wrong guess is reversible: reopen the old row.
Provenance: where did this come from?
HydraDB's blog puts provenance next to bitemporality: every assertion should
carry the source utterance, the model that extracted it, and its
confidence. We keep it small: source_ids is the list of session and turn
ids the fact came from. Two reasons:
- Debugging. When a memory is wrong, you want to see the sentence that produced it.
- Safety. Facts extracted from the user's own words are trusted. Facts that came from a web page or a tool output are not. The source field tells them apart, and that is the defense against memory poisoning.
Reading the chain at answer time
When the read path returns "Maya lives in Paris", it also returns the chain behind it, and the prompt says so:
- Maya lives in Paris. (since 2026-03; replaced "Maya lives in London", 2025-01 to 2026-03)
Now the answer model can handle "where do you live", "where did you live before", and "when did you move" from one line. This is the cheapest temporal reasoning you will ever buy.
What about episodic events?
Events do not get superseded; they accumulate. "Vet checkup on March 14"
does not replace "vet checkup on January 3". Both are true, both are dated.
So supersedes is mostly a semantic-memory tool, and event ordering
questions are answered by sorting episodic memories on event_date.
Closing
A fact is not a cell in a spreadsheet. It is a commit with two clocks: when it was true, and when we learned it. Never overwrite a commit. Add a new one, point it at the old one, and let the read path show the current truth with its history one line long.