brain-bitemporal · git:20260911.1764b2a · 2026-09-11 · sha256 670b78fe97d5581d

brain-bitemporal git:20260911.1764b2aA

Immutable. This exact content is served forever at /api/v1/blob/670b78fe97d5581d.

---
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
})
```

Returns five buckets for the half-open window `[from, to)`:

- `createdFacts` — net-new active facts (excludes rows that were superseded in-window — those go in changedFacts so consecutive diffs never double-count)
- `retractedFacts` — pure retracts with no successor
- `changedFacts` — superseded transitions, each carrying `{ before, after }`
- `newEntities` — `knowledge_entity.createdAt` in-window
- `forgottenEntities` — GDPR-erased tombstones in-window

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_profile({ entityId: "...", asOf: "2026-04-01T00:00:00Z" })
```

Returns the snapshot of beliefs as of that wall-clock moment — including facts that have since been retracted but were believed then.

### 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` returns the audit trail — retracted facts included. Each row carries:

```json
{
  "factId": "...",
  "predicate": "tier",
  "object": "platinum",
  "validFrom": "2026-04-01T00:00:00Z",
  "validUntil": null,
  "recordedAt": "2026-04-02T08:33:00Z",
  "retractedAt": "2026-04-10T14:00:00Z",   // ← present means retracted
  "retractionReason": "Source misattributed; was Bob, not Alice.",
  "status": "retracted"
}
```

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 `recordedAt` axis on `get_entity_timeline` is the transaction-time question. Sorting the timeline by `recordedAt` 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