brain-bitemporal · git:20261101.60e41ae · 2026-11-01 · sha256 a83c69a5d0687a1f
brain-bitemporal git:20261101.60e41aeA
Immutable. This exact content never changes and is served at /api/v1/blob/a83c69a5d0687a1f.
---
name: brain-bitemporal
description: How to query the INITE Brain knowledge graph across time — the asOf parameter, validFrom/validUntil semantics, reading retracted facts, and the memory_diff "what changed between two cursors" surface. Use when the user's question has a temporal dimension ("on X date", "before Y", "what's new since last conversation").
---
# brain-bitemporal
Brain is a bitemporal store. Every fact carries **two** time axes — when it was true in the real world (`validFrom`/`validUntil`) and when brain knew about it (`recordedAt`/`retractedAt`). Most agent code can stay on the "actual now" default; this skill is for when the user's question demands otherwise.
## The two axes
| Axis | Field pair | Meaning |
| --- | --- | --- |
| Valid time | `validFrom` / `validUntil` | When the fact was true in the world. `validUntil` open-ended (`null`) means "still true". |
| Transaction time | `recordedAt` / `retractedAt` | When brain learned the fact / when brain learned it was wrong. `retractedAt = null` means "still believed". |
Examples:
- Alice was *gold tier* from 2026-02-01 to 2026-04-01, then upgraded to *platinum* — two facts, sequential `validFrom`/`validUntil`. Neither is wrong; one succeeds the other.
- Brain recorded Alice's tier on 2026-03-15 from an inbox message, then learned on 2026-04-10 that the message was misattributed — fact retracted, but the row stays for audit.
## The default is "actual now"
Without `asOf`, every brain query returns only facts currently true in the world AND currently believed by brain:
```
validFrom <= now AND (validUntil IS NULL OR validUntil > now)
AND status NOT IN ('superseded', 'retracted', 'compacted')
AND (retractedAt IS NULL OR retractedAt > now)
```
This matches the Datomic / Zep convention and is almost always what callers want. If you're not sure whether to pass `asOf`, **don't**.
## "What changed?" — the memory_diff surface
When the user asks **"what's new since last time"** / **"what changed in the last week"** / **"diff between two points in time"**, don't fetch the timeline and diff it yourself — use `memory_diff`:
```ts
memory_diff({
from: "2026-05-15T00:00:00Z",
to: "2026-05-22T00:00:00Z",
entityIds: undefined, // optional — scope to a set of entities
predicates: ["status", "tier"], // optional — scope to a predicate family
userId: "u_42", // the person you act for
})
```
By default (`detail: "text"`) the answer reads as a changelog of the half-open window `[from, to)`:
- `changed:` one line per attribute that took a new value, oldest first: `ledger-sync — queue_backend: Redis Streams (2026-03-02) → NATS JetStream (2026-03-18)`;
- `learned:` everything else brain learned, under one heading per day, each piece as its facts or as the turn it was read from, whichever is shorter. When the window's turns cost less than the facts restating them (a window spanning most of a memory), you get the turns themselves under `said`;
- then `retracted:` (walked back, with the day), `new:` entities, `forgotten:` (GDPR-erased), and `expected to be over, not confirmed since:` (temporary states, like ill or away, whose expected end passed with nothing confirming it: worth asking about).
`detail: "full"` returns the JSON buckets with ids:
- `createdFacts`: net-new facts (rows superseded in-window go in `changedFacts`, so consecutive diffs never double-count);
- `retractedFacts`: pure retracts with no successor;
- `changedFacts`: superseded transitions, each carrying `{ before, after }`;
- `newEntities`, `forgottenEntities`, `lapsedExpectations`, and `truncated` when a bucket hit its cap.
The killer use case: a session-resume agent fetches `memory_diff(lastSessionEnd, now)` and uses the result to brief the user on what brain learned while they were away. Cheaper than re-fetching every relevant profile from scratch.
Consecutive diffs over adjacent windows compose: `diff(T0, T1)` + `diff(T1, T2)` covers `[T0, T2)` without double-counting because the window is half-open.
`asOf` is also available on `search_multi_hop` — the planner threads the cursor through every hop, so a historical multi-hop question (e.g. "what tenants in April had been complaining since March?") works the same way as a single-hop search.
## When to pass `asOf`
Three legitimate cases:
### 1. Historical question
```
"What was Alice's tier on March 15?"
→ search_knowledge({ query: "Alice tier", asOf: "2026-03-15T00:00:00Z" })
```
Returns the fact that was valid on that date.
### 2. "What did we know" investigation
```
"On April 1, what did brain believe about this dispute?"
→ get_entity_timeline({ entityId: "...", detail: "compact" })
keep fact.recorded events with at <= 2026-04-01, drop those retracted by then
```
`asOf` on the profile is valid time (what was true then), not a knowledge snapshot: a fact retracted since is not shown. The knowledge-time snapshot is the REST route `GET /v1/entities/:id?recordedAt=2026-04-01T00:00:00Z`, or the timeline events above.
### 3. Pre-incident reconstruction
```
"Before the migration on May 5, what status was tenant X in?"
→ get_entity_profile({ entityId: "...", asOf: "2026-05-04T23:59:59Z" })
```
Same as case 2 but with a sharper temporal frame — useful for postmortems where you need to prove "the data we were acting on said Y".
## Reading retracted facts in timeline
`get_entity_timeline` with `detail: "compact"` returns the audit trail as events — retractions included:
```json
{ "type": "fact.recorded", "at": "2026-04-02T08:33:00Z", "validFrom": "2026-04-01T00:00:00Z",
"factId": "...", "predicate": "tier", "object": "platinum" }
{ "type": "fact.retracted", "at": "2026-04-10T14:00:00Z", "factId": "...",
"reason": "Source misattributed; was Bob, not Alice." }
```
`at` is when brain learned (or unlearned) it; `validFrom` is when it became true in the world.
How to surface to the user:
- **Default**: hide retracted unless the user asked for history. The agent's job is to answer the question, not perform an audit.
- **Audit mode**: list retracted with a strikethrough or `(retracted: <reason>)` suffix. Never claim it never existed.
- **"What changed?"**: diff the timeline grouped by `predicate`. Each predicate's currently-active fact is the head of a chain; older versions sit behind it.
## Common pitfalls
### Confusing the two axes
"What was true in March" (valid time) is different from "what brain knew in March" (transaction time). The `asOf` parameter in brain currently filters on the **valid-time axis** with retracted-row gating against the same instant. If you genuinely need a transaction-time-only query (rare — usually only postmortems), pull the full timeline and filter by `recordedAt` client-side.
### Passing an `asOf` from a previous turn
If the user asked an as-of question, then asks a follow-up "and what about her email?", **drop the `asOf`** unless they say "still on April 1". Sticky `asOf` is a footgun.
### Reading a `validUntil` as "expired"
A fact with `validUntil = 2026-04-01` was true *up to* that moment. It's not "stale data" — it's bitemporally correct. Don't filter it out client-side; brain already did the right thing.
### Treating `single_active` predicates like `bitemporal`
Some predicates are `single_active` (e.g. `name`, `email`, `phone`): there can be only one active value at a time. New value supersedes old, no overlapping windows. Other predicates are `bitemporal` (`status`, `intent`, `address`): values are explicitly tagged with a window and can overlap with retracted but never with active. Conflict resolver scores both kinds the same; the semantics differ only at read time. Full table at `/docs/concepts/predicates`.
## When in doubt
Default to no `asOf`. Brain's "actual now" answers ~95% of agent questions correctly. Reach for `asOf` only when the user's question explicitly contains a date / "before X" / "what did we know" phrasing.
## "When did we first learn this?"
The `at` of a timeline event is the transaction-time question. Sorting the events by `at` gives you brain's learning order; sorting by `validFrom` gives the real-world order. They're often different — a fact "valid from 2026-01-01" might be `recordedAt = 2026-05-12` if a backfill landed late.
## Companion docs
- `/docs/concepts/bitemporal` — full semantics with Allen-interval-algebra examples
- `/docs/concepts/conflict-resolution` — how brain decides which fact wins when two ingests overlap