Korely

Blog Agent memory · Temporal facts

Bi-temporal memory for AI agents: what was true, and when

Bi-temporal memory gives every fact two clocks: when it was true and when your agent learned it. How it works and what it costs, with code.

On this page
Maria's plan, in 55 seconds: why a memory that keeps both facts still answers wrong, and what two clocks change.

The problem

Your agent quotes a fact that stopped being true

Your support agent tells a customer they are on the Pro plan. They downgraded three weeks ago. The memory layer had both facts, "Maria is on Pro" and "Maria moved to Free", with nearly identical embedding scores, and the retriever picked the stale one.

This is the most common way agent memory fails in production, and better retrieval does not fix it: the store holds two facts about Maria and has no way to say which one is still true.

Bi-temporal memory fixes this by giving every fact two clocks. This post explains what that means, how we built it in Korely, and what it costs.

Definition

What is bi-temporal memory?

Bi-temporal memory stores each fact with two time axes: valid time, when the fact was true in the world, and transaction time, when the system learned it. A new fact that contradicts an old one closes the old fact's validity window instead of deleting it, so the agent reads current truth by default and can still ask what was true, or what it knew, on any past date.

The idea is older than LLMs. Bi-temporal tables have existed in databases for decades (SQL:2011 added system-versioned and application-time periods). What is new is applying it to facts that a model extracts from conversation, where contradictions arrive as free text instead of as an UPDATE statement.

Two clocks on every fact: valid time says the customer lived in Turin from 1 September, known time says the store learned it on 5 October, so on 20 September as_of answers Turin and known_as_of answers Milan

The options

Three ways to handle a fact that changes

When "the EU server costs 40 euro" is followed a month later by "the EU server now costs 50 euro per month", a memory layer can do one of three things.

ApproachWhat the store holds after the changeWhat breaks
Keep everythingBoth prices, as two similar memoriesThe agent quotes whichever one retrieval ranks first. Half the time it is the old one.
Overwrite or deleteOnly the new priceThe agent can never answer "what was it before?" or "when did it change?". The audit trail is gone.
Bi-temporalBoth prices; the old one carries the moment it stopped being true and a pointer to the fact that replaced itNothing is lost. Default reads see only the current price; history is one parameter away.

The second row is not hypothetical. One published memory system describes a DELETE operation "for removal of memories contradicted by new information" in its base pipeline, while its graph variant marks conflicting relationships "as invalid rather than physically removing them" (Chhikara et al., 2025, arXiv:2504.19413, sections 2.1 and 2.2).

We are also not the only ones doing the third row. The temporal knowledge graph of Rasmussen et al. "implements a bi-temporal model" with four timestamps on each edge and invalidates contradicted edges by setting their end to the start of the new one (Rasmussen et al., 2025, arXiv:2501.13956, sections 2.1 and 2.2.3). If you are choosing a memory layer, bi-temporality is the property to look for, whoever provides it.

How it works

How Korely stores a fact, and how a contradiction closes it

Korely extracts typed facts from what your agent writes: a subject, a predicate and an object, with the predicate normalized into one of 10 families (preferences, people, places, work, ownership, health, financial, events, identity, other). Each fact carries:

FieldMeaning
valid_fromWhen the fact became true: the timestamp sent with the write; else a date the text states with its year ("moved in May 2026"); otherwise the moment the memory arrived.
invalid_atWhen it stopped being true. null while the fact is open.
invalidated_byThe fact that closed it.
created_atWhen the store learned the fact.
closed_known_atWhen the store learned the fact had ended.

There is no status column. A fact is current when valid_from is not in the future and invalid_at is null or still ahead. The two timestamps carry the state.

Closing a fact is a write-time decision, in two stages. The first stage is cheap and deliberately over-matches: it looks for open facts that could be about the same thing: the same subject and relation (costs 40, costs 50), facts about the same two entities under another relation (uses Jira, stopped using Jira), and the nearest facts by meaning.

The second stage shows a model the new fact with its candidates in one call and asks which of them can no longer be true. "Marco likes tea" leaves "Marco likes coffee" true: both can hold. "The EU server costs 50 euro per month" means "the EU server costs 40" is no longer true. Only a confirmed conflict closes the old fact.

Because all of this happens when the memory is written, reading is a database lookup with no language-model call. Extraction runs asynchronously and settles within a few seconds of the write, so a read issued immediately after a write may not see the new facts yet.

Reading the past

Reading history with as_of and known_as_of

as_of answers what was true on a date, as the store knows it now. known_as_of answers what the store knew on a date. They differ when a change is reported late, which in real conversations is most of the time.

The agent already knows that a customer lives in Milan. Send that first memory with timestamp set to when it was true (for example timestamp="2026-08-01"), because a memory without a timestamp is valid from the day it is written, and a later report dated earlier does not close it. On 5 October the agent learns that the customer moved to Turin on 1 September 2026:

from korely_memory import Korely

korely = Korely()  # reads KORELY_API_KEY

# What was true on 20 September, as we know it today
korely.get_facts(user_id="customer-4812", as_of="2026-09-20")        # lives_in -> Turin

# What we knew on 20 September
korely.get_facts(user_id="customer-4812", known_as_of="2026-09-20")  # lives_in -> Milan

The first call is what you want when the agent acts today. The second is what you want when someone asks why the agent sent a parcel to Milan on 20 September: on that day, Milan was the right answer given what it knew. The second call returns Milan only if the Milan memory was really written before 20 September. If you replay or backfill the memories later, it returns an empty list for that date, because the store learned the facts on the day of the write.

A full history, with every closed fact and what closed it, is one flag:

history = korely.get_facts(user_id="customer-4812", include_invalidated=True)
# most recently confirmed first; closed facts carry invalid_at and invalidated_by

known_as_of depends on closed_known_at, which Korely has recorded since 11 October 2026. For facts closed before that date the value is estimated from what was stored, so read older history as approximate.

Cost and next steps

What it costs, and how to try it

Closing facts is paid for at write time: an extraction call, model calls that place each relation the store has not seen before among the ones it knows and decide whether it holds one value at a time, and a judge call for the new facts that have candidates. A write takes a few language-model calls, and longer memories take more, because the engine reads them in windows of up to 6,000 characters, one extraction call each. Reads cost nothing in language-model calls, because they are lookups.

The trade-off is latency on the write path. If your agent writes a fact and reads it back in the same second, design for the few seconds of asynchronous extraction.

The free Hobby plan includes 250 writes a month (one per memory up to 6,000 characters), stored in the EU. Add --processing-region eu if the memories must also be read by a model in the EU. Install the SDK, write two contradicting facts, and read them back with include_invalidated=True:

pip install korely-memory
korely init --agent --agent-caller your-name --processing-region eu --json

The temporal model is documented in full in Temporal facts, and every read parameter is in the API reference.

FAQ

Questions about bi-temporal memory

What is the difference between bi-temporal memory and a vector store?

A vector store returns the passages most similar to a query, with no notion of which one is still true. Bi-temporal memory stores facts with validity windows, so the store itself knows that the Pro plan ended when the Free plan began.

Does Korely delete facts that are no longer true?

No. A contradicted fact gets an invalid_at and a pointer to the fact that replaced it, and stays in the store. Deleting is a separate, explicit operation, for example when a user asks to be forgotten.

Is Korely the only bi-temporal agent memory?

No. The temporal knowledge graph of Rasmussen et al. (2025, arXiv:2501.13956) tracks four timestamps per edge and invalidates contradicted edges. Korely exposes the same idea through typed facts and two read parameters, as_of and known_as_of, on the REST API, the SDKs and the MCP server.

Can I ask what my agent knew when it made a decision?

Yes, with known_as_of. It returns the facts the store had learned by that moment, each with the end it knew about then.

Back to top ↑