Brainy Brainy
Docs Brainy

ADR-003 — Semantic (vector) time travel

In this section

Status: Proposed 2026-06-15. Phase 0 (verification) shipped in the 8.0.0/3.0.0 lockstep release. Phase 1 and the unfiltered half of Phase 2 shipped in 11.2.2 — the historical vector leg is native-served and rebuild-free (limits 2 and 3 closed). Limit 4 (embedding-model drift across history) is open; Phase 3 is a research track.

Related:

  • ADR-002 — DiskANN vector index

  • docs/snapshot-safety.md — the shadow-page generation substrate this builds on

  • Graph time-travel generation-threading (brainy 3.0 / brainy 8.0 getVerbEndpointsAtGeneration)

Context

Brainy 8.0's db.asOf(g) returns the database as it existed at generation g. For a triple-intelligence find() (metadata filter ∩ graph hop ∩ vector similarity), every substrate must answer "what did you look like at g."

Two of the three are solved by the shadow-page generation substrate: the metadata LSM and the graph endpoints store record brainy's generation per write and resolve point-in-time reads from a version chain. The id-mappers are monotonic. So the existence dimension — which entities and edges were present at g — is correct.

The vector/semantic dimension is the hard one, and it decomposes into three sub-problems:

  1. Candidate set at g — which entities existed. Already solved (the versioned substrates above).

  2. Vector values at g — an entity re-embedded after an edit has a different vector now than at g. The historical value must be recoverable.

  3. Index structure at g — HNSW/DiskANN build a navigation graph. The current graph differs from the one at g.

Plus a fourth, cross-cutting limit:

  1. Embedding-model drift — the query is embedded with the current model. If the embedding model changed between g and now, comparing a current-model query against old-model data vectors is a comparison across different vector spaces — silently wrong unless detected.

Versioning a million-node ANN graph per generation is prohibitive, which is why no standalone vector database offers semantic time travel. This ADR records a strategy that does, without paying that cost in the common case.

The key insight

In a triple-intelligence query the vector leg rarely runs alone — it runs over the intersection of the metadata and graph filters, which are already time-travel-correct and usually small. Over a candidate set of hundreds (not millions), an exact rerank over the entities' gen-g vectors gives perfect recall in sub-millisecond time, with no historical ANN index at all. The other two intelligences pre-pay the cost of vector time travel.

The hard residue is only the unfiltered case (pure semantic search over everything, as of g), which is where the index itself must time-travel.

Decision

A tiered strategy on the existing generation substrate, selected automatically by the AdaptiveDiskAnnModeSelector on (filter selectivity × history depth):

Regime

Mechanism

Recall

Cost

Filtered, any depth (common)

Exact rerank over the pre-filtered candidate set's gen-g vectors

Exact

Sub-ms (small set); no new index

Unfiltered, recent

Differential overlay: current index + visibility bitmap + sparse per-node vector version chain (only re-embedded nodes); navigate current graph, mask future nodes, evaluate gen-g vectors

Near-exact

One index; near-zero overhead

Unfiltered, deep

LSM-tiered shadow-page ANN: immutable index segments at generation checkpoints + deltas; search the segment(s) covering g; lazily materialized + cached

Exact (segment)

Bounded per-segment; pay-per-use storage

Cross-cutting

Embedding-model versioning: stamp each vector with its model; auto-project the query into the historical space, or hard-error — never silently wrong

Detection is O(1)

Options considered (and why not, alone)

  1. Exact rerank over the pre-filtered window — chosen for the filtered case; insufficient alone (unfiltered queries fall through).

  2. LSM-tiered / shadow-page ANN — chosen for the unfiltered-deep case; reuses the proven base+delta+compaction primitive so the vector index joins one unified MVCC substrate. Cost: search amplification across segments.

  3. Generation-tagged Vamana + delta-replay — exact historical structure, but Vamana's robust-prune rewires many neighbors per insert → fat delta logs; folded into (2) as the segment-internal representation rather than a standalone mode.

  4. Persistent / copy-on-write immutable ANN (structural sharing, à la Datomic) — elegant and instant asOf, but ANN's high fan-out + rewiring limits sharing and CoW pointer-chasing fights the cache locality ANN depends on. Research track (Phase 3).

  5. Differential overlay — chosen for the unfiltered-recent case; structure is current (approximate recall for deep history), which is why it is scoped to recent generations.

  6. Time as a first-class ANN filter ([birth_gen, death_gen] predicate) — composes with our metadata/graph filters and is how the candidate set is computed; suffers known recall cliffs at extreme selectivity, so it feeds the rerank/overlay rather than standing alone.

  7. Embedding-model re-projection — chosen as the cross-cutting correctness layer; the alignment map (Phase 3) is research-grade, but the detection of model drift (Phase 1) is cheap and ships early.

Open-core behavior (brainy alone vs brainy + brainy)

The API and correctness are identical; only speed and scale differ — the open-core contract.

  • brainy alone (MIT): db.asOf(g).find() is correct via brainy's canonical-record materialization — the historical candidate set is rebuilt from versioned records and reranked in JS. Slower (brute-force, no native acceleration), bounded to the JS-feasible scale (~10⁵–10⁶), but semantically correct.

  • brainy + brainy: the candidate set comes from brainy's versioned native substrates, the rerank is native, the unfiltered-deep path uses the tiered shadow-page ANN, and scale extends to 10⁸–10¹⁰. Same asOf(g).find() call; faster and larger.

This means semantic time travel is a brainy feature that brainy accelerates — not a brainy-only capability.

Phasing

  • Phase 0 — Verify + document (this release, 8.0/3.0). ✅ VERIFIED — see the guarantee + limits below. A cross-layer test (src/native/semanticAsOf.test.ts) builds entities, re-embeds one across generations, then asserts asOf(g).find({vector, where}) returns only the g-valid set, ranked by the at-g embedding, with the where filter seeing the at-g metadata. It passes (incl. a leak-detector that fails if the vector leg used the live index). No new critical-path code.

Phase 0 — verified guarantee + its limits (be honest in GA messaging)

Guarantee (verified, 8.0.0-rc.2 + brainy 3.0): db.asOf(g).find({ vector, where }) is set-correct, rank-correct (at-g embedding), and filter-correct (at-g metadata) — for mutations committed via brain.transact([...]).

Limits — all four are real; do not claim past them:

  1. ~~transact()-only.~~ RESOLVED before GA — brainy's Model-B landed in 8.0.0-rc.3 (5c3bb2c): single-op add()/remove() writes ARE retained (per-write persistSingleOp/commitSingleOp generation-stamping), so asOf(g) is history-correct for BOTH transact([...]) and single-op mutations. Regression-pinned in src/native/semanticAsOf.test.ts (runs green in the release gate vs the published 8.0.11). The rc.2-era gap this bullet used to describe no longer exists in any shipped 8.x.

  2. ~~The historical leg is brainy-JS-served this release.~~ CLOSED in 11.2.2 — the historical vector leg is served by the NATIVE index. NativeDiskAnnWrapper implements VersionedIndexProvider, so asOf(g).find({ vector }) routes to it for the filtered AND the unfiltered case; the ephemeral reader brain and its rebuilt JS HNSW are no longer on the path. isGenerationVisible(g) is the honesty gate — it advertises a generation only when the as-of window can be established and g is at or above its floor, and every refusal answers false, so the engine's canonical materialization stays the correct slower route.

  3. ~~O(n-at-g) rebuild per asOf Db — small/medium scale only.~~ CLOSED in 11.2.2 WHERE THE WINDOW IS INTACT — see the measured note below — nothing is rebuilt per call and nothing is proportional to the store. The read runs in two phases: the provider's masked walk over the CURRENT index returns an over-fetched candidate set (bounded by AT_GEN_UNFILTERED_OVERFETCH × page, capped at AT_GEN_UNFILTERED_CANDIDATE_CAP), and those candidates' at-g vectors are resolved from the canonical generation records and exact-reranked. Membership is exact on BOTH axes because the record layer — not the vector family's tombstone log — is the authority: the tombstone log records deletions and resurrections but never first adds, so it cannot place a birth, while the records can. Cost is the over-fetch, not the corpus.

  4. No embedding-model-drift guard on the HISTORICAL path. STILL OPEN, and worth restating precisely now that the native leg makes historical reads cheap enough to be routine.

    What IS guarded: the LIVE corpus. The vector family's main.familygen stamp carries modelId + modelDimension (and the leg variant), a cold open refuses a family whose stamp names a model this process is not configured for (DiskAnnModelMismatchError), and the re-embed ceremony converges every live row into the new space and publishes it atomically — there is no interval in which a query ranks a new-space vector against an old-space one.

    What is NOT guarded: history. The ceremony converges the live corpus and republishes the family; it does not rewrite the canonical generation records, so the at-g vectors phase 2 resolves are whatever was written at g — in the OLD space if the model has changed since. A current-model query scored against them compares two vector spaces, and nothing refuses it today. Detection belongs with the at-g vector resolution (the record would have to carry the space its vector was written in, and the leg would have to refuse — or project — on mismatch); alignment remains Phase 3.

    Until then the honest statement is: asOf(g) is exact for a store whose embedding space has not changed since g, and unguarded across a re-embed. Treat a ceremony as a boundary history does not cross.

What the native leg actually engages on — MEASURED, and the honest limit

The mechanism is proven end-to-end: src/native/semanticAsOfNativeLeg.test.ts runs the same history through the native leg and through the JS materializer and requires the same rows in the same order — re-embedded rows at their at-g position, rows born after g absent, rows removed after g present, hidden tiers hidden.

MEASURED on a copy of a real fleet brain (14,056 nouns / 72,679 verbs, 26 GB allocated, generation 419,387, exclusive gate lock held, 25 queries per row after a warm-up):

read

p50

p95

live now() unfiltered vector find

12.16 ms

30.30 ms

asOf(now) unfiltered — native leg

2.21 ms

7.84 ms

The historical read is FASTER than the live one, and that is the shape of the design rather than a surprise: the at-generation path exact-reranks a bounded candidate set, where the live path runs the full find pipeline.

Two honest limits behind that number.

First, what the gate used to refuse. Routing was originally keyed to the vector family's tombstone window, and on this same store that window was four generations wide (419,410–419,413) while the store sat at 419,387 — so the leg advertised nothing, at any depth. Every fold consumes tombstone records and pushes the floor to the newest consumed generation, so ordinary maintenance destroyed the window the gate was keyed to. The window was the wrong key: it is the floor of the provider-only answer, while the engine's composed read takes membership from the canonical generation records and re-supplies what the index no longer holds. Routing now asks only whether there are candidates to offer.

Second, and still open: depth is expensive, and the cost is in the record layer, not the walk. asOf(now) is the cheap case because every candidate resolves as current. One generation back, each candidate needs a real point-in-time resolveAt, and on a store with 419,387 generations that did not finish 25 queries within ten minutes — so the run was stopped rather than hold the exclusive lock while other legs queued. Deeper depths are therefore NOT characterized here, and nobody should read the 2.21 ms above as the price of deep history. Making at-depth resolution cheap — an index over the generation records, or the tiered shadow-page ANN this ADR already names — is the next piece of work, and it is a record-layer piece, not a vector one.

  • Phase 1 — Native-accelerate + drift detection. The native exact-rerank path LANDED in 11.2.2 (see limits 2 and 3). Embedding-model version stamping landed for the LIVE corpus (the main.familygen stamp + DiskAnnModelMismatchError + the re-embed ceremony); the cross-model hard-error on the HISTORICAL path is still owed (limit 4).

  • Phase 2 — Unfiltered paths. The unfiltered case LANDED in 11.2.2 as the masked walk + canonical exact re-rank described in limit 3 — the differential overlay's job, done against the record layer instead of a per-node version chain, which is why no new index structure was needed. The LSM-tiered shadow-page ANN remains the answer for a window whose deletions consolidation has already baked out: below the as-of floor the leg REFUSES (DiskAnnAsOfRefusalError, naming the floor) rather than answering approximately.

  • Phase 3 — Research track (lands when ready). Persistent/CoW ANN; learned cross-model re-projection (semantic time travel that survives embedding-model upgrades).

Consequences

  • Semantic time travel ships correct-first (Phase 0 verifies the realistic query), then fast (Phase 1+), rather than waiting on a versioned ANN index that may never be worth its cost.

  • The vector index becomes a first-class citizen of the same generation substrate as metadata and graph — one MVCC clock across all three intelligences.

  • The honest limit (embedding-model drift) is made explicit and, in Phase 3, solvable — rather than ignored.

Division of labor

  • brainy: asOf routing (when to materialize vs serve native); canonical-record retention of per-generation vectors; embedding-model version stamping in the record; the open-core JS rerank path.

  • brainy: native exact-rerank; the differential overlay; the LSM-tiered shadow-page ANN; mode selection by selectivity × depth; the model-drift detector + (Phase 3) re-projection.