Carebun Interactive/Memory Memory labCheat sheet

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.

11 min read · interactive

On this page
  1. The problem with overwriting
  2. Two clocks
  3. Supersede: close the old card, do not tear it up
  4. Git for facts
  5. Provenance: where did this card come from?
  6. Who decides that a fact is superseded?
  7. Reading the chain at answer time
  8. What the lab taught us
  9. Try it yourself: time travel in six lines
  10. Common questions
  11. Carry this
  12. 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.

DesignMechanics"Where do I live?""Where did I use to live?"
Update in placeoverwrite the rowParisgone
Temporal invalidationkeep both rows, stamp each with datesParis (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 fieldWhich clock
noneevent_dateValid time: when it happened or became true
created_atvalid_fromTransaction time: the day of the conversation that taught us
updated_atvalid_to, superseded_byTransaction time: a change closes the row and points to the new one
last_accessedlast_accessedNeither: when the row was last retrieved, used for decay
sourcesource_idsNeither: the turns the fact came from
nonecreated_atBookkeeping: 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_to and superseded_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.

  1. Debugging. When a memory is wrong, we want to read the sentence that produced it. Was the extractor wrong, or did the user misspeak?
  2. 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.
  3. 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.

ArgumentClockThe question it answersFields it reads
as_oftransaction timewhat did the assistant believe on that date?valid_from, valid_to
as_of_eventvalid timewhat 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.