Session Knowledge Closeout
The store compounds only if what goes in is true, general, and non-redundant. A store that only grows becomes a store that lies. Capturing nothing is a perfectly good outcome; capturing five mediocre lessons is a bad one, because retrieved-wrong costs more than retrieved-nothing.
Step 0 — Locate the store and resolve the namespace
The SessionStart hook injects the store.py path into context each session
(look for # Memory skill: invoke ...). Use that exact path. Fallback:
${CLAUDE_PLUGIN_ROOT}/skills/memory/scripts/store.py (Claude Code) or
${ZCODE_PLUGIN_ROOT}/... (ZCode). Set S to it for the commands below.
Never hand-write a namespace. Keys are derived from the git remote, so a
guessed project:<foldername> writes somewhere nothing ever queries. Derive it:
python -c "import sys;sys.path.insert(0,r'$(dirname "$S")');import host;print(host.resolve_namespace('.'))"
Choose the scope deliberately:
| Scope | Use when |
|---|---|
the derived project:... |
the lesson is only true inside this repo (its build, its conventions, its gotchas) |
user:global |
the lesson holds anywhere — a language footgun, a tool behaviour, a workflow rule |
When in doubt prefer user:global for genuinely portable knowledge and the
project key for anything that references this repo's structure. Worktrees and
second clones of the same remote resolve to the same key automatically.
Step 0.5 — Review captured correction candidates
The live-capture hook (capture-correction, issue #47) queues corrections the
user typed mid-session ("no, use X", "don't refactor unrelated code",
"remember: ...") into a namespace-scoped sidecar queue — it NEVER writes the
store (hooks only queue; this skill writes). If this session-start hook surface
mentioned a pending count, or you want to check, review the queue now:
python "$S" queue-list --namespace "<derived namespace>" --json
The items[] shape is a superset of transcript-mining (corrections) items, so
the same review discipline applies. For each item, apply the adapted
claude-reflect rubric — executed by YOU (the session's agent) reading this
skill, never by shelling out to an LLM CLI:
- Keep only corrections reusable across sessions — reject questions, one-time task instructions, context-specific requests, and vague feedback ("fix it", "wrong").
remember:items are always presented, never silently dropped — they are explicit, highest-confidence capture requests.- Trust user corrections as authoritative for model names, API versions, tool availability, and flag values — do not second-guess them against your training data.
- Rewrite accepted items as actionable imperative claims with trigger conditions (Step 2 guidance below).
Accepted items then flow through the existing pipeline unchanged: Step 1
recall-before-write (dedupe/supersede check) → Step 2 add with
--signal user (never higher — SIGNAL_CONFIDENCE maps user → 0.6) and
--source-ref "session:<id>".
Cold-start (bootstrap) candidates: the mine-history command (issue #48)
can also queue mined candidates with source: "history-mine" — salvaged from
HISTORICAL Claude Code transcripts (~/.claude/projects/**/*.jsonl), not
live-captured. They appear in queue-list exactly like live items; apply the
same rubric. Two queue-item kinds exist:
kind: "correction"(source: "history-mine") — a mined user correction. Treat exactly like a live correction above. Itsoccurrencesfield says how many transcripts contained the same (near-identical) message; corrections do NOT carryreview_priority(that flag is exclusive toerror_patternitems), so give the row its honest signal-derived confidence like any other correction.kind: "error_pattern"(source: "history-mine") — a recurring tool error aggregated across sessions (grouped byerror_type+project_folder, countN). It is NOT a corrective claim yet. Rewrite itssuggested_guidelineas a starting draft into alessonwith the ACTUAL trigger condition you observed ("when X fails witherror_type, do Y"), then write it via Step 2add --type lesson. Signal honesty applies: repeated tool errors do NOT automatically qualify astest/compilegrounding — assign the signal the evidence actually supports.review_priority(their ordering weight) must not become the row's confidence; give the row the honest signal-derived one.
Rejections mined into the report (not queued) are context for judging whether a
feature/tool is being misused; they are a #46 report surface, not a queue
candidate. mine-history never writes the store in any mode.
Secrets: an item flagged secret_warning: true carried secret-like text at
capture time. Render the warning to yourself, and write that item via
add --capture-mode auto so any remaining secret-like text is redacted before
it reaches the store (the default manual mode would keep the original wording).
Redaction operator feedback (issue #65, 10.6): when an add/update in
--capture-mode auto reports a redaction warning (the structured
{"type": "redacted", "count": N} warning on --json, or the stderr NOTICE
line), you MUST emit exactly one operator feedback line in your closeout
summary, derived ONLY from the warning count — never from the captured value:
zmem: redacted <N> secret-like value(s) from the captured memory (value not shown).
The stored row shows [REDACTED_SECRET] markers where the values were; the raw
value must never appear in your feedback, the store, or the transcript.
After processing, clear the processed items from the queue (leave explicitly deferred items in place), and prune stale low-confidence candidates:
# remove the specific processed item ids
python "$S" queue-clear --namespace "<derived namespace>" --id <id> --id <id>
# prune stale (past decay) items with confidence < 0.6
python "$S" queue-clear --namespace "<derived namespace>" --drop-stale
Step 1 — Recall before you write
For each candidate lesson, check what the store already believes:
python "$S" recall --query "<the lesson in a few words>" --limit 5 --hybrid --no-bump
Three outcomes, and they lead to different actions:
- Already there, still true → capture nothing. Redundancy dilutes recall.
- There, but this session proved it WRONG or outdated → supersede it (Step 3). This is the step most closeouts skip, and it is the one that keeps the store honest.
- Not there → capture it (Step 2).
--no-bump keeps this audit from inflating retrieval counts (it records a passive
surface on surfaced_count, not a retrieval — issue #21). --hybrid blends vector
and keyword matching so you find near-misses phrased differently from your query.
Decision-point checkpoints (required, #85 direction E)
Recall is not only a closeout step. When ANY workflow this skill feeds is about to run a hazardous git operation, run the matching explicit recall FIRST and treat a hit as blocking review (see the memory skill's "Decision-point checkpoints" section for the full contract):
- before
git stash pop(stash-consume) —python "$S" recall --query "git stash pop foreign stash conflict" - before
git reset --soft(squash assembly) —python "$S" recall --query "git reset soft origin main stale tree" - before
git push—python "$S" recall --query "git push stale tree fetch rebase verify" - before editing a file named by a stored citation/ratchet lesson —
python "$S" recall --query "<path basename> ratchet citation re-pin"
Step 2 — Capture, with a hard bar
A lesson earns a row only if all of these hold:
- A future session facing a different but similar task would act differently because of it.
- It is not already discoverable in the repo (README, CLAUDE.md/AGENTS.md, docstrings). Don't mirror documentation into memory.
- It is not a one-off — not a typo, a transient network failure, or a now-fixed bug in code you already corrected.
- Getting it wrong again would cost real time.
Good: "vec0 KNN is namespace-blind; the recall path now over-fetches by
ZMEM_VEC_NS_OVERFETCH (default 8) and post-filters by namespace in a single
helper shared with the dedup window. The footgun is mitigated, still
over-fetch; consolidate escalates k until a below-threshold row appears,
capped at 500."
Bad: "Fixed the consolidate bug." (narrative, not reusable)
Bad: "Use pytest for tests." (already in the repo docs)
Write the content as an actionable claim, not a story. Include the trigger condition ("when X, do Y, because Z") so recall can match a future situation.
python "$S" add \
--namespace "<derived namespace or user:global>" \
--type <lesson|convention|fact|preference|decision|constraint> \
--content "<specific, actionable, includes the trigger condition>" \
--tags "comma,separated" \
--signal <test|compile|lint|reviewer|user|none> \
--source-ref "session:<session-id>"
Signal honesty is load-bearing — signal sets confidence, confidence gates recall, and only grounded signals are promotable. Never inflate:
| Signal | Means | Only if |
|---|---|---|
test / compile / lint |
a tool verified it | that tool actually ran and passed/failed accordingly |
reviewer |
an independent review confirmed it | a reviewer/critic actually said so |
user |
the user stated it | they actually did |
none |
your own inference | everything else — including "it seems right" |
Re-running add with identical content in the same namespace refreshes the
existing row rather than duplicating it. Paraphrases dedup at ≥0.85 cosine;
the dedup window now uses the same shared ZMEM_VEC_NS_OVERFETCH-based
helper as recall, so a same-namespace paraphrase cannot be crowded out by
other namespaces on a busy multi-namespace store — the footgun is
mitigated, still over-fetch. Step 4's consolidate is the backstop, which
is why it is part of this routine and not optional.
Aim for 0–5 rows. If you have more than five, you are probably capturing narrative or duplicating docs — re-apply the bar.
Step 3 — Supersede what is now wrong
If this session disproved, replaced, or outdated a stored memory, tombstone it. This preserves history while removing it from recall (issue #59):
python "$S" invalidate --id <full-uuid> --reason "<why the fact is no longer true>"
invalidate REQUIRES a reason — it is the preferred form for "this fact is no
longer true" because the correction is auditable. For a revision that keeps the
same topic (wrong details, now corrected) use update instead, which is
append-only and preserves point-in-time recall:
python "$S" update --id <full-uuid> --content "<the corrected lesson>"
update tombstones the old row, creates a NEW live row, and links the new row
back via update_of — --as-of before the update still returns the OLD
content, so the correction never destroys history. Plain supersede remains
for general tombstones (consolidated/pruned rows) where no reason is required.
Then capture the corrected lesson as a new row if update was not the right
shape. A store whose wrong entries are never retired will confidently mislead
a future session.
Step 4 — Consolidate near-duplicates
python "$S" consolidate --dry-run
Review the proposed clusters. Merging is namespace-scoped — it will not fold one
project's memory into another's — but the keeper choice still deserves a
glance. The dry run models the cadence gate, so if it reports would merge N
you can trust a real run will merge; if it reports would skip by cadence gate, the
store was consolidated recently and has not grown enough to warrant another pass.
If the clusters look right:
python "$S" consolidate
A real run that the cadence gate declines prints [zmem] consolidate: skipped by cadence gate (...) (it is never silent) and changes nothing. If you want to
consolidate anyway — e.g. you just imported a large batch of near-duplicates —
pass --force:
python "$S" consolidate --force
Contested clusters are never auto-merged — not even by --force. Similarity
alone cannot tell "always X" from "never X", so when a cluster's members differ
in negation polarity (a negator like never / don't / not / avoid on one side
only) consolidate reports it as a CONTESTED cluster ... NOT merged block and
leaves every member live. Resolve a contested pair with Step 3 (supersede the
wrong side, then recapture the corrected lesson) — do not merge contradictions;
merging would absorb a memory's own refutation into the row it contradicts.
Pass --merge-contested only when you have confirmed the contest is a heuristic
false positive (both sides mean the same thing). For machine-readable output
(including the contested list), pass --json: stdout then carries only the JSON
run report, with human output moved to stderr.
Pruning low-value, never-surfaced, never-retrieved rows is opt-in and destructive-ish; inspect first and only proceed if they are genuinely noise:
python "$S" consolidate --prune --dry-run
retrieval_count = 0 is NOT evidence a memory is unused. Since hook-driven recall is
passive (--no-bump) and records the surface on surfaced_count (issue #21), a memory
surfaced into context on every prompt still shows retrieval_count = 0. consolidate --prune
only retires rows with BOTH retrieval_count = 0 AND surfaced_count = 0 (plus low
confidence, old age, signal = none) — do not hand-prune on retrieval_count alone, and do
not read retrieval_count = 0 as "dead weight".
Step 5 — Review promotion candidates
python "$S" promote --dry-run
Promotion turns a lesson into a SKILL.md in both ~/.claude/skills and
~/.zcode/skills. Be selective — every promoted skill costs trigger-matching
attention in every future session.
Promote only when the lesson is (a) grounded (test/compile/lint),
(b) repeatedly retrieved, and (c) genuinely a reusable procedure rather than a
stored fact. Promote at most 1–3 per closeout, newest-highest-value first,
even when the candidate list is long.
The description is the entire trigger surface — a vague one means the skill
never fires and the promotion was wasted. Always write it yourself:
python "$S" promote --id <uuid> --description "Use when <explicit trigger context> — <what it prevents>" --confirm
--confirm is required to actually write; --dry-run alone changes nothing.
Step 6 — Report
State plainly:
- Lessons captured, with signal and namespace for each
- Anything superseded, and what replaced it
- Whether consolidation merged anything
- Correction candidates reviewed (from Step 0.5's queue): reviewed N, captured M, rejected K (and why)
- Skills promoted (and why those, not the others)
- What you deliberately did not capture, and why
That last line matters most: it is the evidence the bar was actually applied, not skipped.
Never
- Never put secrets, credentials, tokens, or PII in the store — it is local plaintext and the write-time scanner is advisory only.
- Never inflate a signal to make a lesson look promotable.
- Never treat
retrieval_count = 0as evidence a memory is unused — hook-surfaced memories carry their count onsurfaced_count; base prune/rank decisions on both (issue #21). - Never capture in-trajectory refinement ("first tried X, then Y") — capture only the conclusion that would help next time.
- Never write to
tasks/<slug>/*.mdorissue-traces/<issue>/*.mdfrom here; the memory store wraps durable session state, it does not replace it.