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.
| Field | What it decides |
|---|---|
| title | Identity, together with scope. Normalised, so re-remembering the same fact refines it in place instead of growing a second copy. |
| body | The fact. Written for a stranger six months out, not for the session that stored it. |
| type | The decay tier, whether a commit can falsify it and whether it gets a review date. |
| scope | Where it applies. Nests on :, flows inward only. |
| confidence | confirmed, reported or assumed. Folds into the ranking tie-break. |
| importance | 1 to 5, 3 is neutral. Stakes, not relevance. |
| source | The citation. Required to store something as confirmed. |
| trust | stated, imported or external. Where the row came from, as distinct from how solid the claim is. |
| source_commit | Stamped automatically on the types a commit can falsify. Drives the freshness verdict. |
| valid_from / valid_to | When the fact became true and when it stops being true, for things with a known window. |
| superseded_by | The correction that retired it. Recall follows this link forward. |
| pin | Always in the brief. |
| session | Which 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.
| Tier | Half-life | Weight | Types |
|---|---|---|---|
| A | never decays | 1.30 | infra access gotcha incident |
| B | never decays | 1.20 | decision constraint commitment |
| C | 60 days | 1.00 | preference relationship |
| D | 4 days | 0.70 | state |
| N | no decay, no weight | 1.00 | knowledge 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.
| Confidence | Weight | Means |
|---|---|---|
| confirmed | 1.15 | Cited. Somebody else could check it: a path, a command, a URL, a commit, or the user saying so. |
| reported | 1.00 | The default. Believed, not proven. |
| assumed | 0.80 | Inferred. 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:
forgetrefuses to delete a memory that supersedes others. Nulling those links republishes facts that were retracted on purpose.--supersedeson a memory another correction already superseded needs--fork-ok, because two live successors to one fact is the thing supersession exists to prevent.
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.
- Measures. A memory answers what is true. A measure answers what the value was and which way it is going. Readings sit outside the search index and outside ranking entirely, so a number can never outrank a fact.
- Tasks. A memory is true until something changes it. A task is owed until it is done. Delivery is pull, on the hooks that already run, so a task on a project nobody opens for a fortnight waits a fortnight.
- Events. The activity log. Append-only, never ranked and the raw material for "what did the last session actually do".
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.