Temporal Query
Point-in-time and version-chain lookups over the bitemporal columns
(valid_from, valid_to, superseded_by) that mem_003_temporal added
to memories. Ordinary search cannot answer these questions: search,
list, stats and single-id lookups all filter valid_to IS NULL, so
they only ever see the current state.
What the temporal columns actually mean
Read this before phrasing an answer — getting the axis wrong produces
confident but false claims.
valid_from / valid_to track when the store recorded a version,
not when the fact was true in the outside world. as_of=T answers
"what did this memory store hold at T", not "what was true at T".
If the user recorded a 2024 decision yesterday, it enters the timeline
yesterday.
update does not edit in place. It closes the old row
(valid_to = <update time>, superseded_by = <new id>) and inserts a
new row with a new id and valid_from = <update time>. The id in
the user's notes from last month no longer resolves through search.
delete also closes the row (valid_to set) but leaves
superseded_by = NULL. That is the discriminator: a closed row with a
forward pointer was replaced; without one it was retracted.
- Rows created by
add / capture carry valid_from = NULL; queries
fall back to created_at via COALESCE. A null valid_from in a
result means "original version", not a data defect.
commit_sha is present in the schema but is not populated by the
current write paths. Do not present it as provenance.
Steps
Resolve the temporal anchor from the user's wording into a single
UTC ISO timestamp: "last month" -> first day of that month,
"before the migration" -> the timestamp of the migration memory,
"at the time we chose X" -> the created_at of the decision memory.
State the resolved timestamp in the answer so the user can correct it.
Take the point-in-time snapshot:
memory(action="as_of", as_of="2026-06-01T00:00:00+00:00", limit=50)
Returns rows satisfying
valid_from <= as_of AND (valid_to IS NULL OR valid_to > as_of),
newest first. as_of accepts no query filter, so raise limit
(max 100) and filter by topic yourself over the returned content.
Omitting as_of returns the current state instead.
Trace the version chain for any row that matters. Each closed row
carries valid_to (when the belief changed) and superseded_by (the
id that replaced it). Follow the pointer forward by snapshotting just
after that timestamp and matching the id:
memory(action="as_of", as_of="<valid_to of the old row>", limit=50)
Repeat until you reach a row with valid_to = null — that is the
surviving belief.
Use the entity timeline when the knowledge graph is populated.
history takes an entity id, and only entity_graph returns entity
ids (entity_search returns memories with a matched_entity name,
not the id):
memory(action="entity_graph", name="FastAPI", depth=1, limit=20)
memory(action="history", entity_id="<nodes[].id from the call above>")
history returns every version ever linked to that entity, including
closed ones, ordered oldest-first. It takes no limit.
Fall back cleanly when the graph is empty. Entity extraction runs
as a background task and needs a configured LLM provider; without one
entity_search / entity_graph / history return empty results even
though the memories exist. In that case answer from steps 2-3 alone
and say the timeline is reconstructed from version history rather than
entity links.
Query recipes
| Question |
Call |
| "What did I believe about X last month?" |
as_of at that date, limit=50, filter results for X |
| "When did this change, and what replaced it?" |
read valid_to + superseded_by on the closed row, then step 3 |
| "What did I know when I decided Y?" |
search for the decision -> take its created_at -> as_of at that timestamp |
| "Show the full timeline of X" |
entity_graph(name="X") -> history(entity_id=...) |
| "Was this deleted or replaced?" |
superseded_by non-null -> replaced; null -> retracted |
Output template
## As of <resolved UTC timestamp>
**Then**: <content of the version valid at that time> (id <old-id>)
**Changed**: <valid_to> -> superseded by id <new-id>
**Now**: <content of the surviving version> (id <new-id>, valid_to null)
<one sentence on what the change means for the question asked>
If the snapshot is empty, say so plainly and give the earliest timestamp
that does return rows — do not present a current-state answer as history.
Quality rules
- Always pass UTC. Timestamp comparison is lexicographic over ISO
strings, so an equivalent instant written with a different offset gives
wrong results:
2026-07-25T17:40:04+07:00 returns nothing where
2026-07-25T10:40:04+00:00 returns the row. Convert to +00:00
before querying.
as_of pairs only with action="as_of". Passing it alongside
search or list is rejected with an explicit error rather than
silently returning current-state results; do not retry by dropping the
parameter and presenting the answer as historical.
- Never claim a fact was true at T — only that it was recorded at T.
- Quote both ids when reporting a change. The user's older notes
reference the pre-update id, which no longer resolves through search.
- Know which action hides what.
as_of excludes archived rows on top
of the validity window; history deliberately does not filter archived
rows, because a retired memory is still part of the timeline;
entity_search returns current rows only and is therefore the wrong
tool for a timeline. Hand-written SQL over memories reproduces none
of these filters — use the actions.
When to Use
- The user asks what they thought, decided, or knew at an earlier time.
- A current memory contradicts something the user remembers storing, and
the question is when it changed.
- Auditing why a past decision looked correct given what was known then.
- Before overwriting a long-lived memory, to show the user the chain they
are about to extend.
1---2name: temporal-query3description: Answer time-travel questions over stored memory — what was believed at a past point in time, when a belief changed, and what replaced it. Use when the user says "as of", "back in", "at the time", "history of", "timeline", "what did I think then", or asks why a current memory contradicts an older one.4---56# Temporal Query78Point-in-time and version-chain lookups over the bitemporal columns9(`valid_from`, `valid_to`, `superseded_by`) that `mem_003_temporal` added10to `memories`. Ordinary search cannot answer these questions: `search`,11`list`, `stats` and single-id lookups all filter `valid_to IS NULL`, so12they only ever see the current state.1314## What the temporal columns actually mean1516Read this before phrasing an answer — getting the axis wrong produces17confident but false claims.1819- `valid_from` / `valid_to` track **when the store recorded a version**,20 not when the fact was true in the outside world. `as_of=T` answers21 *"what did this memory store hold at T"*, not *"what was true at T"*.22 If the user recorded a 2024 decision yesterday, it enters the timeline23 yesterday.24- `update` does **not** edit in place. It closes the old row25 (`valid_to = <update time>`, `superseded_by = <new id>`) and inserts a26 new row with a **new id** and `valid_from = <update time>`. The id in27 the user's notes from last month no longer resolves through `search`.28- `delete` also closes the row (`valid_to` set) but leaves29 `superseded_by = NULL`. That is the discriminator: a closed row with a30 forward pointer was **replaced**; without one it was **retracted**.31- Rows created by `add` / `capture` carry `valid_from = NULL`; queries32 fall back to `created_at` via `COALESCE`. A `null` `valid_from` in a33 result means "original version", not a data defect.34- `commit_sha` is present in the schema but is not populated by the35 current write paths. Do not present it as provenance.3637## Steps38391. **Resolve the temporal anchor** from the user's wording into a single40 UTC ISO timestamp: "last month" -> first day of that month,41 "before the migration" -> the timestamp of the migration memory,42 "at the time we chose X" -> the `created_at` of the decision memory.43 State the resolved timestamp in the answer so the user can correct it.44452. **Take the point-in-time snapshot**:46 ```47 memory(action="as_of", as_of="2026-06-01T00:00:00+00:00", limit=50)48 ```49 Returns rows satisfying50 `valid_from <= as_of AND (valid_to IS NULL OR valid_to > as_of)`,51 newest first. `as_of` accepts no query filter, so raise `limit`52 (max 100) and filter by topic yourself over the returned content.53 Omitting `as_of` returns the current state instead.54553. **Trace the version chain** for any row that matters. Each closed row56 carries `valid_to` (when the belief changed) and `superseded_by` (the57 id that replaced it). Follow the pointer forward by snapshotting just58 after that timestamp and matching the id:59 ```60 memory(action="as_of", as_of="<valid_to of the old row>", limit=50)61 ```62 Repeat until you reach a row with `valid_to = null` — that is the63 surviving belief.64654. **Use the entity timeline when the knowledge graph is populated**.66 `history` takes an entity id, and only `entity_graph` returns entity67 ids (`entity_search` returns memories with a `matched_entity` name,68 not the id):69 ```70 memory(action="entity_graph", name="FastAPI", depth=1, limit=20)71 memory(action="history", entity_id="<nodes[].id from the call above>")72 ```73 `history` returns every version ever linked to that entity, including74 closed ones, ordered oldest-first. It takes no `limit`.75765. **Fall back cleanly when the graph is empty.** Entity extraction runs77 as a background task and needs a configured LLM provider; without one78 `entity_search` / `entity_graph` / `history` return empty results even79 though the memories exist. In that case answer from steps 2-3 alone80 and say the timeline is reconstructed from version history rather than81 entity links.8283## Query recipes8485| Question | Call |86| --- | --- |87| "What did I believe about X last month?" | `as_of` at that date, `limit=50`, filter results for X |88| "When did this change, and what replaced it?" | read `valid_to` + `superseded_by` on the closed row, then step 3 |89| "What did I know when I decided Y?" | `search` for the decision -> take its `created_at` -> `as_of` at that timestamp |90| "Show the full timeline of X" | `entity_graph(name="X")` -> `history(entity_id=...)` |91| "Was this deleted or replaced?" | `superseded_by` non-null -> replaced; null -> retracted |9293## Output template9495```96## As of <resolved UTC timestamp>9798**Then**: <content of the version valid at that time> (id <old-id>)99**Changed**: <valid_to> -> superseded by id <new-id>100**Now**: <content of the surviving version> (id <new-id>, valid_to null)101102<one sentence on what the change means for the question asked>103```104105If the snapshot is empty, say so plainly and give the earliest timestamp106that does return rows — do not present a current-state answer as history.107108## Quality rules109110- **Always pass UTC.** Timestamp comparison is lexicographic over ISO111 strings, so an equivalent instant written with a different offset gives112 wrong results: `2026-07-25T17:40:04+07:00` returns nothing where113 `2026-07-25T10:40:04+00:00` returns the row. Convert to `+00:00`114 before querying.115- **`as_of` pairs only with `action="as_of"`.** Passing it alongside116 `search` or `list` is rejected with an explicit error rather than117 silently returning current-state results; do not retry by dropping the118 parameter and presenting the answer as historical.119- **Never claim a fact was true at T** — only that it was recorded at T.120- **Quote both ids** when reporting a change. The user's older notes121 reference the pre-update id, which no longer resolves through search.122- **Know which action hides what.** `as_of` excludes archived rows on top123 of the validity window; `history` deliberately does not filter archived124 rows, because a retired memory is still part of the timeline;125 `entity_search` returns current rows only and is therefore the wrong126 tool for a timeline. Hand-written SQL over `memories` reproduces none127 of these filters — use the actions.128129## When to Use130131- The user asks what they thought, decided, or knew at an earlier time.132- A current memory contradicts something the user remembers storing, and133 the question is when it changed.134- Auditing why a past decision looked correct given what was known then.135- Before overwriting a long-lived memory, to show the user the chain they136 are about to extend.