Aurra Docs

Bi-temporal Memory

Facts have lifespans, not just truth values. How Aurra tracks when a fact was true and when you learned it.

Most memory systems store facts as true or false. Aurra stores them as true during some period. This is the core abstraction that makes everything else in Aurra - supersession, citations, audit trails - work.

Two time axes

Aurra tracks two separate times for every memory:

TimeQuestion it answersField
Valid time (fact time)When was this fact true in the real world?valid_from, valid_to
Transaction time (system time)When did Aurra believe it was current?transaction_time_start, transaction_time_end

These are not the same. You might record today (transaction_time_start = 2026-05-05) that Alice started at Acme Corp last year (valid_from = 2025-03-15). A naive system that only stores active facts can't represent this difference.

created_at is the moment a row was inserted into the database — it's similar to transaction_time_start for newly-created rows, but it doesn't track when the system stops believing a fact (transaction_time_end). Use the transaction_time_* fields when you need to ask 'what did the system know on date X?' — see Transaction-time queries below.

"Bi-temporal" is a term borrowed from database research* (Snodgrass, 1999). It's how enterprise systems like SAP and Oracle handle financial history and regulatory compliance. Aurra applies the same idea to agent memory.

Why this matters

Naive memory systems (think dict, key-value stores, or mem0's default behavior) do this when you update a fact:

10/1/2026: "Alice works at Acme Corp"  <-- stored
3/1/2026:  "Alice works at Globex"    <-- update (overwrites)

The previous fact is gone. You can no longer answer questions like:

  • What did the agent know about Alice on Feb 15?
  • When did Alice change jobs?
  • Did the agent recommend the right person when it sent that email back in January?

In regulated industries (healthcare, finance, legal, HR), this is not an edge case - it's a requirement. An auditor needs to confirm what the system thought was true at the moment a decision was made.

Aurra preserves both times so you can always reconstruct state.

How it works in practice

Every memory row has this shape in the database:

id                     : UUID
decision               : "Alice works at Acme Corp"
valid_from             : 2025-03-15T00:00:00Z      <-- when the fact became true (Level 1)
valid_to               : null                      <-- null = still current (Level 1)
superseded_by          : null                      <-- null = not replaced (Level 1)
transaction_time_start : 2026-05-05T23:09:59Z      <-- when system started believing (Level 3)
transaction_time_end   : null                      <-- null = system still believes (Level 3)
created_at             : 2026-05-05T23:09:59Z      <-- when the row was inserted

When you call POST /memories/{memory_id}/supersede to replace Alice's job:

[old row]  decision:  "Alice works at Acme Corp"
           valid_from: 2025-03-15  <-- unchanged
           valid_to:   2026-05-05  <-- NEW: when it stopped being true
           superseded_by: UUID of new row

[new row]  decision:  "Alice works at Globex"
           valid_from: 2026-05-05  <-- when the new fact became true
           valid_to:   null
           superseded_by: null

Everything stays. The old row is only modified to record the moment of supersession:

  • valid_to — when the fact stopped being true in the world (Level 1)
  • superseded_by — pointer to the new replacement (Level 1)
  • transaction_time_end — when the system stopped believing it was current (Level 3, F4)

The other columns (decision, valid_from, transaction_time_start, created_at) are never touched.

Point-in-time queries

Because both times are preserved, you can query Aurra as it would have answered at any point in the past.

curl "https://api.aurra.us/agent/memories?as_of=2026-03-01T00:00:00Z" \
  -H "Authorization: Bearer $AURRA_API_KEY"

This returns every memory that was current on March 1, 2026 (where valid_from <= as_of < valid_to or valid_to is null).

Use cases:

  • Replay agent decisions. If your agent recommended Alice for a role on February 15, replay that decision with the memories it had at the time.
  • Audit responses. "What did the system know when it processed claim X/T$ms#123?" is a single query.
  • Debug drift. Compare queries at two timestamps to see exactly which facts changed.

The temporal object

Every memory response includes a temporal object for quick boolean checks:

"temporal": {
  "valid_from": "2026-05-05T23:09:59.008804+00:00",
  "valid_to": null,
  "superseded_by": null,
  "is_current": true,
  "is_expired": false,
  "is_superseded": false,
  "transaction_time_start": "2026-05-05T23:09:59.008804+00:00",
  "transaction_time_end": null,
  "is_known_current": true
}
FieldMeaning
is_currenttrue if this row is the active truth (not expired, not superseded).
is_expiredtrue if valid_to is set without a superseded_by (explicit expiry, no replacement).
is_supersededtrue if superseded_by points to a newer row.
is_known_currenttrue if transaction_time_end is null — the system currently believes this fact (Level 3).

is_current and is_known_current answer different questions:

  • is_current = "Is this fact still TRUE IN THE WORLD?" (Level 1)
  • is_known_current = "Does the SYSTEM still believe this is true?" (Level 3)

For most reads they match. They diverge during the gap between something stopping being true and the system being told. Exactly one of is_current, is_expired, is_superseded is true at any point.

Transaction-time queries

Aurra supports time travel along the transaction time axis too — answering "what did the system believe at point X?" rather than "what was true at point X?"

curl "https://api.aurra.us/agent/memories?transaction_as_of=2026-04-15T00:00:00Z" \\
  -H "Authorization: Bearer $AURRA_API_KEY"

This returns every memory that the system believed was current on April 15, 2026 — even if those facts have since been superseded.

Combine as_of and transaction_as_of for full bi-temporal queries:

curl "https://api.aurra.us/agent/memories?as_of=2026-03-01T00:00:00Z&transaction_as_of=2026-04-15T00:00:00Z" \\
  -H "Authorization: Bearer $AURRA_API_KEY"

This answers: "As of April 15 (transaction time), what did we think was true on March 1 (valid time)?" — the canonical audit query.

See the Transaction-time page for the full Level 3 surface, including SDK examples, the 2x2 of bi-temporal queries, and use cases.

Mental model

Think of a memory as a lease on a fact. When you add it, the lease starts (valid_from). It runs indefinitely (valid_to: null) until either:

  • You explicitly expire it (supersede with valid_to).
  • A new fact replaces it (supersede with superseded_by).
  • Level 2 auto-supersession detects a replacement automatically.

Once a lease ends, the row still exists in the database and is reachable via:

But it's excluded from the default read path (query and unfiltered list).

When to reach for this

The bi-temporal design starts paying off the first time one of these happens:

  • A customer or auditor asks "why did your agent do X on Y date?"
  • A regulator requests a reconstruction of system state.
  • A bug ships into prod and you need to know which users had stale context when.
  • A team mate claims "if it ever knew X" - you can prove it (or prove they're wrong).

If you're building a personal chatbot, you likely don't need point-in-time queries. If you're building an agent that touches regulated data, financial decisions, customer communications, or anything that could end up in court: you need this.

Next steps

On this page