Brainy Brainy
Docs Brainy

Memory

Recall is not a search.

In this section

Recall is not a search.

A search asks what matches these words. Recall asks what should come to mind now — which is the match times how much the memory matters, how well it is still remembered, how recently it was made, and how often anyone has actually used it — and then what it is attached to.

Every product that needs memory has built that same thing out of four database calls and a blend written in the host language. Measured on a real personal brain against Brainy 11.2.0:

stage

cost

find({ query, where })

146–315 ms

related() expansion + host-language filters

28–41 ms

cross-encoder rerank on the host thread

655–2,307 ms

the five-signal blend

microseconds

end to end

843–2,656 ms, against a 300 ms target

Every number in that table is the cost of doing memory outside the engine that already holds the data. brainy's memory layer moves it inside.


1. Declare the shape

A memory is a noun with a known face. declareProfile writes that face onto the store:

import { Brainy, MEMORY_PROFILE } from '@soulcraft/brainy'

const brain = new Brainy({ storage: { path } })
await brain.init()
await brain.declareProfile(MEMORY_PROFILE)TS

From that moment every write through brain.memory is validated against the shape, and a violation refuses by name:

memory.remember(): profile 'memory' refuses field 'citedCounts' — the profile does
not declare it and the profile is SEALED. Did you mean 'citedCount'? An undeclared
field is stored and then never read by any signal or fence — which is
indistinguishable from a typo. Declare it (extend the profile) or correct the name.

That refusal is the whole point of the profile. citedCounts would have been stored happily, read by nothing, and ranked as though the memory had never been cited — for the life of the store, with nothing to see.

The record

field

type

who writes it

what it is

source

enum, required

caller

conversation · explicit · inferred · imported · synthesized · demo. Selects the initial stability.

productOrigin

string

caller

Which product wrote it.

sourceConversationId

string

caller

Where it came from.

tags

string[]

caller

Retrieval may union a tag membership with the vector leg.

citedCount

int

cite()

How many times a consumer actually used it.

world

'imagined'

caller

Absent means live. The fence is world missing.

dreamOf

string[]

caller

Which rollups an imagined row bridges.

retracted

object

retire()

The soft-retract record. Absent on a live row.

processing

object

jobs

Rollup bookkeeping — { abstractedIntoMemoryId }.

summary · intent · model

string

caller

Writer provenance.

classifier

object

caller

How the row was typed.

extras

object

caller

The long lists. Declared unindexed — carried whole, no postings.

recallCount

int

engine

How many times recall returned it.

lastRecalled

timestamp

engine

0 — never null — until the first recall.

stability

float (days)

engine

FSRS stability.

difficulty

float 1–10

engine

FSRS difficulty.

rememberedAt

timestamp

engine

Epoch ms remember() wrote this row, from its own clock. The origin a first recall measures elapsed time from — never system.createdAt, the engine row's real-clock stamp, which can disagree with the profile's clock under an injected now.

The last five are engine-written. A caller that passes one is refused, because they are the inputs to the rank signals: a caller able to set them could rank its own rows to the top while the engine reported an honest formula.

The layer is the subtype

There is no layer field. Every memory is an episode; an abstraction is one whose subtype is semantic-memory, and a passage is an auto-derived chunk of a longer document. A stored layer would be a second spelling of a fact the subtype already states, and two spellings disagree.

Adoption is one file

declareProfile writes _system/record-profiles.json and, when the profile names unindexed fields, updates _system/field-index-policy.json through the existing store-don't-index door. Nothing is re-embedded. No row is rewritten. There is no window. Validation applies from that write onward, and every memory already in the store serves unchanged — pinned in src/memory/memory.e2e.test.ts, which builds a store the old way, declares the profile, and asserts every row comes back byte-identical with the data generation unmoved.

The one behaviour that changes: extras becomes unindexed, so queries on it refuse by name from that moment. That is what the field is for.

Extending it

import { extendMemoryProfile } from '@soulcraft/brainy'

const shape = extendMemoryProfile({
  validFrom: { kind: 'timestamp', description: 'When this became true.' },
})
await brain.declareProfile(shape)TS

Additive in both directions: a later declaration naming fewer fields un-declares none, and a field re-declared with a different type refuses by name rather than letting two writers disagree about one store.


2. Write

const receipt = await brain.memory.remember({
  content: 'The release gate is the owner\'s; nothing ships without an explicit go.',
  type: 'norm',
  subtype: 'working-agreement',
  source: 'explicit',
  confidence: 1,
  metadata: { productOrigin: 'self', tags: ['release', 'gate'] },
  relate: [{ verb: 'relatedTo', to: anchorId }],
})
// { id, generation, related: [...], schedule: { recallCount: 0, lastRecalled: 0,
//   stability: 9, difficulty: 5 } }TS

Embed, add, relate — one call, one receipt carrying the id and the generation, so a caller can immediately ask the store what it just wrote at the exact version it wrote it. Relations are written after the row, so their source is always the id the receipt returns.

source seeds the FSRS stability: explicit 9 days, synthesized 10, conversation 6, inferred 5, imported 9.

The write itself acks at durability, not at embedding: the row is findable by id, tag or metadata the instant remember() returns, and its vector lands behind the doors on the engine's own background worker (receipt.embedding: 'deferred'). Recall by meaning converges only once that vector lands. A caller that needs to read its own write back by meaning right away has two ways to ask for it, both bounded by the embedder's own budget and both refused by name on timeout rather than a lie: pass awaitIndexed: true to remember() itself (the ack then waits inline and reports embedding: 'landed'), or call await brain.memory.awaitIndexed({ ids: [receipt.id] }) afterward — the same wait, on one id or several, for a caller that finds out only later that it needs one. Neither polls; both watch the store's own change feed.

Author

Every write door — remember, retire, cite, reinforce, consolidate — takes an optional author: an actor slug, the same string the guardrails calls an actor (a token's sub verbatim, a static key's kind:principal, or a caller-supplied slug in-process). remember() stamps it on the row itself; retire()/consolidate() stamp it on the RETRACTION record, never on the row's own author — demoting a memory does not change who wrote it, only who demoted it; reinforce() records it on the reinforcement ring entry it already writes, and cite() stamps it as a plain field on the cited row (lastCitedBy) — never as a graph relation, because an author is an actor slug and this engine never creates a node for a principal to relate from.

The host stamps it. Called through brainy host (any credential — a static key or a bearer token), memoryCall's caller becomes the author of every write in that call: the credential always wins, and a request body that names a different author refuses by name (MemoryAuthorMismatchError) rather than silently taking the body's word for it. A body that repeats the credential's own identity is a no-op, never a refusal. This is the exact seam memory.turn() already used to stamp its own caller — every other write door now shares it rather than inventing a second one. In-process, on a Brainy instance constructed with no caller at all, a call's own author lands verbatim; a call with none lands unattributed.

Unattributed is a fact this engine states plainly, never a default identity standing in for one: a row remember()d before this field existed, or written with nobody bound and nothing supplied, reads back with author simply absent — never 'unknown', never the empty string. recall(), context() and every read surface carry it exactly that way: present when the row has one, absent when it does not.

cite() and reinforce() still take the earlier by field too — author and by are one field, two spellings. Naming both is fine when they agree; naming both with different values refuses the same way a hosted mismatch does, because attributing a write to whichever of two disagreeing claims happened to be read first would be a guess wearing an audit trail's clothes.


3. Recall — one door, five stages, one budget

const answer = await brain.memory.recall({
  query: 'what did we decide about pricing',
  limit: 8,
  budgetMs: 300,
})TS

stage

what it does

retrieve

find({ query, where }) over the fences, to a pool (default limit × 8), plus an optional tag leg unioned with the vector leg.

fuse

The pool ranked inside the index by the memory blend. Nothing crosses but the order.

expand

related({ node, type, where }) — the neighbour predicate evaluated natively, inside the cap.

rerank

The cross-encoder stage, when this build carries one.

strengthen

The write side (below).

Why a pool and not a page

A blend applied to the top 10 the vector chose can only permute those 10. The row the signals would have lifted from rank 40 to rank 1 was never fetched — so a host-language blend is cosmetic exactly where it matters most. The cure consumers reach for is over-fetch: ask for 500, sort, return 8.

Fusing in the engine removes both. The columns are walked where they live, the arithmetic happens where the numbers are, and no signal value crosses the boundary at all — so a pool of ten thousand costs the same crossing as a pool of ten.

The blend

rankScore = cosine
          × recency(createdAt: <1d 2.0, <7d 1.5, <30d 1.2, else 1.0)
          × (0.5 + 0.5 × clamp01(confidence × retrievability))
          × min(1 + 0.05 × recallCount, 1.5)
          × (1 + 0.15 × min(1, ln(1 + citedCount) / ln 9))

Every signal produces a multiplier. Nothing is added, nothing is normalised behind the caller's back, and answer.plan.formula renders exactly the above — so a rankScore can be recomputed by hand. A ranking nobody can check is a ranking nobody should trust.

Change it with rank, extend it with rank: { also: [...] }, or turn it off with rank: false.

Retrievability is computed, never stored

R = (1 + 19/81 × elapsedDays / stability) ^ −0.5

The FSRS forgetting curve, evaluated during the fuse stage against the engine's clock, and returned as row.score.retrievability. It is not written back: a stored retrievability is wrong the instant after it is written, and a store full of stale ones ranks by how recently a job happened to touch each row rather than by how well it is remembered. The stored truth is stability plus lastRecalled / createdAt; retrievability is what they mean now.

A memory recalled 20 times is learned: the curve stops applying and it stays at 1.

Three states, kept apart

A row that does not carry a signal's field scores neutral for that signal. Absence is a real state — citedCount is absent until the first citation — and a row in that state has no evidence in either direction.

A field the store knows but holds no values for is also a real state: a brain where nobody has recorded a confidence yet, or a profile field nothing has written. Its signal contributes the neutral 1 to every row, which is the honest answer with no evidence either way. Refusing here would take recall out on every fresh store, so it is reported instead — plan.unservedFields names it, and the fuse stage's detail line counts it:

answer.plan.unservedFields   // ['system.confidence'] — the signal did not fireTS

A field the store has never heard of — neither an engine scalar nor one the declared profile carries — is the third state, and it refuses:

memory.recall(): rank signal on 'citationRank', which this store has never heard
of — it is neither an engine scalar nor a field the declared profile carries. The
signal would contribute nothing to every row, forever, and the answer would not
say so — so it refuses instead.

That is the typo — citationRank for citationCount — a field the caller believes is being ranked on and never will be, for the life of the store. Engine scalars are always known: the field-addressing law refuses a misspelled system. name long before it reaches the ranking.

Scope

await brain.memory.recall({
  query: '…',
  scope: { world: 'live', visibility: 'external', retired: false },  // the defaults
})TS

One option, not a family of includeX booleans: three booleans make eight universes with no name. Every default is conservative, and every one is a real fence in the where the retrieve stage runs — a fenced-out row never enters the pool and never costs the ranking anything.

The plan is part of the answer

answer.plan.stages   // [{ stage: 'retrieve', ms, in, out, detail }, …]
answer.plan.formula  // the blend, as arithmetic
answer.plan.fields          // the index keys the ranking read
answer.plan.unservedFields  // those the store has no values for yet
answer.plan.pool     // how many candidates were actually scored
answer.plan.skipped  // stages the budget did not allow
answer.asOf          // the generation this answer is as-ofTS

A stage that would run past budgetMs is skipped and named, never truncated in silence.


The verbs are the engine's

The verb vocabulary is a closed set of 127 terms, and relate() refuses one outside it by name. brainy's memory layer obeys that rather than widening it — a memory feature minting private verbs would put edges in the graph that no other product's traversal, export or ontology could read. Three needed a decision:

what it means

verb

why that one

a consumer cited this memory

references

supports is evidential support — a claim about the content, which belongs to whoever wrote it, not to the act of using it.

an episode's rollup

conceptualPartOf

Not a new idea: it is the graph twin consumers already write (member conceptualPartOf abstraction), so the engine and the service agree without either changing.

two memories came to mind together

correlatesWith

Observed from use. Not similarTo (a claim about their content nobody measured) and not causes (a claim nobody made).

Recall expands along relatedTo, supports, causes, enables, references and conceptualPartOf by default; pass any other term the ontology carries.

Co-recall edges are written with visibility: 'internal' — they are the engine's own bookkeeping, not something a person wrote — so reading them back takes includeInternal: true.


4. Recall writes

Recall strengthens what it returned — recallCount, lastRecalled, and the FSRS state — and writes co-recall edges between the memories that came to mind together, in the same call.

A memory's FIRST strengthen has no lastRecalled yet to measure from, so it measures elapsed time from rememberedAt — the row's own write-time clock — rather than the engine row's system.createdAt. The two normally agree; they can disagree under a test's or a replay's injected clock, and only rememberedAt means what the memory profile itself believes about when it wrote the row.

A door that writes when the caller only asked to read has to say so. Both are declared behaviours of the memory profile, narrated once per brain, and each is one option away:

await brain.memory.recall({ query: '…', strengthen: false, coRecall: false })TS

Expanded rows are never credited: they were near something that came to mind, they did not come to mind. Co-recall is bounded to the top 3 by default — linking a whole page of 8 would mint 28 edges per recall, which is not a graph learning from use but a graph memorising every query.

Citations

await brain.memory.cite([id], { author: 'agent:brainy' })TS

citedCount is the one feedback signal a query does not generate. The gap between recallCount and citedCount is the difference between returned and useful, which is why the blend gives citations their own saturating boost rather than folding them into the recall count.

author (who is citing) lands on the cited row's own record (lastCitedBy) — never as a graph relation. An author is an actor slug, and this engine never creates a node for a principal to relate from; relate() requires both endpoints to already be real rows. This is a different fact from the memory→memory citation GRAPH §9 below describes: that graph is built between two MEMORIES, and cite() credits a consumer's use of one, which is not the same relationship.


5. Retire — a demotion, never a delete

await brain.memory.retire(id, { reason: 'superseded by the September ruling' })

// When something replaced it, say so: one hop from the demoted row to its
// successor, so a reader holding an old memory is never left at a dead end.
await brain.memory.retire(id, { reason: 'superseded', supersededBy: newerId })TS

Recall stops returning the row. get(), the change feed, history and every asOf view still hold it, unchanged. A reason is required: a retraction with no reason is a deletion wearing a nicer name, and every later reader is left unable to tell what happened or whether to trust what cited it.

There is no forget(), and there will not be one.

options.author — who is retiring it — lands on the retraction record itself, never on the row's own author: retiring a memory does not change who wrote it, only who demoted it. See Author.

memory.list({ scope: { retired: true } }) is where a person finds what they retracted.

The demotion itself lands in one write, so a caller never waits for a job. What a retirement implies elsewhere — the episode it belonged to now has one fewer live member — is settled behind the doors by the retirement pass, which is also where the retention law is CHECKED rather than merely intended. See Housekeeping.


6. Context — a slice that already fits

const slice = await brain.memory.context('what did we decide about pricing', {
  budget: { chars: 12_000 },
})TS

One call that fills a model's window: cited facts first, the recent tail verbatim, and older episodes represented by the abstractions that absorbed them. Every item carries its provenance — { id, generation, layer, source, why } — and an abstraction says which episodes it stands for, so a caller can render "and 43 more" rather than pretending the rollup is all there ever was.

Two rules the door will not bend:

  • The engine composes; it does not write. An abstraction's text is whatever the service wrote into it. Nothing generates, summarises or paraphrases.

  • The budget is characters, never tokens. Tokenisation belongs to the model server; it changes per model and per version, and counting it here would mean counting for the wrong tokeniser and reporting the number as though it were true. Convert once, where the tokeniser lives.

A budget that cannot hold even one item refuses by name, saying whether the scope was empty or the shortest row was simply longer than the budget.


7. Everything else the namespace does

await brain.memory.list({ orderBy: 'system.createdAt', order: 'desc', limit: 50 })
await brain.memory.episodes()               // the abstractions this brain holds
await brain.memory.episodes(abstractionId)  // and the episodes one stands for
await brain.memory.stats()                  // what this store holds
await brain.memory.consolidate({ dryRun: true })  // the near-duplicate clusters, unwritten
await brain.memory.jobs()                   // a health row per housekeeping jobTS

stats() answers from posting-list cardinalities — no id is materialized — so it costs the same on a ten-million-row brain as on a ten-row one. It also reports which rank signals this store can actually serve, which is the honest warning that a recall on it would refuse.


7b. Housekeeping

Four jobs run behind the doors on a memory brain: decay (the importance a memory's use earns), episodes (structure only — the summary is a service's to write), consolidation (near-duplicates linked, the older retired) and retirement (a demotion's consequences, and the retention law's watchman). None of them is bounded by the size of the store, none of them deletes anything, and a job with nothing to do says nothing at all.

They arm themselves the first time a memory door is used. There is no start button. Housekeeping has the cadences, the budgets, the health rows, and the reasoning behind each one.


8. Time in two senses

A memory carries two clocks and they answer two different questions.

question

clock

owned by

reached through

What did I know then?

record time

the generation log

recall({ asOf })

What was true then?

event time

you

recall({ validAt })

They are independent, and confusing them is the most common way a memory system lies. "The office moved to Portland", written on Tuesday about a move that happened in March, is record-time Tuesday and event-time March. asOf(monday) must not return it — it was not known yet. validAt(april) must — it was true. Ask both at once and you have asked the audit question: as the brain stood then, what did it hold true at that moment.

// What was true in April?
await brain.memory.recall({ query: 'where is the office', validAt: new Date('2026-04-01') })

// What did the brain believe at generation 4210?
await brain.memory.recall({ query: 'where is the office', asOf: { generation: 4210 } })

// As the brain stood last Monday, what did it hold true in April?
await brain.memory.recall({
  query: 'where is the office',
  asOf: { at: lastMonday },
  validAt: new Date('2026-04-01'),
})TS

The event-time window

Two optional fields, [validFrom, validUntil) — inclusive start, exclusive end, both epoch ms, both absent by default:

what you write

what it means

neither

true for all time — the common case

validFrom only

true from then on

validUntil only

true until then

both

true across that half-open interval

The end is exclusive so two adjacent facts about the same thing are never both true at the seam. With an inclusive end, "Portland until March 1" and "Seattle from March 1" would both answer validAt(March 1), and the brain would contradict itself at exactly one instant per revision — the kind of defect that is invisible in every test that does not land on the boundary.

A validUntil at or before its validFrom refuses by name (MemoryValidIntervalError). There is no sane repair: clamping would invent a duration nobody claimed, dropping the bound would turn a bounded fact into a permanent one, and skipping the row would lose it silently.

validAt is a native predicate — (validFrom missing OR validFrom <= t) AND (validUntil missing OR validUntil > t), every operator in it served by the index — so it is evaluated inside the walk, never as a filter over an over-fetched page. validDuring(from, until) is its interval form, for "what was true at any point during Q3"; sampling instants would miss every fact whose window fell between the samples.

Reading history does not change it

A recall through asOf runs against a reader. The strengthen pass and the co-recall write are turned off for that call — reading history must not change it, and crediting a retrieval against a past generation would credit something that has not happened yet from that generation's point of view. A store whose engine does not serve asOf() (it is optional in the contract) refuses by name rather than quietly answering from the present.


9. Provenance is a graph, and citations are counted

Where a memory came from used to be a string on the row. A string is a dead end: you cannot ask what else came out of that conversation, you cannot ask which memories rest on a document that was later retracted, and you cannot rank a memory by how much of the brain leans on it — because the thing being leaned on is not in the brain at all.

So the source is a node and provenance is edges:

(memory) --references/derivedFrom--> (source: conversation | document | import)
(memory) --references/cites-------> (memory)
(replacement) --supersedes--------> (retired memory)
(memory) --correlatesWith/coRecalls--> (memory)TEXT

remember({ source, sourceRef }) creates the source node or reuses the one already there. Reuse is race-free, not best-effort: the node's id is derived from sourceRef as a natural key (any non-UUID string normalizes to a stable UUID v5) and written with ifAbsent, so two memories written concurrently from one conversation converge on one node instead of splitting its citation count in half.

One relationship, one edge, one counter

A memory cited eight times BY ANOTHER MEMORY is one relationship that happened eight times, not eight citations — the machinery underneath the (memory) --references/cites--> (memory) edge above, shared by whatever writes that graph. The counter lives on the edge, incremented rather than duplicated on every repeat citation.

This is a different fact from brain.memory.cite(), the public door: that one credits a CONSUMER's use of a memory (citedCount, and lastCitedBy when an author is named), never a memory citing another memory, and it touches no edge at all — see Author above for why.

Both alternatives to a counted edge are wrong in ways that matter. Eight parallel edges make the rank read one citation as eight votes, so a single enthusiastic consumer outvotes the rest of the brain. An array on the row (cites: [id, id, …]) grows without bound on exactly the hottest rows, cannot be walked from the other end, and mints a posting per element.

Co-recall edges — "these came back together" — are canonically ordered, the lexicographically smaller id first. Written naively, the pair (a, b) makes one edge from one recall and the opposite edge from the next, so a count that should read 2 reads 1 and 1 and the signal is halved exactly when it starts to matter.

citationRank — a rank, not a count

Counting citations says a memory cited eight times by throwaway rows is worth more than one cited twice by the two rows the whole brain rests on. Rank fixes that: importance flows along citations, so being cited by something important is worth more than being cited often. On a memory brain that is exactly the distinction that matters — the facts everything else is built on should surface, and they are rarely the facts that got mentioned the most.

citationRank is a bounded-iteration PageRank over the cites graph, scaled so 1.0 is the most-cited memory in this brain and 0 is uncited. Each edge is weighted by its counter — one vote of weight N, with the citer's whole outflow still bounded by its own rank, so nobody buys influence by citing the same row repeatedly. With uniform weights it reduces exactly to the classic formula.

It is never computed per query. A rank is a property of the whole graph, so computing it inside a recall would make every recall pay for every citation in the brain — the cost class that took two fleet consumers down when a per-update full value-space walk shipped in 4.2.1. It runs as a marker-backed background job with a budget, preemptible by any foreground door:

armCitationRankJob(runner, brain)      // the cadence
citationsChanged(runner)               // the write path's re-arm
await brain.memory.rank()              // one pass now, for a fixture or an operatorTS

A pass writes only the rows whose rank actually moved (past 1e-4 on the 0–1 scale). Writing every row every cycle would be a full-store rewrite on a cadence — a maintenance term that scales with the store rather than with the work. On a settled brain a cycle writes nothing.

A consumer that cites (cite(ids, { by }) writes consumer → memory) takes part in the flow without receiving a rank of its own: it votes, it is not voted for, and nothing stores a citation rank on something that is not a memory.


10. Reinforce — telling the engine how it went

Before this door, a memory was strengthened for being returned. That is the wrong signal, and it degrades a brain in a self-reinforcing way: a row that a weak query keeps pulling up gets stronger every time it is pulled up, which makes it rank higher, which makes it get pulled up more. The rows that climb are the ones the retriever happens to favour, not the ones that helped anybody — and nothing in the engine could tell the difference, because nothing in the engine was ever told.

await brain.memory.reinforce(id, { outcome: 'useful', by: 'venue', recallId })TS

outcome

stability

difficulty

confidence

useful

FSRS Good — it grows

eases

+0.05

neutral

FSRS Hard — a damped update, not a skipped one

hardens

unchanged

wrong

unchanged — it does not strengthen

hardens

−0.10

Being wrong costs more than being right pays, deliberately. Symmetric steps would let two useful reports cancel one wrong one, so a row that is right half the time would climb steadily toward certainty.

wrong does not apply FSRS's lapse branch, and that is a considered choice. A lapse models a holder who forgot — evidence about the memory. wrong here is a consumer's verdict on an answer: the row came back for a question it did not fit. Decaying stability for that would punish a well-held fact for the retriever's choice, and it would decay exactly the rows a weak query keeps surfacing, over and over, until they vanished from a brain that never had anything wrong with it.

Idempotent per (id, recallId)

A consumer that retries a failed call must not strengthen a memory twice, and a consumer whose transport retried without telling it has no way to know it did. Every applied event leaves an entry in a small ring on the row, and an event whose recallId is already there is not applied again.

The ring entry stores the state the row was in before the event, which buys something an "already applied" flag cannot: a correction. Report useful, then learn the answer was wrong, and sending wrong for the same recallId recomputes the row from the state before the first report rather than compounding on top of it.

await brain.memory.reinforce(id, { outcome: 'useful', recallId: 'r-91' })  // applied
await brain.memory.reinforce(id, { outcome: 'useful', recallId: 'r-91' })  // duplicate — no-op
await brain.memory.reinforce(id, { outcome: 'wrong',  recallId: 'r-91' })  // correctedTS

The bound is stated rather than implied: the ring holds eight graded retrievals per row, so idempotency covers the last eight — a retry window measured in retrievals, not in time. Omit recallId and one is derived from (id, outcome, by), which makes an identical retry idempotent for a caller that keeps no ids of its own. It is deliberately not time-based; a timestamped derivation would make every retry a new event, which is the exact double-counting the door exists to prevent.

by is the earlier spelling of author — one field, two spellings, and the ring entry itself carries whichever was given.

reinforce() and recall()'s own write side go through one function, so the two doors cannot move a memory's counters differently. A read-only view refuses by name: a historical recall can be read, never reinforced.


11. Scope — the fences, as one option

A brain holds rows that are real and rows that are not: dreamed rows an imagining pass wrote, internal faculty and ledger rows nobody asked about, and rows retired because something better replaced them. All three stay in the store — nothing is ever deleted — and all three are absent from an ordinary recall.

axis

values

default

world

'live' · 'imagined' · 'all'

'live'

visibility

'external' · 'internal' · 'all'

'external'

retired

false · true

false

await brain.memory.recall({ query, scope: { world: 'imagined' } })   // only the sandbox
await brain.memory.recall({ query, scope: { retired: true } })       // including the retiredTS

Three-valued rather than three booleans, because an opt-in flag can only widen. The questions people actually ask — "show me what I imagined about this", "show me the internal ledger rows" — are narrowing ones, and with opt-ins they are unaskable: include: { imagined: true } returns the lived rows too, and you are back to filtering in your own code over a page that was cut before your filter ran.

world absent on the wire means live. A lived row does not carry the field at all, which is why 'live' lowers to missing rather than to a value test: had the live state been written (world: 'live'), every row in every memory brain would carry a field whose only value is the default — a posting per row, forever, to say nothing. It is also why it is not ne: 'imagined', which would admit any future world value nobody has defined yet: a fence that opens itself the moment the vocabulary grows.

Superseded rows are retired rows. retire(id, { supersededBy }) writes the soft-retract record and the supersedes edge. The record is what the scope tests; the edge carries "and this is what replaced it". A second materialized superseded flag would be a second spelling of one fact, and two spellings disagree.

Every scope is applied inside the where, against the posting lists, and every recall reports the scope it actually ran at:

answer.plan.scope    // { world: 'live', visibility: 'external', retired: false }
answer.plan.validAt  // the event-time instant, when one was asked forTS

Always reported, even when nothing was narrowed — the fence held and matched nothing and the fence was open are different facts about an empty result, and a caller who cannot tell them apart will eventually explain one as the other.


12. What this rests on

Three engine doors, each usable on its own:

door

what it removes

brain.rankCandidates(ids, scores, spec, fields, k)

The host-language blend that can only permute a page.

brain.related({ node, type, where, limit })

The batchGet + host-language filter that hydrated rows about to be discarded — and makes limit count admissible neighbours.

brain.countByValue(field, value)

getIdsForFilter(f).length, which answers a count by marshalling every matching id first.


13. What follows — the graph-via context boost (11.3.1)

One signal in the shipped blend's ancestry is not here yet, and it is named rather than quietly dropped.

Consumers boost a memory by the context it sits in: a note filed under a space the person opened this morning matters more than the same note filed under a space they have not touched in a year. That is a signal whose value comes from a traversal, not from a column — and the fusion door reads columns.

The shape it will take:

rank: {
  also: [{ kind: 'boost', via: 'filedUnder', field: 'lastOpenedAt', weight: 0.2 }],
}TS

The mechanism is one small change to the native door: rankFuse accepts caller-supplied per-candidate values as a virtual field alongside the real columns, and the recall door fills that vector from one native traversal before the fusion runs. The signal grammar itself needs nothing new — it is a boost over a field the store does not happen to hold in a column.

It is 11.3.1 and not 11.3 because until it exists, a consumer's own boost is a handful of microseconds over a page it already has — the one place a host-language blend costs nothing. Keep yours until this lands.

The citationRank signal (§9) does not wait for it: a rank over the whole cites graph cannot be a per-query traversal at any price, so it is computed by a job and READ from a column like any other signal. The virtual-field door is for signals whose value is genuinely per-query, which a whole-graph rank is not.


14. Refusals, in full

refusal

when

RecordProfileError

A shape violation at the write door — names the field, what was expected, what arrived, and a did-you-mean.

RankSignalError

A rank signal on a field the store has never heard of. (A field it knows but has no values for is reported in plan.unservedFields, not refused.)

RecallError

An empty query, a non-positive limit, or a rerank asked for on a build with no rerank stage.

RememberError

Empty content, a noun type outside the memory set, a retraction with no reason, or an id the store does not hold.

ContextError

A budget that cannot hold one item.

MemoryValidIntervalError

A validUntil at or before its validFrom.

MemoryInstantError

An event-time bound that is not a finite instant — an Invalid Date, an Infinity, an unparseable string.

MemoryOutcomeError

A reinforce() outcome outside useful / neutral / wrong.

MemoryReadOnlyError

A write door reached through a historical (asOf) or read-only view.

MemorySourceRefError

A source with no stable sourceRef to create-or-reuse its node by.

MemoryAuthorMismatchError

A write names an author (or, for cite/reinforce, a legacy by) that disagrees with the identity the call is actually attributed to — a hosted body claiming a different author than its credential, or one call's author and by naming two different actors.

UnindexedFieldError

Any query on extras (or another declared-unindexed field).

Every one of them names the field or the parameter and states the cure. None of them is a warning that a caller can miss.