Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content
Memory lifecycle

Bi-temporal memory

Separate when something happened from when the agent learned it — and supersede beliefs without deleting the history they came from.

Every entry in Mubit carries two independent times: ingestion time (when the store learned it) and occurrence time (when the event actually happened). Most memory systems conflate the two, which makes "what did we believe in April?" and "what happened in April?" the same — usually wrong — question. Use this pattern when your agents ingest events after the fact, when beliefs change over time, or when any agent needs to reconstruct a past state instead of just the current one.

How it works

Two timestamps per entry. Ingestion time is stamped automatically. Occurrence time you set explicitly on the write; if you don't, the runtime falls back to extracting it from metadata keys (occurrence_time, event_time, session_date) before finally treating ingestion time as the event time.

Time-bounded queries. min_timestamp / max_timestamp (inclusive, Unix seconds) restrict evidence to entries whose occurrence time — falling back to ingestion time — lands inside the window. Temporal bounds activate a dedicated temporal retrieval branch, so history is a first-class lane, not a post-filter.

Supersession instead of deletion. When a belief changes — a new fact contradicts a stored one — the old entry is not overwritten. It is marked is_stale: true with superseded_by pointing at its replacement (supersessions written by fact reconciliation also stamp invalid_at, closing the old belief's validity interval):

  • Current-state answers exclude it. Synthesis works from the live belief only.
  • Historical and time-bounded queries can still see it. The past stays queryable.

This is what makes the two-timestamp model useful: the store distinguishes "no longer true" from "never happened". How stale entries surface (ranking penalty, superseded_by chains, knowledge_confidence) is covered in Trust, confidence, and staleness.

Walkthrough

In March, Ari stored "Acme Corp is on the Starter plan". On May 3, Acme upgrades. Ari writes the new plan fact with the upgrade's occurrence time; the Starter fact is superseded, not erased.

# May 3: Acme upgrades. Record WHEN it happened, not just when we heard.
# From SDK v0.12.0: occurrence_time is a typed remember() kwarg —
#   client.remember(..., occurrence_time=1777766400)
# On earlier versions, pass it as a metadata key; the server extracts it.
client.remember(
    session_id="support:acme",
    content="Acme Corp upgraded to the Scale plan.",
    intent="fact",
    metadata={"occurrence_time": 1777766400},  # May 3, 2026 UTC
)
 
# Current state: the Scale fact answers; the Starter fact comes back
# is_stale with superseded_by pointing at its replacement.
now = client.recall(session_id="support:acme", query="What plan is Acme on?")
print(now["final_answer"])  # ... Scale plan ...
 
# April state, for Sana's monthly digest: bound the query to occurrence
# time. From SDK v0.12.0, recall() forwards min_timestamp / max_timestamp
# directly; on earlier versions use the low-level query passthrough:
april = client.advanced.query({
    "run_id": "support:acme",
    "query": "What plan was Acme on?",
    "min_timestamp": 1775001600,   # Apr 1, 2026
    "max_timestamp": 1777593599,   # Apr 30, 2026
})
for e in april["evidence"]:
    print(e["content"], "[stale]" if e.get("is_stale") else "")
# The Starter fact is in-window and returned — flagged stale, but queryable.
ℹ️Note

From SDK v0.12.0 the typed helpers forward all three fields natively in every language — remember(..., occurrence_time=...) and recall(..., min_timestamp=..., max_timestamp=...) in Python and JS, matching what Rust's RememberOptions/RecallOptions already carried. On 0.11.0 and earlier the Python/JS helpers do not forward all of them, which is why the tabs above route through the occurrence_time metadata key and the advanced passthrough — both routes remain valid on every version (occurrence_time is a wire-level ingest-item field; the temporal bounds are wire-level query fields). See SDK methods → Temporal queries for the per-language matrix.

When not to use it

  • Events ingested as they happen. If ingestion time ≈ occurrence time (a live agent writing what it just did), skip explicit occurrence_time — the fallback is already correct. Reserve it for backfills, imported history, and user reports about the past.
  • You want the old value gone, not superseded. Supersession is deliberate retention. For actual removal (a compliance delete, a poisoned entry), use explicit deletion — forget() — not a superseding write; see Utility-weighted forgetting for how deletion differs from archival.
  • The "change" is really a refinement. If the new write clarifies rather than contradicts ("Scale plan, billed annually"), write-time fact reconciliation resolves it as an UPDATE instead of a supersession — see Write-time reconciliation.

Related

ℹ️Note

Configuration. Bi-temporal supersession is an instance feature, on by default on hosted instances (MUBIT_CL_BITEMPORAL=0 disables it — beliefs then coexist without staleness marking). Occurrence-time storage and time-bounded queries are always available and need no configuration.