| name | closeout |
| description | End-of-session knowledge closeout for the zmem store. Recall what is already known, capture only generalizable lessons with honest signals, supersede anything this session proved WRONG, consolidate near-duplicates, and review skill-promotion candidates. Use when the user says "closeout", "wrap up", "end of session", "capture what we learned", or after finishing a substantial task whose lessons would otherwise be lost. Not for mid-task capture — the PostToolUseFailure and Stop hooks already handle that.
|
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. Its occurrences field says how
many transcripts contained the same (near-identical) message; corrections do
NOT carry review_priority (that flag is exclusive to error_pattern items),
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 by error_type + project_folder, count
N). It is NOT a corrective claim yet. Rewrite its suggested_guideline as a
starting draft into a lesson with the ACTUAL trigger condition you
observed ("when X fails with error_type, do Y"), then write it via Step 2
add --type lesson. Signal honesty applies: repeated tool errors do NOT
automatically qualify as test/compile grounding — 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:
python "$S" queue-clear --namespace "<derived namespace>" --id <id> --id <id>
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 = 0 as evidence a memory is unused — hook-surfaced
memories carry their count on surfaced_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>/*.md or issue-traces/<issue>/*.md from here;
the memory store wraps durable session state, it does not replace it.