Part II · Apprentice
Chapter 11
Time and truth: two clocks
Facts change. Overwriting loses the past. Two clocks on every card, and a pointer from the old card to the new, keep both the present and the history.
On this page
- The problem with overwriting
- Two clocks
- Supersede: close the old card, do not tear it up
- Git for facts
- Provenance: where did this card come from?
- Who decides that a fact is superseded?
- Reading the chain at answer time
- What the lab taught us
- Try it yourself: time travel in six lines
- Common questions
- Carry this
- Check yourself
In this chapter, we will learn how a memory system handles a fact that changes: a move, a new job, a broken habit. We will see why overwriting is a trap and meet the two clocks that every fact needs. Then we will read a memory store the way a programmer reads the history of a project.
The problem with overwriting
Maya lived in London. In March 2026 she moved to Paris. The simplest thing a store can do is find the London row and rewrite it. That is the UPDATE verb from Chapter 10.
It works for one question and fails for another.
| Design | Mechanics | "Where do I live?" | "Where did I use to live?" |
|---|---|---|---|
| Update in place | overwrite the row | Paris | gone |
| Temporal invalidation | keep both rows, stamp each with dates | Paris (the only open row) | London (closed in March) |
Temporal invalidation means marking a fact as no longer current, with a date, and keeping it. "Temporal" means "about time". "Invalidation" means "marking as no longer valid". Nothing is erased. The old fact simply stops being the answer to questions about today.
People ask about the past more often than we expect. "What was that restaurant near my old flat?" "How long did I work at the hospital?" "When did I move?" A store that overwrites cannot answer any of them. The public exams for memory systems know this (Chapter 18 explains them). LongMemEval has a whole question type called knowledge update. BEAM has contradiction resolution and event ordering.
Two clocks
Every fact has two different times, and a good store keeps both. Database people call this bitemporal, which only means "two times".
Valid time is when the fact was true in the world. Maya lived in London from 2024 until March 2026. That is a fact about her life. It would be true even if no assistant existed.
Transaction time is when the system learned it. It is also called system time. The assistant was told about London on 14 January 2025, and about Paris on 2 March 2026. That is a fact about the store.
Why keep both? Because they often differ, and they answer different questions.
2 March 2026 Maya: By the way, I started learning French back in January.
valid time January 2026 when she started (true in the world) transaction time 2 March 2026 when she told us (known to the system)
Ask "was Maya learning French in February?" and the honest answer is yes. Ask "did the assistant know that in February?" and the honest answer is no. If the assistant recommended an English-only phrasebook in February, it did not make a mistake. It did not know yet. Only a store with two clocks can tell those two stories apart.
Supersede: close the old card, do not tear it up
To supersede a fact is to replace it with a newer one while keeping the old one, closed and linked. Here are the two rows after Maya's move, with the fields that do the work.
#3 "Maya lives in London."
event_date 2024-09-01 valid time: when it became true
valid_from 2025-01-14 transaction time: when we learned it
valid_to 2026-03-02 closed: we stopped believing it is current
superseded_by #7 the pointer to its replacement
source_ids [s2025-01-14:t1]
#7 "Maya moved from London to Paris in March 2026."
event_date 2026-03-01
valid_from 2026-03-02
valid_to (empty) open: this is current
superseded_by (empty)
source_ids [s2026-03-02:t1]
A row whose valid_to is empty is open. It is what the assistant believes
today. A row with a date in valid_to is closed. It is history.
The field names can mislead. valid_from and valid_to sound like valid time, but
they hold transaction time: the window in which the system believed the fact. Valid time lives in
event_date. The class notes call the two ends of a fact's life valid_at and
invalid_at.
The class row of Chapter 7 had four fields about time and origin. Here is where each one went.
| Class row (Chapter 7) | hanumemAI field | Which clock |
|---|---|---|
| none | event_date | Valid time: when it happened or became true |
created_at | valid_from | Transaction time: the day of the conversation that taught us |
updated_at | valid_to, superseded_by | Transaction time: a change closes the row and points to the new one |
last_accessed | last_accessed | Neither: when the row was last retrieved, used for decay |
source | source_ids | Neither: the turns the fact came from |
| none | created_at | Bookkeeping: when the row was physically written |
Drag the date across Maya's five addresses and watch which card was true on that day. Then switch between the two designs and compare what each one can still answer.
Git for facts
If you have used git, the tool programmers use to track changes to code, you already understand this design. If you have not, here is all you need.
In git, a saved change is called a commit. A commit is never edited. To change something, you add a new commit that points back at the old one. The newest commit is the current truth. The chain behind it is the history.
#1 Bristol <-- #2 Berlin <-- #3 Manchester <-- #4 London <-- #5 Paris (current) 2021 2022 2023 2024 2026
A memory store built this way gives us three things at once.
- The present. Read only the open rows.
- The past. Follow the pointers backwards.
- An undo. If a row was closed by mistake, open it again by clearing its
valid_toandsuperseded_by. Nothing was lost.
Provenance: where did this card come from?
Provenance is the record of where a memory came from. In hanumemAI it is the
source_ids field: the list of session and turn ids that the fact was extracted from.
It looks like bookkeeping. It earns its place for three reasons.
- Debugging. When a memory is wrong, we want to read the sentence that produced it. Was the extractor wrong, or did the user misspeak?
- Safety. A fact from the user's own mouth can be trusted. A fact copied from a web page cannot. The source tells them apart. Chapter 15 shows what happens without it.
- Measurement. Our benchmarks check whether the right turn of the conversation reached the prompt. That check is free, needs no model, and works only because every row knows its source.
Who decides that a fact is superseded?
The extractor decides, in the same call that writes the new fact. When the secretary
writes "Maya moved from London to Paris in March 2026", she has the 10 most similar existing cards on her
desk. One of them says "Maya lives in London". She returns the new fact with
supersedes: [id of the London row]. The store stamps valid_to on the old row and
links the two.
No second model call is needed. And a wrong guess is cheap to repair, because the old row still exists.
Reading the chain at answer time
When the librarian finds the Paris card, she also walks back along its chain, up to three cards, and brings the London card with it. The old card is clearly marked. The block handed to the model looks like this.
(January 14, 2025) Maya lives in London. [outdated as of March 02, 2026] (March 02, 2026) Maya moved from London to Paris in March 2026.
From these two lines the model can answer "where do you live?", "where did you live before?" and "when did you move?". The closed row travels only as the history of a current hit. It never competes for a place on its own.
What the lab taught us
Keeping history is right. Showing it carelessly is not. Three experiments drew the line.
Try it yourself: time travel in six lines
The library is called hanumemAI in this book. Until the rename ships, the import is hmem.
from hmem import Memory, Config
m = Memory(Config(db_path="maya.sqlite"))
m.add("I live in London and work as a data engineer.", user_id="maya", observed_at="2025-01-14")
m.add("Big news: I just moved from London to Paris for a job at a payments startup.",
user_id="maya", observed_at="2026-03-02")
# today: only open rows compete; the closed London row comes along as history
for r in m.search("where does Maya live", user_id="maya", k=3):
print(r.text, "| valid", r.valid_from[:10], "..", r.valid_to or "now")
# transaction time: what did the assistant BELIEVE on 1 June 2025?
m.search("where does Maya live", user_id="maya", as_of="2025-06-01")
# valid time: what was TRUE on 1 June 2025?
m.search("where does Maya live", user_id="maya", as_of_event="2025-06-01")
# the chain behind one fact, oldest first
paris = next(r for r in m.get_all("maya") if "Paris" in r.text)
for r in m.history(paris.id):
print(r.valid_from[:10], r.text, "->", "closed " + r.valid_to[:10] if r.valid_to else "current")
The two "as of" questions use the two clocks.
| Argument | Clock | The question it answers | Fields it reads |
|---|---|---|---|
as_of | transaction time | what did the assistant believe on that date? | valid_from, valid_to |
as_of_event | valid time | what was true in the world on that date? | event_date of the row and of its successor |
Editing a memory by hand follows the same rule as everything else. update() does not
rewrite the row. It writes a new row and closes the old one, so history() still shows the
original text.
new_id = m.update(paris.id, "Maya lives in Paris, in the 11th arrondissement.", user_id="maya")
# a wrong user_id raises KeyError: nobody edits another person's memory
Common questions
Does keeping every old row not make the store huge?
Less than you would fear. Facts that change are a small share of all facts. In our BEAM runs there were only about 12 superseded rows per conversation. And closed rows are left out of ordinary search, so they cost disk space, not answer quality. Chapter 12 covers what is worth removing for real.
What if Maya never says when something happened?
Then the valid time is unknown and event_date stays empty, or the extractor makes a careful
guess from the conversation date. The transaction time is always known, because the system wrote the row
itself. That is why the block shows the recorded date. A valid-time query falls back on the recorded date
when event_date is empty.
What if the extractor supersedes the wrong fact?
Nothing is lost. The old row is closed, not deleted. Reopening it is a one-line change to the row, and
history() shows exactly what happened and when. hanumemAI has no ready-made call for reopening
yet, so today that line is written by hand against the database. Compare that with a wrong DELETE in
Chapter 10, which cannot be undone.
If the user deletes a memory, does it stay in the history?
A user's delete is a different thing from a supersede. When a person asks for something to be removed,
it must really be removed. History is for facts that changed, not for facts the user wants gone. Be careful
here: in hanumemAI today only delete_user() erases rows. delete() on one memory
closes the row, so it leaves ordinary search but stays on disk.
Chapter 12 and
Chapter 15 cover erasure.
Is this a graph database?
No. It is one ordinary table with one pointer column, superseded_by. Systems such as Zep's
Graphiti and HydraDB put the same two clocks on the edges of a full graph.
Chapter 19 compares them. For chains like Maya's addresses, a pointer
is enough.
Which clock should I use for "what did I tell you last week?"
Transaction time. The question is about when something was said, so it is about what the system learned and when. "Where was I living last summer?" is about the world, so it uses valid time.
Carry this
- Overwriting answers "where do I live?" and destroys "where did I use to live?".
- Two clocks: valid time is when it was true in the world, transaction time is when the system learned it.
- Supersede means add a new row, stamp the old one closed, and point it at its replacement. Never tear up a card.
- Every row carries its sources, for debugging, for safety and for measurement.
- Closed rows travel only as the history of a current fact. Facts are superseded. Events pile up.
Check yourself
1. On 10 May, Maya says "I changed jobs three weeks ago." Give the valid time and the transaction time of the new-job fact.
Answer
valid time 10 May - 21 days = 19 April when the job changed transaction time 10 May when the system learned it
2. Maya's chain is Bristol (2021), Berlin (2022), Manchester (2023), London (2024), Paris (2026). How many rows are open, how many are closed, and what does an update-in-place store hold?
Answer
One open row (Paris) and four closed rows, five in total. An update-in-place store holds one row, Paris, and cannot name any of the four earlier cities.
3. "I live in London and I'm vegetarian" was stored as one fact. Maya moves to Paris. What goes wrong, and which rule prevents it?
Answer
The Paris fact supersedes the whole row, so the vegetarian part is closed together with the address, and the assistant may stop treating Maya as vegetarian. The rule is one attribute per fact, so that a change to one attribute can never close another.