Skip to content

Temporal facts

Facts can carry a validity window, [valid_from, valid_to). Nothing changes until you use it: a fact with no validity data is always current.

// The user moved; the move happened on March 1.
const { newId } = await wiki.supersede(
'user-123',
seattleFactId,
{ title: 'Lives in San Francisco', body: 'Moved in March.' },
{ validFrom: Date.parse('2026-03-01') },
);
await wiki.read('user-123', 'where do I live'); // San Francisco
await wiki.read('user-123', 'where do I live', { asOf: Date.parse('2026-02-01') }); // Seattle
await wiki.history('user-123', seattleFactId); // [Seattle, San Francisco]
  • The window is half-open. At the switch instant, only the new fact matches.
  • asOf answers “what do we now know was true at time T”. If you learn about a past change late, past queries still come out correct.
  • traverseGraph({ asOf }) only walks edges whose two facts were both live at that instant.

To record a new fact that replaces nothing, use remember() with an optional valid_from. It stores the fact directly, with no LLM call. supersede() is still the way to replace one.

supersede() throws WikiSupersedeError (with a reason) when the target:

  • is an immutable_document fact (re-ingest the document instead),
  • belongs to another entity,
  • is already superseded, or
  • would create a cycle.
await wiki.write('user-123', {
event_type: 'observation',
summary: 'Moved to SF',
occurred_at: Date.parse('2026-03-01'), // real-world time; defaults to ingestion time
});

Pass the client’s timestamp, so a delayed or queued write still reflects when the event happened.

The 'ops' librarian sets valid_from on each fact it extracts. It uses the date the model gives, if any. Otherwise it uses the source event’s occurred_at, or the time the event was stored when that is not set. If the model names no source event, it uses the newest such date in the batch, but never a date later than the time of the pass. A fact taken from an event with a future occurred_at keeps that future date, so current reads leave it out until then.

History is kept indefinitely by default. Set config.pruneSupersededAfter (in days) to let runPrune() retire facts that expired that long ago. getMemoryBundle() and exportDump() return all non-deleted facts, including history.