Skip to content

Latest commit

 

History

History
301 lines (272 loc) · 21.7 KB

File metadata and controls

301 lines (272 loc) · 21.7 KB

REVIEW — The Memory Review Ritual

When and how to recompute usage metadata, reshuffle tiers, and keep memory/continuity.md lean. Applies the rules in DECAY.md.

Like DECAY.md, this doc is generic and ships into every enabled repo (installed at the repo root by ENABLE.md): the ritual runs inside the repo as part of the normal session routine, so the agent needs it locally.


When it runs

Three triggers:

  1. Cadence — when sessions_since_last_review ≥ review_every (from memory/decay-policy.md). Checked during the post-session update.
  2. On command — the user says "review memory" / "compact memory".
  3. Size — when the live layer (memory/continuity.md + the memory/open-threads/ files) holds more than continuity_max_facts decaying facts/threads (the primary signal — a count, immune to verbosity and session velocity), or continuity.md exceeds continuity_max_lines (a coarse backstop).

The triggers don't rely on the agent remembering. memory-lint surfaces all three as advisories — [review-overdue] (cadence) and [continuity-bloat] (facts/lines) — so a lapsed review shows up on every lint run + the forge CI floor (GitHub Actions, GitLab CI, or Azure Pipelines), not just when someone thinks to check. (Added v4.24.0, after a real product repo ran 61 sessions and never archived because the cadence trigger only ever fired in the agent's head.)

Within a review, one more cadence is checked — invariant verification: when sessions_since_last_invariant_check ≥ verify_invariants_every, the review prompts a human to re-confirm the never-decay facts (routine step 7). It rides on the review, so it never fires more often than reviews do.

Also within a review — stalled-thread gating (v4.40.0): every unchecked Open Thread not referenced for more than thread_stale_window sessions is listed in one human closure gate (routine step 8) — a stalled thread is a signal for closure, and the decision is the owner's. Like invariant verification it rides on the review; between reviews memory-lint surfaces each stalled thread as [thread-stale], so the condition cannot hide.

last_review and last_invariant_check are tracked in continuity.md Project State (each a YYYY-MM-DD plus the session file it last ran through).

Inputs

  • memory/continuity.md — facts + metadata
  • memory/open-threads/ — Open Threads, one file per thread (v4.39.0)
  • memory/decay-policy.md — windows + triggers
  • memory/sessions/ — the event log; read each ## Memory References
  • memory/archive/ — cold storage + INDEX.md

Run reviews serialized. Start from an up-to-date default branch and commit the result promptly, before other memory work: the metadata refresh rewrites many footers at once, and running it on a stale branch tangles mechanical churn with teammates' in-flight substantive edits — the worst conflict shape. (Reviews are cadence-gated and effectively single-actor; this just makes that explicit.)


The routine (incremental — the normal path)

  1. Gather the window. List session files after last_review. Read each one's ## Memory References.

  2. Apply events. For every id named:

    • Referenced / Created: increment uses; set last_used to the latest session date that names the id.
    • Reactivated: if the id currently lives in the archive, move it back into the live layer as active (a fact into continuity.md; a thread back to its own memory/open-threads/thread-<id>.md), then apply the Referenced bump.
    • Superseded: <old> → <new> (or <old> (invalidated)): confirm the old fact is marked tier: superseded + superseded-by: <new> (the agent marks it at write time — DECAY.md §9; set it here if missing) and the successor carries supersedes: <old>.
  3. Re-tier every fact. For each fact in continuity.md and each thread file in memory/open-threads/, compute sessions_since_last_used (count files — DECAY.md §4) and apply the DECAY.md §5 rules in order. Record each tier change.

    Preferred — steps 2–3 are pure arithmetic; run the refresh-metadata skill (agent-skills/refresh-metadata/; Python or Node) to recompute every fact's last_used / uses / tier from the reference log and write the footers back (--dry-run to preview). This is the deterministic "full rebuild" below, made runnable — don't update 30 footers by hand (agents reliably skip this pass, leaving stale tiers; memory-lint flags the gap as [stale-metadata]). It only re-tiers — core/superseded are untouched and it never archives.

  4. Archive. Facts that resolve to archived (faded) or superseded (false):

    Preferred — use the archive-fact skill (agent-skills/archive-fact/; Python or Node) to perform the move deterministically. It reads continuity.md into memory and writes once, so the truncate-before-read trap can't recur. You still decide which ids to archive; the helper does the move: python3 agent-skills/archive-fact/scripts/archive-fact.py --id <id> [--id <id> …] [--reason "superseded by <new>"] (--dry-run to preview). It refuses if an id is missing or already archived (all-or-nothing). By hand (no runtime — use append-mode / read-into-variable, never a truncate-first write, see Safety):

    • append the fact with its metadata comment to memory/archive/<YYYY>-Q<n>.md under a dated heading, noting the reason — faded or superseded by <new-id>,
    • add/refresh its line in memory/archive/INDEX.md (id — one-line — <reason> — <quarter file>),
    • remove it from continuity.md. Superseded facts archive promptly — no archive_window wait, since they are false, not merely stale — and carry their superseded-by link into the archive.
  5. Sweep completed threads. - [x] Open Threads whose completion is older than archive_window sessions move to the archive the same way (usually the biggest lean-up) — for a thread file the sweep moves its block to the quarter file + INDEX.md and deletes the file (archive-fact handles thread files; the move preserves everything). Keep recently-completed threads for context — but condense them to stubs (v4.38.0): while a completed thread waits out archive_window, its record is 3–6 lines — outcome, PR/commit/release refs, one durable lesson, and its origin: pointer. Trim prose only; never edit the id or footer metadata. Nothing is lost — the full narrative lives in the thread's origin session log (immutable), and [closed-thread-bloat] is the advisory that measures this. A condensed thread later archives as its stub; retrieval follows origin: to the full record. Completion age is the closing reference (v4.41.0). A thread's close record is the completion event: the session that closes it declares the id under ## Memory References, and sessions_since_last_used then measures completion age for this sweep. An [overdue] on a thread closed within the window is therefore a missed declaration, not decay — check the closing commit, declare the id retroactively in this session's log, and let the counter reset rather than sweeping a record a human just wrote (the pre-commit [undeclared-reference] advisory catches the omission at commit time).

  6. Verify archival (required — guards against a miscounted sessions_since_last_used). Archival is the costliest error, and "sessions since last used" is the easiest count to get wrong. A "use" is an id under a session's ## Memory References (§2 / DECAY.md §2) — not a passing mention in prose. Verify against that definition:

    • Preferred — run the memory-lint skill (agent-skills/memory-lint/; Python or Node, whichever the machine has). It recomputes sessions_since_last_used from ## Memory References only, exits non-zero if any archived-as-faded fact was actually referenced within archive_window (⇒ reactivate it), and confirms no id lives in both continuity.md and the archive. The script counts, so it is immune to the prose trap below. (No runtime? SKILL.md says install Python or Node — don't hand-count if you can avoid it.)
    • By hand (fallback): for each fact you archived as faded, grep the last archive_window session files for its id — but only count a hit that sits inside a ## Memory References block. A hit outside it — e.g. a prior review summary (## Memory Review) that names the id while recording its decay status, or a ## What happened mention — is not a use; ignore it. (A raw full-text grep that counts such mentions creates an archival livelock: every review that defers a fact re-names it, so the guard never clears — the ot-review-step6-prose bug, same class as the v4.10.1 prose-vs-heading false positive.) If a genuine ## Memory References hit appears, your count was wrong — do not archive it (it is still active / archive-candidate); move it back into continuity.md.

    Either way, then confirm no id lives in both continuity.md and the archive (a fact exists in exactly one place). Record the result in the summary. (Superseded facts are exempt — they archive on truth-state, not recency.)

    Declaration gaps (facts, v4.42.1; commit check v4.42.2, refined v4.42.3) — the fact-level twin of step 5's thread rule. Both checks above count only declared uses, and a fact consulted to make a decision leaves no edit to the fact, so for each fact archived as faded, look for undeclared use of its subject — the code, rule or contract it records, not its id — in two places:

    • The window's commits — start here. The window's work begins where the log before it ends (the newest log older than the archive_window logs), so list the commits after that log's full timestamp that touch a path the fact names: git log --since=<YYYY-MM-DDTHH:MM:SSZ> --format='%h %s' -- <paths> (its backticked paths — .agent/schema.md asks a fact that governs files to name them so; a <placeholder> becomes *). Take the timestamp from the log's file name (2026-09-30-164034 → 2026-09-30T16:40:34Z) and never pass a bare date: git completes one with the current time of day. Leave out a hub — a path touched by more than 5% of all commits and by more than 5% of the window's commits, at least two of them (git rev-list --count [--since=<timestamp>] HEAD -- <path> against git rev-list --count [--since=<timestamp>] HEAD): AGENTS.md, UPGRADE.md or memory/continuity.md would flag every fact. Busy over all history is not enough: a path that was busy once and is quiet now — a retired module kept as a placeholder that only a release sweep touches — carries exactly the rare commit this check exists for, and a path the window touched once is never a hub, since one candidate is cheap to check. Each commit maps to the session log it carries, else to the log carried by the next commit in history that carries one — never the next log by file name: a log is named when first written and may be enriched by later commits. Only the window's logs count.
    • The window's logs. Search them for the subject's distinctive terms (backticked identifiers, words from its bold title), skipping ## Memory Review and ## Memory References blocks.

    Sessions describe their work in prose, not by the paths a fact names, so the commits are the reliable signal: on this repo's own history they found both sessions behind the wrongful git-hook-fragment-dispatch archival, the log search neither (RFC-0006). Then apply the decision test to every hit — did the session rely on what the fact states; would it have decided differently without it? If one did without declaring the id, the fade is a declaration gap, not disuse: move the fact back as above, name it under this review's ## Memory References (re-affirmed, citing the session that relied on it) so its count resets, and note the reversal in the ## Memory Review block. Two things are not use. A mention is not an exercise: prose that names the subject or the id — a prior review summary, a decay note, a plan never acted on — is not evidence (the ot-review-step6-prose livelock). And a fact that records an event — a release shipped, work completed — is not kept alive by later work on the same code: its substance lives in the changelog, and only a fact stating a live rule, decision or contract can be relied on this way. The read is judgment and never counts on its own; only the declaration it prompts does. (Field report: mercury-composable and mercury, 2026-10-04 — three in-use facts archived in five weeks; this repo's git-hook-fragment-dispatch was a fourth.)

  7. Verify invariants (cadence). If sessions_since_last_invariant_check ≥ verify_invariants_every (or last_invariant_check is unset and that many session files exist), raise one Open Thread listing every never-decay fact — tier: core, everything under ## Architectural Invariants, and the Vision (memory/vision.md) — for a human to re-confirm: - [ ] Re-verify invariants (due): confirm <id>, <id>, … and the Vision still hold, or supersede any that don't (DECAY.md §9). The review never auto-invalidates an invariant — it only prompts; the human confirms (checks the thread off) or supersedes the false ones (§9). Then set last_invariant_check to today + the latest session file. (Never-decay ≠ never-checked.) If not due, skip this step.

  8. Gate stalled threads (v4.40.0). For each unchecked Open Thread whose sessions_since_last_used exceeds thread_stale_window (a never-referenced thread counts from created; memory-lint lists them as [thread-stale]), raise one Open Thread — the human closure gate — naming every stalled thread with its count: - [ ] **Close stalled threads (due):** <id> (N sessions), <id> (N sessions) — for each, close it, or re-affirm it (DECAY.md §6) (id close-stalled-threads-<YYYYMMDD> — an id names the thing, never its kind, DECAY.md §1; a gate raised before v4.41.1 keeps its ot- id — in its own thread-<id>.md; if an unchecked gate already exists, add the new ids to it rather than raising a second). Re-read each listed thread's body with the human — strike items that shipped elsewhere — then the human decides per thread: close it (- [x] + a 3–6-line close record; anything undelivered is recorded as deliberately dropped, never silently lost — a (blueprint) gap closing this way is an altitude decision, DECAY.md §12), or re-affirm it (name it under this session's ## Memory References with the reason it stays open — the only thing that resets its count; re-affirmation is the exception, not the default). Declare every disposition: a closed thread is named under this session's ## Memory References too — its close record is the completion event that starts the sweep clock (list it as closed); only inspecting a stalled thread without a decision is not a use. The review never closes a thread itself — a stalled thread is a signal for closure, and the decision is the owner's (never-pick-a-winner). Check the gate off once every listed thread is dispositioned; the sweep archives it later like any completed thread. If nothing is stalled, skip this step.

  9. Stamp. Set last_review to today + the latest session file name.

  10. Summarise. Write a ## Memory Review block into this session's log — list the archived / swept / reactivated ids there, in that block. ⚠️ Do not list archived ids under ## Memory References. Archiving a fact is not a "use." memory-lint (and the by-hand check) count any id under ## Memory References as referenced this session — so naming an archived id there sets its sessions_since_last_used to 0, re-arms the over-archival guard, and forces a false reactivation (it will demand you move the fact you just archived back). ## Memory References records only the ids you genuinely relied on / created / reactivated this session (e.g. a new (knowledge-harvest) or invariant-reverify thread you created) — and a fact kept after a declaration gap (step 6), which was moved back, not archived. The ## Memory Review block is not parsed as references, so archived ids belong there. (Learned the hard way: a review summary that listed its archived ids under ## Memory References threw 13 spurious over-archived ERRORs.) Inspecting a stalled thread as gate evidence is not a use either — list a stalled thread here when the human acted on it: re-affirmed (that entry is the reset, DECAY.md §6) or closed (the close record is the completion event — list it as closed); a thread merely read and left as it was is not listed. The gate thread you raised is listed as Created. (v4.41.0 corrected the v4.40.0 wording, which named only re-affirmation and led a field session to leave two closures undeclared — [undeclared-reference] now catches that at commit time.)

Contradiction backstop. The review reads every fact anyway, so give them a quick contradiction scan — the write-time check (DECAY.md §10) may have missed one, or two facts may have drifted into conflict over time. Surface any conflict as a - [ ] Contradiction: <fact> conflicts with <id> — resolve (supersede one, or reconcile) Open Thread; never silently reconcile or pick a winner. Extend the same scan up the altitudes (VBDI, DECAY.md §12): flag any Implementation / Design / Blueprint item that no longer serves the one above it — - [ ] Drift: <item> doesn't serve <id>. Extend it to each unchecked thread's body as well (v4.40.0): strike items that shipped elsewhere; a thread whose every item has verifiably shipped closes as a normal completion, and one with anything left undelivered goes to the human closure gate (step 8) — never silently dropped. A thread's content carries no metadata, so only a read catches this.

Smoke test. A review is also a natural time to run memory/smoke-test.md — a quick manual check that memory still answers the orientation questions a newcomer would ask.

Full rebuild (the ground-truth path)

Because metadata is derived, you can discard stored uses/last_used/tier and recompute everything from scratch by scanning all session logs' ## Memory References. Use this to repair drift, after heavy manual edits, or if reviews were skipped for a long stretch. The result is deterministic and reproducible by any agent. The same scan repairs each fact's origin — the earliest session whose ## Memory References names the id under Created (DECAY.md §11).

Reactivation

When an archived id is named in a session (Referenced/Reactivated):

  • move the fact from its archive/<quarter>.md back into the live layer (continuity.md; a thread back to memory/open-threads/thread-<id>.md),
  • set tier: active, refresh last_used, increment uses,
  • remove or annotate its archive/INDEX.md line,
  • note it in the review summary.

This two-way movement is what keeps the system smart rather than merely lossy. Superseded facts are the exception — they are terminal (DECAY.md §9) and are not reactivated by a reference; only a human can reverse a supersession by hand.


Review summary format

## Memory Review (2026-06-20, through 2026-06-20-141503)
- Reactivated:   1  (drizzle-over-prisma — referenced today after 9 dormant sessions)
- Superseded:    1  (rest-versioning-v1 → rest-versioning-v2; archived flagged superseded)
- Archived:      3  facts → memory/archive/2026-Q2.md (faded)
- Swept threads: 4  completed Open Threads → archive
- Archive-verify: pass (no archived id appears in the last archive_window sessions; no id in both places)
- Tier changes:  6  (2 working→active, 1 active→archive-candidate, 3 →archived)
- Invariants:    not due (next re-verify in 6 sessions)   # or: "prompted — 2 invariants up for re-confirmation"
- Stalled threads: 2  (gate close-stalled-threads-20260620 — legacy-soap-adapter 57, csv-bulk-import 44)   # or: "none"
- Promoted core: 0  (auto-core off; core is human-set)

Safety

  • Never delete a fact — archiving is a move, not a removal.
  • Never truncate a memory file when scripting the move. To append to the archive / INDEX.md, use append mode (>> file, or open(f, "a")); to rewrite continuity.md, read the whole file into a variable, then write. Never open(f, "w").write(open(f).read() + …) — opening in "w" truncates f to empty before the inner read runs, so it silently wipes the file (this exact trap has wiped a version.md stamp and this repo's archive — 50 facts → 6 — once each). Same caution for any sed -i-style in-place rewrite.
  • After any scripted memory mutation, run memory-lint. It catches a truncation immediately — the archived/continuity count drops and supersession links dangle. Every memory file is git-tracked, so git checkout HEAD -- <file> recovers cleanly. Treat the lint as the deterministic gate on your own edits, not just on the review's arithmetic.
  • Never overwrite a hand-set tier: (especially core) or a hand-set id.
  • Never edit past session logs — they are the immutable ledger this ritual reads.
  • Stay within the repo's memory/ and archive/; never touch ~/, ~/.claude/, Application Support, AppData, or system paths.