Docs · the memory model

What a memory is,
and what makes one
outrank another.

Read this once. Every command, every default and every refusal in the tool follows from what is on this page and the arguments here are the ones the codebase actually makes in its own comments.

Anatomy of a memory #

A memory is a title, a body and the metadata that decides how it ages and how it ranks. Nothing here is free text pretending to be structure.

FieldWhat it decides
titleIdentity, together with scope. Normalised, so re-remembering the same fact refines it in place instead of growing a second copy.
bodyThe fact. Written for a stranger six months out, not for the session that stored it.
typeThe decay tier, whether a commit can falsify it and whether it gets a review date.
scopeWhere it applies. Nests on :, flows inward only.
confidenceconfirmed, reported or assumed. Folds into the ranking tie-break.
importance1 to 5, 3 is neutral. Stakes, not relevance.
sourceThe citation. Required to store something as confirmed.
truststated, imported or external. Where the row came from, as distinct from how solid the claim is.
source_commitStamped automatically on the types a commit can falsify. Drives the freshness verdict.
valid_from / valid_toWhen the fact became true and when it stops being true, for things with a known window.
superseded_byThe correction that retired it. Recall follows this link forward.
pinAlways in the brief.
sessionWhich session stored it. Matters once more than one agent works one project.

Types and decay tiers #

The type you pick sets the shelf life. This is the single most consequential choice at write time and it is why --type is not optional in practice.

TierHalf-lifeWeightTypes
Anever decays1.30infra access gotcha incident
Bnever decays1.20decision constraint commitment
C60 days1.00preference relationship
D4 days0.70state
Nno decay, no weight1.00knowledge brand sop process standard policy report note

Decay never deletes. A tier D memory a month past its half-life is still in the store and still returned by an exact query; it has simply stopped outranking things that still matter. That is the behaviour you want from "the box is rebooting" and it happens with nobody remembering to clean up.

incident is tier A, not tier D

An outage feels ephemeral and is not. What happened to a system does not stop being true and the value of an incident record is being findable by whoever asks months later rather than that week.

Guardrail types bypass ranking entirely

policy and constraint memories go into every brief whether pinned or not. A binding rule that can lose its slot to something more topical is not binding.

The scoring model #

Retrieval is SQLite FTS5 first. What comes back is then scored:

score = relevance × recency × stakes

  relevance  bm25, mapped onto 0.35..1 — a matched row can never score
             as if it had not matched at all
  recency    0.5 ^ (days / tier half-life), or 1.0 for the tiers
             that never decay
  stakes     tier × (importance / 3) × usage × confidence,
             clamped to the band 0.70 .. 1.43

Why the stakes term is bounded

It was not, once. Measured on this project's own 571 memories over ten real queries, lexical relevance spanned 1.12x while the multipliers spanned 43.75x, so the published formula was in practice "the multipliers". 54% of result pairs came back out of lexical order, with promotions of up to twelve places and in every worst case the demoted memory was the one that literally answered the question.

The rule now is: what a record is breaks ties, it does not override what matched. The band is derived rather than picked. It is the widest of the tier weights (1 / 0.70), so the coarse ranking of types passes through untouched and only the compounding of several signals is bounded.

Decay is deliberately outside the band. Staleness is a claim about whether something is still true, not about how much it matters, so a four-day memory thirty days old is allowed to sink out of sight.

The usage term

A memory that keeps being returned and kept earns a small, capped lift: 1 + min(0.15 × ln(1+uses), 0.5). Recall counts as a use unless you pass --no-touch, which exists so that maintenance passes and scripted sweeps do not inflate the signal they are auditing.

Scopes #

A scope is a path that nests on :. Information flows inward only.

$ ns-brain recall "cert renewal" --scope channel:deploy:prod

sees:      channel:deploy:prod
           channel:deploy
           channel
           global
never sees: channel:deploy:staging, or anything below it

Everything defaults to global, which means "everywhere in this project" rather than "everywhere in the world". Scopes are declared in brain.json with ns-brain scope and ns-brain map reconciles what is declared against what is actually stored, so a scope invented once by a typo shows up as a scope nobody declared.

Use them when a project has genuinely separate domains. Two or three earn their keep. A dozen is a filing system you will stop maintaining and an unfiled memory in global is still found.

Confidence and trust #

They answer different questions and both are stored.

ConfidenceWeightMeans
confirmed1.15Cited. Somebody else could check it: a path, a command, a URL, a commit, or the user saying so.
reported1.00The default. Believed, not proven.
assumed0.80Inferred. Kept because losing it is worse, ranked accordingly.

high, medium and low are accepted as spellings of the same three, because another brain in the house uses that vocabulary and silently storing a value nothing understands is worse than translating it.

confirmed is not a self-assessment. It needs a citation in --source. A standing preference has no artifact but the person who stated it, so --source "stated by the user" is a legitimate citation. An uncited claim is stored one tier down as reported rather than refused, because losing the text at the moment you still hold it is the worse failure.

Trust is separate: stated is the default and means it came from this project's own work, imported is set on everything that arrives through import and external marks a claim from outside. A dump is untrusted input like anything else arriving from outside and the stamp survives so a later session can tell.

Supersession #

A markdown file has no way to say that one statement retired another. This does.

$ ns-brain remember --type infra --supersedes 31 \
    --title "Postgres, not SQLite: three writers" \
    --body "moved to Postgres 2026-03-04; the WAL contention..."

The identity moves to the new memory. The old one keeps standing as history, with a superseded_by link and recall follows that link forward: a query in the outdated wording answers with the current fact, not with both at equal weight. --all shows the retired versions when you want the trail.

Two guards sit on this and both were written after something went wrong:

Duplicate refusal #

Identity is scope plus normalised title, so storing the same title twice updates in place. The harder case is a second memory about the same subject under a different title and the write path refuses that too: titles overlapping by half their tokens or more are blocked, with the existing id and the three ways out.

#42 in channel:deploy is already about this (67% title overlap)
  the fact CHANGED         --supersedes 42
  same fact, better words  ns-brain update 42 --body ...
  genuinely separate       --allow-duplicate

ns-brain review had always reported duplicates, but a report arrives after the store has already grown one and it reaches a different session than the one that made it. The write path is the only moment the caller still holds the text.

Titles under five tokens are exempt. prod db host and prod db port overlap 0.50 and are different facts. Measured against this project's own titles (7 to 47 tokens, median 15) the floor costs nothing and failing open is the right default for a heuristic: the fallback is the behaviour there always was.

Provenance and freshness #

A memory whose claim is about code records the commit it was written against and recall renders a verdict on it:

tree unchanged since stored (a41f22c9)
14 commits since (a41f22c9 → 6893e37b), re-check against source
stored at a41f22c9, which is not in this tree — re-check against source

So a brain does not abolish re-reading the files. It routes it, one fact at a time and tells you which claims are safe to act on without opening anything.

Only gotcha, state and incident get stamped. The list is narrow on purpose: a freshness line that fires on every memory is a freshness line everyone learns to skip. A decision deliberately carries none, because a decision is falsified by the person who made it and never by a commit and flagging one as stale only invites re-arguing a settled call.

A memory belonging to another project in a shared store gets no verdict either. Its claims were recorded against a tree that is not this one.

Review dates #

Some facts are owned by the world rather than by you or by your repository. A host moves, an IP is reassigned, a key is rotated and nothing in the project records that it happened. There is no commit to count and no user to contradict it, so the claim reads exactly as current on the day it goes wrong. On 2026-08-11 a session named a box as staging from a 25-day-old note; it had been production for a fortnight.

So infra and access get a review date 30 days out, automatically and say so when stored. Thirty days because a horizon that fires every week is a block everybody learns to scroll past.

$ ns-brain remember --type infra --title "..." --body "..."
  review on 2026-09-20 (default for infra, 30d — --review <date> to
  move it, --no-review if it is permanent)

$ ns-brain verify 31 --source "checked against the live box"
$ ns-brain review --stale-days 90   # tier A/B nobody has re-checked

The brief and its budget #

What arrives at session open is not a dump of the store. It is assembled in priority order and it has a byte budget: it cuts from the least important end, names the sections it cut and prints the command to see the rest. It cannot grow to 73KB, because there is a number stopping it.

That priority is what the budget cuts against, not the order the sections come out in. Guardrails survive first because they bind, then how the user wants to be worked with, then pinned memories, then what is due, then the working tree read live from git rather than from memory, then the inbox, then recent activity — a brief that has to lose something loses it from that end. What is printed first is how the user wants to be worked with, because it governs how everything under it is read. A section with nothing in it does not appear.

Two hooks put it there. SessionStart runs ns-brain wakeup before your first turn. UserPromptSubmit runs ns-brain hook-recall, which matches what you just typed against the store and injects what it finds. Neither depends on the model deciding to look, which is the argument this project makes about memory generally: what depends on being remembered will eventually not be remembered.

What is not a memory #

Three things live in the store and stay outside the memory table on purpose, because folding them in would corrupt ranking.

Tasks, measures and the opt-in domains →

What is worth storing #

The failure mode worth naming is not an empty brain. It is a full one nobody trusts. A competitor's auto-learner produced a store that was two thirds function renames and generic advice, all filed at the widest scope and the machinery was excellent. Contents decide whether anyone reads the output.

Do not store what the repository already records. Code structure, what a function does, a fix that is in the git history, anything in the project's own instructions file. A session can read those and a stale copy of them is worse than no copy.

Do store the reasoning that never got written down: why the obvious approach was rejected, what broke last time and what it looked like, the constraint that is not in any file, the preference the user stated once and expects to hold. BEST-PRACTICES.md is the long version and it ships in the binary.