Turn mined candidate turns into a small set of durable facts worth remembering.
-
Run the mine. sessions memory mine --repo <path> --json (omit --repo for the current repo; --all mines every repo in the index; add --since-last to mine only transcripts that changed since the previous mine, which is what the weekly summary uses). stdout is a JSON array of candidate records; progress goes to stderr. If the command is not found, say so and stop — do not substitute a search. If the array is empty, say there is nothing to triage and stop; never invent candidates.
-
Confirm fuzzy paraphrase pairs. sessions memory report --repo <path> --json (same scoping flags as the mine) returns a fuzzy array: freshly mined clusters that look like paraphrases of an APPROVED memory but scored below the binary's assert threshold. The binary deliberately does not make this judgment — you do. Present each pair (cluster.text ~ memory.text, with its similarity and session count) and ask confirm/deny, batched with the triage questions in step 7 rather than as a separate interrogation. If the array is empty, move on.
- Confirm →
sessions memory merge <memory-id> <cluster-id> — the approved memory is the canonical, the new phrasing folds into it. This is the same write-back as step 8's clustering merge: future mines recognize the phrasing as accounted for, and the pair stops appearing in the report. If merge exits non-zero on an unknown id, run sessions memory mine --repo <path> first and retry — report reads fresh evidence without persisting it, and merge only knows ids the store holds.
- Deny →
sessions memory snooze <cluster-id>. Reject would kill the fact itself, which is terminal and wrong when the fact is real; snooze is the suppression that persists the dismissal without a verdict. The report's pipeline drops suppressed clusters before classifying, so the denied pair goes quiet on later runs instead of being re-asked every triage. If snooze exits non-zero on an unknown id, run sessions memory mine --repo <path> first and retry, exactly as with merge. A denied pair resurfaces only the way any snooze does — a later merge folding in a new phrasing after the 30 days pass, which the current mine never produces, so in practice the denial holds until the clustering evidence changes.
-
Cluster paraphrases. Group candidates whose texts assert the same fact in different words — "use canary as the base branch" and "we branch off canary" are one memory in two phrasings. A cluster's distinctPhrasings is the number of distinct member texts. Keep the clearest phrasing as the cluster's text; the rest are evidence, not separate memory. Byte-identical repeats were already collapsed upstream, so every member you see is genuinely a different wording.
-
Read what is already binding. sessions memory documented --json lists every statement an agent working here is already told — the global CLAUDE.md, a repo CLAUDE.md or AGENTS.md, and Claude Code's own per-project memory store. Read it before the rubric, because it decides the rubric's first question.
These are read, never written. This store complements those surfaces; it does not replace, edit, or override them.
Other agents' stores are candidates, not gospel. If the user asks to pull in what another agent remembers — pi-hermes-memory's store, Claude's CLAUDE.md files — run sessions memory import --from pi-hermes (or claude, or all) FIRST, before mining: those facts land in the candidate batch you are about to triage, marked by their lack of session evidence. Triage them through the same rubric below; the review_agent_memories MCP tool previews what would import and flags entries that overlap memories already in the store (similarTo), and get_memory_sources inventories what stores exist. A pi-hermes fact scoped to a bare project name arrives unbound — bind it with --scope repo:. when you approve it, or reject it if the project is gone.
-
Apply the generalizability rubric. For each cluster ask: does this fact hold beyond the session it appeared in, and is it not already stated somewhere binding? Propose only the clusters that pass both.
- Passes — standing constraints ("API keys go in the keychain when available"), repo or tooling facts ("this repo branches off canary", "skills can invoke inline scripts"), architectural rules.
- Fails — one-off task instructions ("make it Ideation instead of docs/ideation", "let's do a single PR"), bug reports ("syntax highlighting isn't loading"), anything naming a specific transient artifact.
- Fails as already documented — the fact is already stated in step 4's output. Say which source when you drop it. Claude Code's memory store is the same genre as this one, captured the same way from the same sessions, so this is the expected overlap rather than a rare one — a second copy spends context twice and drifts from the first the moment either is edited.
- Fails loudly — text that reads like a copy-pasted prompt or eval fixture. A suspiciously boilerplate candidate is chaff, not signal.
- Contradicts something binding — do not silently pick a winner and do not approve it. Surface both statements to the user and let them decide, once, here — a contradiction resolved at triage costs one question; resolved never, it costs an agent's confusion in every future session.
- Assign
kind: instruction for "do this / don't do that", information for "this is how the world is".
-
Write the fact, not the utterance. A candidate's text is a verbatim user turn, so it is routinely a question, an aside, or a fragment that only implies the rule. Whenever the clearest statement of the fact is not exactly what the candidate says, approve with --as:
sessions memory approve <id> --as "Do not generate Cursor MCP config — Cursor is not used here."
This matters more than it looks. Without --as, what a future agent receives is the raw turn — and the tool that serves it tells that agent to treat it as binding. "Cursor MCP config? I don't use Cursor" is a fine candidate and a useless instruction. The original is kept as evidence, and the evidence count does not change: your rewrite is not a phrasing the user ever used.
-
Walk triage. One AskUserQuestion per cluster, batching up to 4 independent clusters per call. For each, show the text, the derived scope (repo with its container, or workflow), distinctPhrasings, and the firstSeen–lastSeen range. Options: Approve, Approve as always-on, Reject, Snooze (hide without a verdict).
Offer Approve as always-on only when the fact is a standing constraint an agent must see no matter what it is working on — "canary is the mainline branch", "API keys go in the keychain". Retrieval is topic-conditional by default, so a normal approval means the memory comes back only when the task looks related. Always-on is the exception, and proposing it for everything defeats the filter — mechanically as well as in spirit: the set is hard-capped (20 entries / 2,000 chars), and a grant past the cap is refused.
-
Persist. One command per decision, using the cluster's id:
sessions memory merge <id> <other-id>... — run this first for any cluster with more than one record
sessions memory approve <id> --as "<the fact>" (add --always-on for a standing constraint)
sessions memory reject <id>
sessions memory snooze <id>
Each exits non-zero on an unknown id, so a failure means the decision was not recorded — surface it rather than reporting success.
An approve can also be refused outright, with the reason on stderr. Two cases. A content refusal means the text matches secret material or prompt-injection patterns: reject the record, or — if the underlying fact is real — approve a clean rephrasing with --as that states the fact without the flagged content. An always-on budget refusal means the standing set is full: either approve without the flag (the memory still serves topic-conditionally), or ask the user which existing constraint to demote and free its slot with sessions memory approve <id> --no-always-on. Never silently drop the decision — report what was refused and what you did instead.
Merge is not optional bookkeeping. Your clustering in step 3 exists only in this conversation until you write it back. An id is a hash of that record's own text, so every phrasing is a separate row and nothing else can ever tell them apart. merge folds the members' evidence onto the canonical record — distinct phrasings counted, sessions unioned, date range widened — which is what makes distinctPhrasings mean "how many ways the user has said this" instead of a permanent 1. It is also what lets a snoozed memory come back, and what gives the cross-author quorum something real to count. Pick the clearest phrasing as the canonical, pass the rest as members, then approve/reject/snooze the canonical.
If the user says a fact applies to a set of related repos rather than just this one, assign a project group: sessions memory approve <id> --scope group:<name>. Only ask when they volunteer it — group membership comes from ~/.local/share/sessions/groups.json, which the user maintains by hand, and a group they have not configured there is silently never returned. Say so if you assign one.
An imported record needs a repo before it means anything. A record whose scope is repo with an EMPTY key came from someone else's bundle: export strips the local path, and retrieval skips a keyless repo memory rather than letting it match every repo. Approving it as-is succeeds and changes nothing. Bind it in the same command: sessions memory approve <id> --scope repo:. for this repo, or --scope repo:<path> for another (the path is resolved to its repo container, so a worktree or subdirectory works). If the fact is clearly not repo-specific, say so and let the user decide instead of guessing a repo.
-
Report. Proposed count, approved count, and the ratio. That ratio is the number the precision goal is measured on, so state it plainly even when it is bad.
Report the untriaged backlog too, from sessions memory pending. You triaged one batch; the store can hold thousands of candidates from earlier mines, and a summary that says "triage complete" while thousands wait is the one line in this skill that can be actively false. Say "N approved of M proposed this batch; K candidates still awaiting triage."
Also report how many clusters you dropped as already documented, and where they were already stated. That number is the measure of whether this store is complementary or is quietly duplicating what Claude Code already injects — the one thing no test can check.