Temporal Reasoning and Memory Provenance
Temporal memory retrieval with as-of queries, staleness scoring, and evidence provenance.
A retrieval policy can rank an old claim highly even after a later event invalidates it. Temporal reasoning determines when a claim was valid, while provenance records the episodes and derived claims that support it. Both are required for historical queries, contradiction handling, and source explanations.
The as-of query; temporal intent classification
A lightweight intent classifier extracts the query’s reference time before retrieval. Common temporal intents include:
Explicit absolute time; “what did we decide on March 15?” The reference time is 2026-03-15. The retrieval filters by valid_from ≤ 2026-03-15 ≤ valid_to, drops the recency term entirely (α = 0), and may also filter by transaction_time ≤ as-of-date if the user is asking about historical system belief rather than historical world state.
Explicit relative time; “what did we decide three months ago?” Resolve to absolute (now - 90d) and proceed as above. The trap is locale-dependence: “yesterday” at 23:55 means a different date in UTC than in PST; the classifier should resolve relative times against the user’s locale or fail explicitly. The Test of Time benchmark (Fatemi et al., 2024) measures exactly this kind of relative-time resolution and finds that LLMs lose 23-35% accuracy when shifting from “in 2020” to “4 years ago,” even though the two refer to the same absolute date; the classifier carries non-trivial difficulty and should be measured.
Implicit temporal intent; “what’s the user’s job?” carries no explicit time but reads against a fact that has known turnover. The classifier needs to recognize the fact category (job, manager, address, preferences-known-to-change) and resolve the reference time to “current”; which means the filter is valid_from ≤ now ≤ valid_to, dropping any fact whose valid_to is in the past. Without the classifier, an old “user works at Acme” fact and the current “user works at Beta Corp” fact both retrieve, and the rerank picks whichever has higher cosine similarity to the query.
Before/after queries; “what did we believe before the pivot?” requires identifying the pivot event in the timeline and filtering by valid_from ≤ pivot_time. The pivot itself is typically an episode in the store; the classifier resolves “before the pivot” to “valid_to < pivot_episode.ts” by walking back to find the canonical episode that the user is referencing.
No temporal intent; most queries fall here. The default is “as of now,” and the temporal filter is permissive (valid_from ≤ now ≤ valid_to) but doesn’t override the rerank’s recency weighting. The query proceeds through normal retrieval; the temporal filter just prunes facts whose validity windows have explicitly expired.
The implementation is a tiered classifier: cheap regex catches explicit dates and relative-time phrases; a small-model call (Haiku-class, ~50ms) catches the implicit and before/after cases; if both miss, fall through to “no temporal intent.” Mem0’s temporal-intent flag and Zep’s valid_at parameter both expose the resolved reference time to downstream retrieval, and both report measurable wins on temporal-RAG benchmarks (the ChronoQA and TEMPRAGEVAL datasets are the published yardsticks).
Staleness in retrieval
The retrieval-policies article blended three signals: recency, importance, similarity. Temporal-aware retrieval adds a fourth: staleness, the inverse of “the fact is believed to still be true as of the query’s reference time.”
The signal is binary in the strict version (stale or not) and continuous in the soft version (probability the fact is still current). The strict version is a gate: a stale fact gets score = 0 regardless of other signals. The soft version is a down-weight: a fact whose validity is about to expire (or whose category has high turnover) gets multiplied by a freshness ∈ [0, 1] factor. Both are defensible; the right choice depends on the fact category.
Strict gating for tagged-with-validity facts. Any fact with an explicit valid_to timestamp is gated: if valid_to < query_reference_time, exclude. The knowledge-graph world has this for free; every edge carries the interval. The vector-memory world has to add it as metadata on writes, which most retrofitted systems don’t do, which is why the staleness failure mode is more visible in vector-only stacks than in graph-augmented ones.
Soft down-weighting for category-typed facts without explicit validity. A fact tagged as "job_title" has no valid_to written at ingest time, but the category has a known half-life; average job tenure is roughly 4 years. A "food_preference" fact has a half-life closer to 5-10 years. A "name" fact (the user’s own name) is effectively immortal. The down-weight is exp(-Δt / category_half_life), exactly the same shape as the recency decay but tuned per category instead of globally. The signal is empirically powerful; the temporal-validity literature finds that LLM-based fact-retrieval systems with category-conditional staleness gates outperform uniform-decay systems by 6-15% on temporal benchmarks.
The verification boost. A fact that’s been re-confirmed by the user recently (the last_verified clock) gets a freshness boost; freshness *= 1 + δ · 1[recently_verified]. The signal captures the behavioral component of staleness: a fact the user re-mentioned last week is current regardless of when it was first ingested. The mechanic shows up in MemoryBank’s read-driven salience update (the Ebbinghaus-curve-inspired pattern from the retrieval-policies piece) and applies to staleness for the same reason it applies to importance: reactivation is evidence.
The contradiction penalty. A fact that has been explicitly contradicted by a later episode; the user said “I switched stacks” after the agent previously believed “user uses Postgres”; gets freshness = 0 even if its valid_to is null. The contradiction-detection step is itself an LLM call (pattern-match for “X used to be Y but is now Z” or “actually, X” or “we pivoted on X”); the Zep/Graphiti contradiction-handling pipeline is the cleanest documented example, and the memory conflict and forgetting article works the full ADD/UPDATE/DELETE/NOOP resolver pattern in detail. The mechanic is the agent-memory port of the stamp-the-row-as-deleted-but-keep-it pattern from event-sourced systems: the contradicted fact stays in the store (for audit) but is excluded from retrieval.
The combined scoring formula:
| |
Where staleness_gate is binary (0 or 1) and freshness_factor ∈ [0, 1] captures the soft signals. The multiplicative structure is intentional: a stale fact (gate = 0) is excluded regardless of the rest, while a borderline-stale fact (freshness = 0.3) is down-weighted but still retrievable if the other signals are strong enough.
Provenance; the back-pointer chain
The provenance chain is an inverse index over writes: every higher-order claim carries source_episode_ids, and reverse lookup finds dependent claims. It supports these operations:
Walk-down: claim → sources. Given a returned fact, list the episodes that produced it. The chain is read at answer-rendering time; when the agent quotes “the user is vegetarian,” the harness can append “(based on episodes from March 3, March 17, and April 2)” or expose the source IDs through an API for downstream UI. The shape is exactly the citation pattern every production RAG system already ships, ported to memory: don’t return a fact without its sources.
Walk-up: source → dependents. Given an episode that’s been corrected or invalidated, find the higher-order claims that derived from it. The chain is read at write time when a contradiction lands; the contradiction-detection step marks the new episode as superseding the old, and the propagation pass walks every reflection/summary that cited the old episode and either revalidates or marks-as-stale each one. Without the walk-up, contradictions are content-local; the old episode is stamped invalid, but the reflection built on it is still confidently citable in retrieval. The chain is what makes contradictions systemic.
Walk-graph: claim → claim. A reflection cites episodes; a meta-reflection cites reflections; the chain can be arbitrarily deep. The walk-graph operation is a transitive closure over the back-pointer graph and gets called when the agent is asked “explain why you believe this” or when a debugging pass is reconstructing the reasoning behind a confident-but-wrong answer. The Generative Agents paper does this implicitly through its reflection tree; production frameworks like Letta and Mem0 expose it as an API.
The discipline that separates a useful provenance chain from a heavyweight audit fixture is bounded depth. A reflection chain that grows linearly with session count becomes expensive to walk, expensive to render, and (worse) expensive to invalidate. The mitigation is depth-bounded provenance; each claim points back at most 2-3 layers, with deeper provenance reachable via the sleep-time consolidator but not surfaced by default. The full chain stays in the durable store for audit; the runtime walk is shallow.
Code: Python; as-of retrieval with provenance walk
The smallest interesting build: a retrieval pass that takes a query plus a reference time, filters by bi-temporal validity, computes the four-signal score (recency + importance + similarity + staleness/freshness), and returns each hit with its provenance chain attached. Substrate: Chroma for vectors, a sidecar SQLite table for the validity intervals and provenance edges (because Chroma’s metadata filtering doesn’t support range queries on multiple keys efficiently). Install: pip install chromadb.
| |
The staleness gate excludes facts whose validity interval does not cover the reference time. Business time and transaction time are checked separately for historical-belief queries. Freshness then down-weights aging facts that remain valid. Provenance walks run only for the final top-N results, and supersede closes the old validity interval while linking to the replacement fact.