01 Recall
Choosing which lessons enter your context, on every prompt.
Runs on UserPromptSubmit. Produces the block injected above
your prompt.
Selection is a search, not a question#
A fork of your live session is asked which lessons this next prompt needs. It has the whole conversation — the task, the tool calls, what has already been tried — and it searches the store to answer:
| Step | What happens |
|---|---|
| 1 | .rose/index.md is grepped — one line per lesson: id, family, level, title, tags, gist, path |
| 2 | nodes/ is grepped too, for anything the summaries miss |
| 3 | promising lessons are read, preferring grep -C over opening a long file |
| 4 | the fork returns node ids, and the hook injects those bodies |
Step 2 matters more than it looks. The index holds a title and a one-line summary, so a lesson whose body names the exact command or error string will not match on its summary — and after a verbatim skills migration, most of what a store knows lives in bodies thousands of lines long. Grep over a directory of markdown is cheap, so the selector is told to use it freely, along with anything else the shell offers. The index is the cheapest place to look first, not a boundary on where it may look.
The index is searched, never sent. That is the whole scaling argument. The previous design rendered every apex into one question at ~55 tokens each, which at 5,000 lessons is ~225k tokens per prompt — the thing deciding what to load no longer fits beside the work. A grep costs the same at 5,000 lessons as at 50.
It also fixes something quieter: the walk could only reach a lesson by descending into its apex, so a lesson under an unpromising summary was unreachable. A search does not care what level a lesson sits at.
Bounded by recall.selector_max_tool_calls and
recall.selector_timeout_s, because the user is waiting the
whole time. The tools are read-only by construction — this runs unattended
on every prompt, so it must not be able to edit anything.
When it falls back#
Two things it needs: a session to fork, and a backend that can fork one.
Without either — the first turn of a session, a non-Claude backend,
recall.selector: judge — the old apex walk runs instead. That
walk is also the baseline every arm of rose eval-recall is
measured against, so it is kept deliberately rather than left behind.
A selector that fails outright falls back too. What it must never do is look like "nothing applied": an empty pack caused by an outage says so on the notice line, because a user who reads a broken system as an empty one concludes memory does not work.
Three tiers of re-injection#
A lesson already in context should not be paid for twice — but present and still attended to are different things, since attention over long context decays and an early injection ends up in the middle, where models attend least.
| State | Action |
|---|---|
| never served | full body |
served < stays_fresh_turns ago (8) | skipped — still fresh |
| served longer ago | refreshed with its one-line gist, ~20 tokens |
PreCompact clears the whole record, because compaction may
have deleted the text being tracked.
Also in the pack#
- Sticky patches — dropped claims that previously rescued this node get re-attached, rather than waiting for the same failure to recur.
- Unresolved conflicts — surfaced here, at the moment you are thinking about the topic.