rutter_

Memory-of-use: the mechanics in full

Capture, recall, positions, enrichment, note identity, and the gate — where the record lives, how it accrues, and how it all comes back.

The librarian quietly records what each session decided or produced, lets you recall recent work, annotates search results you've engaged before, and measures whether you actually reach for any of it.

All of this is local-first: nothing leaves your machine, the server runs no AI model (your client is the brain), and it only ever writes inside your vault's _librarian/ overlay and the disposable data/ index.

1. Where the librarian keeps what it remembers

Session memory lives in your vault, as plain, human-readable, git-committable markdown:

<vault>/_librarian/sessions/2026-07-24.md

A note reference is stored as a versioned identity — the vault-relative path plus a content-hash captured as it was read — so the reference still tells you what you saw even after the note changes later.

Which workspace an entry came from

Because a single day can span several efforts, each entry also records where the session happened — automatically, with nothing for you to name or configure:

- id: 20260726T101500123Z
  time: 2026-07-26T10:15:00.123Z
  summary: Shipped workspace provenance.
  refs: []
  workspace:
    cwd: /Users/you/Development/personal/rutter
    project: rutter
    repo: https://github.com/you/rutter.git

Everything about this is best-effort and never blocks a capture:

2. Ambient capture — setting it up

Capture is ambient: it happens at session end with no action from you inside the session. It is wired through a Claude Code Stop hook.

Setup is one step — register the hook (npm run install-hook, or by hand; see Step 6 of getting started). There is nothing to add to your CLAUDE.md. The whole capture contract lives in the server's MCP (Model Context Protocol) instructions — a single source, quoted rather than restated everywhere else — and reaches every client on connect.

How the summary is produced (no AI in the server). The librarian server never summarizes anything — that would be inference, which it does not do. Instead your client writes the one-line summary during the session as a directive, and the hook lifts it out of the transcript verbatim:

<!-- librarian-session {"summary":"Decided to store refs by content-hash; shipped capture.","refs":["Notes/foo.md"]} -->

The style contract — how the summary should be written

A summary is authored by a session that is deep in its own context and read weeks later by someone who has none of it. Left alone, that produces build-log lines full of codenames and version tags that were obvious at the time and are opaque now. So the summary carries a style contract:

Write the line for a smart reader in a hurry who was not in this session: lead with what was decided or produced, use common words rather than session shorthand, and expand or avoid codenames, version tags and abbreviations the session invented (terms your vault itself uses are fine). Aim for about 40 words and stop by 60 — one line, not a build log.

The word budget only works because the trigger agrees with it. The contract asks for a line per separable thing, as each thing lands — a client told it gets one directive at the end of a session packs the whole session into one line, which is exactly the overstuffed entry the budget exists to prevent.

The word budget is advisory, and deliberately so. An over-long summary is stored byte-verbatim like any other — the capture path prints its word count so the drift is visible when it happens, and then stores exactly what it was given. Nothing in the server edits a summary to fit; the alternative, silently truncating, would lose the one copy of what the session meant.

Compare:

✗  Landed HK-7/ambient-splice v0.9.3-rc2 behind FLG_SPLICE_V2; idx@4 -> idx@5,
   backfill gated on FKS_DUAL_READ, ZQ-1197 still open, cutover ETA W31.
✓  Shipped the ambient capture path behind a feature flag, and started the
   session-index upgrade — the data backfill is still switched off.

What the server does about style: nothing at all. It stores the summary byte-verbatim — it never rewrites, shortens, "clarifies", or rejects a line for being dense, and there is no style warning written into your record. That is not an oversight, it is the invariant: judging or rewriting prose is inference, and the server runs no model. A dense summary is a readability problem handled by guidance and by read-time rendering, never a data problem solved by editing your memory.

2a. Position capture — a second, rarer directive

Alongside the session summary, your client can leave a second kind of line: a position directive, emitted only when a session forms, changes, reaffirms, or retires a stance on some topic — expected far less than once per session, against the ~19 session-summary outcomes a day the librarian otherwise sees.

<!-- librarian-position POSITION assert my-topic: I think X because of the meeting notes. -->

2b. Position recall — librarian-positions

librarian-positions answers "what do I think about X, and how did that change?" from the stream position capture writes. It is read-only, and it reads a projection, never the stream files themselves.

Ask one of three ways. They are three different questions, so exactly one argument at a time:

Free-text and note matching scan a topic's entire history, not just its current stance: a topic surfaces if any event in its chain matches, however long since superseded. What comes back is still the current stance — what you searched and what you get are separate knobs. Add chain: true for the whole supersession history, oldest event first.

Every answer says whose it is and when. A recalled stance always carries the date it was formed (its original assert) and, where there is one, the date it was last revised. A reaffirm re-endorses a stance without changing it, so it never moves the revision date — it shows up in the chain instead. The server instructions tell any connected client to report all of this as your recorded position rather than restating it as its own present-tense conclusion.

A retired position is a stub, not a deletion. Where the most recent event for a topic is a retire, the answer is that retire event's own text — typically the reason you withdrew the stance — with retired <date> stated as a retirement, never as a revision. Nothing is removed from the history to produce it: every earlier event is still there under chain: true. And a position asserted again after a retirement is simply live again; a retirement is terminal only while it is the last thing recorded.

Dormant is computed, never stored. A live position nothing has touched for a long while is marked dormant, worked out on every read from the events' own timestamps. Nothing about dormancy is written to disk or to the index, so the rule can change without a migration or a rebuild. A retired position is never marked dormant — withdrawing a stance on purpose is not the same fact as letting one go quiet.

Reindex is the only trigger. The projection tables (position_events, position_refs, positions) are rebuilt wholesale from _librarian/positions/*.md at every npm run reindex, exactly like the note identity projection, and are never patched incrementally. Two consequences, both deliberate:

Calling librarian-positions is logged to _librarian/stateful-use.jsonl under its own kind, and does not count toward the desirability gate.

3. Recall recent work — librarian-recent

Ask "what was I working on lately?" and your client calls the librarian-recent tool. It returns recent session summaries most-recent-first, each with its date, its project, and the versioned provenance of the notes it references:

2026-07-26 10:15:00 [rutter] — Shipped workspace provenance.
   refs: Notes/foo.md@sha256:…
2026-07-25 21:40:00 [novel] — Drafted chapter three.
2026-07-24 09:12:00 — An entry captured before provenance existed.

Filters only ever remove entries: the order you get is the same order you would have got unfiltered.

Old, dense entries still read clearly

Records written before the style contract existed are exactly as dense as the day they were captured, and they are never migrated, edited, or re-summarized — memory-of-use is append-only, and rewriting your own past record to look tidier would be a worse bug than the density.

Instead the server's instructions ask your client to report recalled summaries in plain language for whoever is asking — and that applies to every record, not just new ones. What is on disk is the record; what you are told is the answer. If you want the exact stored text, open the day file in Obsidian, or run npm run recent, which prints the raw stored line.

npm run recent                        # everything, most-recent-first
npm run recent -- 3                   # the 3 most recent
npm run recent -- --days 7            # just the last week
npm run recent -- --project rutter   # just one project

How your client knows to ask

You do not have to tell your client when to use the librarian. The server declares its own MCP instructions, which every client receives on connect: recency questions go to librarian-recent; prior-engagement and content questions go to librarian-search; position questions — "what do I think about X?", "did I change my mind?" — go to librarian-positions; recalled summaries, including old, dense ones, get reported in plain language.

The same instructions carry both directives your client writes, and how a recalled stance must be framed. A session leaves a summary line as each separable thing lands, and leaves a position line as well when it forms, changes, reaffirms, or retires a stance. A stance given back has to be reported as your recorded position, with the date it was formed and, where there is one, the date it was last revised — a retirement named as a retirement, and never restated as the client's own present-tense conclusion.

This matters because guidance in a project's CLAUDE.md only helps in that project. Instructions that ship with the server travel to every client and every directory it's connected from — one install, not one per repo.

The guidance says when the tools are the right answer — it does not tell your client to call them unprompted. Memory stays quiet until it's relevant.

4. Search enrichment — quiet prior-engagement signals

When you run librarian-search and a result is a note a past session referenced, that one result carries a quiet prior-engagement note: what you concluded and when. For example:

1. Orbital telemetry pipeline — reference · evergreen · 2026-05-02
   Notes/foo.md
   …matching snippet…
   ↩ prior engagement 2026-07-22: "Decided foo is the canonical source."

5. Measuring the desirability gate

The memory-of-use layer exists behind a kill gate: does ambient memory-of-use actually pull you toward stateful behavior? The gate target is reaching for a stateful behavior unprompted at least three times a week, for two weeks.

Unprompted names intent, not call origin (spec v3.11.0). "What have I been working on?" answered via librarian-recent is unprompted use even though the model executes the call — the assistant is the delivery mechanism, and memory reached through conversation is still memory reached. What doesn't count is a call made only because the server instructions tell clients to prefer these tools, with no human question behind it.

Every time you invoke librarian-recent, or run a search that surfaces at least one prior-engagement signal, the librarian appends one timestamped event to a local, append-only log (_librarian/stateful-use.jsonl). A single search counts as exactly one event no matter how many signals it surfaced.

librarian-positions writes to the same log, under its own kind, and is excluded from the count — the gate measures whether ambient memory-of-use pulls you toward stateful behavior, and folding a later effort's read surface into that number would answer a different question than the one the gate was set up to ask. The log still records the calls, so they remain available to look at; they just do not move the gate figure.

npm run gate                       # counts across all history
npm run gate 2026-07-13 2026-07-26 # counts within a date range

Output is one line per ISO week, marking weeks that met the target:

stateful-use per ISO week (gate target: >=3):
  2026-W29: 2
  2026-W30: 4  ✓
Honesty note

Classifying an invocation as unprompted — attributing it to a live human question versus standing server instructions alone — is left to manual review; the log captures every invocation with a timestamp so that review is possible. The project is prepared to conclude that the stateful behavior doesn't get reached for.

6. Note identity — surviving a vault rename

A reference records two things about a note at the moment it was touched: its vault-relative path, and a content hash. Rename the note later and the path stops resolving — but the hash is still there, so the librarian can tell what the reference meant even after where it lives has moved.

Every npm run reindex runs an identity pass over every recorded reference whose path no longer resolves — and "resolves" means exactly what it sounds like: a file exists on disk at that path, inside the vault, of any type. A reference can legitimately point at a non-note artifact (a .gitignore, an exported .html, anything under _librarian/ itself), and such a reference is live for as long as that file exists. Only a path that is actually missing enters the rest of this section:

librarian-recent and search enrichment resolve bound references through the ledger at read time: the session record you see still says what you wrote, but the ref line shows the note's current path, or [UNRESOLVED -- candidates: ...] when the librarian genuinely doesn't know. Nothing on this path ever rewrites the stored session entry — resolution happens only when it is displayed.

A reference with no candidates at all (renamed and edited, so no current note matches its recorded hash) is visible on librarian-recent only — that listing is the complete discovery surface for unresolved references; search enrichment is a candidate-anchored extra, not a second complete listing. And a note that is merely a candidate for an old reference is never annotated as if a session had actually engaged it — the two claims are never conflated.

Confirmed bindings are sticky, and disagreements are surfaced, not silently settled. Suppose you confirm Notes/old.md to Notes/keep.md, and later the vault changes so that exact-hash matching would now point somewhere else. The confirmed binding still wins — a later piece of automation never outvotes a human decision — but the disagreement itself is not hidden: both read surfaces render it explicitly: confirmed Notes/keep.md; the hash now matches Notes/stray.md. The only thing that ever moves a confirmed binding is a fresh npm run identity-confirm run.

If a reference stays unresolved, resolve it by hand:

npm run identity-confirm -- Notes/old-name.md Notes/new-name.md

This is a local terminal command only — it is never exposed as an MCP tool, so a connected client can never rewrite what a dead reference means on its own.

The ledger is append-only, same as everything else here: a note renamed more than once gets a fresh binding each time, computed directly against what was originally recorded, with the newest automatic entry winning at read time. Confirmed entries are the one exception to "newest wins": once a pair is confirmed, only a later confirmation moves it. Earlier entries are never rewritten, reordered, or removed. The identity projection tables in the SQLite cache are, like the rest of the index, fully disposable: delete data/librarian.db and reindex, and they rebuild from the vault and the ledger alone.

Guarantees