When and how to recompute usage metadata, reshuffle tiers, and keep
memory/continuity.mdlean. Applies the rules inDECAY.md.Like
DECAY.md, this doc is generic and ships into every enabled repo (installed at the repo root byENABLE.md): the ritual runs inside the repo as part of the normal session routine, so the agent needs it locally.
Three triggers:
- Cadence — when
sessions_since_last_review ≥ review_every(frommemory/decay-policy.md). Checked during the post-session update. - On command — the user says "review memory" / "compact memory".
- Size — when the live layer (
memory/continuity.md+ thememory/open-threads/files) holds more thancontinuity_max_factsdecaying facts/threads (the primary signal — a count, immune to verbosity and session velocity), orcontinuity.mdexceedscontinuity_max_lines(a coarse backstop).
The triggers don't rely on the agent remembering.
memory-lintsurfaces 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).
memory/continuity.md— facts + metadatamemory/open-threads/— Open Threads, one file per thread (v4.39.0)memory/decay-policy.md— windows + triggersmemory/sessions/— the event log; read each## Memory Referencesmemory/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.)
-
Gather the window. List session files after
last_review. Read each one's## Memory References. -
Apply events. For every id named:
Referenced/Created: incrementuses; setlast_usedto the latest session date that names the id.Reactivated: if the id currently lives in the archive, move it back into the live layer asactive(a fact intocontinuity.md; a thread back to its ownmemory/open-threads/thread-<id>.md), then apply the Referenced bump.Superseded: <old> → <new>(or<old> (invalidated)): confirm the old fact is markedtier: superseded+superseded-by: <new>(the agent marks it at write time —DECAY.md§9; set it here if missing) and the successor carriessupersedes: <old>.
-
Re-tier every fact. For each fact in
continuity.mdand each thread file inmemory/open-threads/, computesessions_since_last_used(count files —DECAY.md§4) and apply theDECAY.md§5 rules in order. Record each tier change.Preferred — steps 2–3 are pure arithmetic; run the
refresh-metadataskill (agent-skills/refresh-metadata/; Python or Node) to recompute every fact'slast_used/uses/tierfrom the reference log and write the footers back (--dry-runto 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-lintflags the gap as[stale-metadata]). It only re-tiers —core/supersededare untouched and it never archives. -
Archive. Facts that resolve to
archived(faded) orsuperseded(false):Preferred — use the
archive-factskill (agent-skills/archive-fact/; Python or Node) to perform the move deterministically. It readscontinuity.mdinto 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-runto 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>.mdunder a dated heading, noting the reason —fadedorsuperseded 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 — noarchive_windowwait, since they are false, not merely stale — and carry theirsuperseded-bylink into the archive.
- append the fact with its metadata comment to
-
Sweep completed threads.
- [x]Open Threads whose completion is older thanarchive_windowsessions 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.mdand deletes the file (archive-facthandles 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 outarchive_window, its record is 3–6 lines — outcome, PR/commit/release refs, one durable lesson, and itsorigin: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 followsorigin: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, andsessions_since_last_usedthen 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). -
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-lintskill (agent-skills/memory-lint/; Python or Node, whichever the machine has). It recomputessessions_since_last_usedfrom## Memory Referencesonly, exits non-zero if any archived-as-faded fact was actually referenced withinarchive_window(⇒ reactivate it), and confirms no id lives in bothcontinuity.mdand the archive. The script counts, so it is immune to the prose trap below. (No runtime?SKILL.mdsays 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_windowsession files for its id — but only count a hit that sits inside a## Memory Referencesblock. A hit outside it — e.g. a prior review summary (## Memory Review) that names the id while recording its decay status, or a## What happenedmention — 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 — theot-review-step6-prosebug, same class as the v4.10.1 prose-vs-heading false positive.) If a genuine## Memory Referenceshit appears, your count was wrong — do not archive it (it is stillactive/archive-candidate); move it back intocontinuity.md.
Either way, then confirm no id lives in both
continuity.mdand 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_windowlogs), 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.mdasks 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>againstgit rev-list --count [--since=<timestamp>] HEAD):AGENTS.md,UPGRADE.mdormemory/continuity.mdwould 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 Reviewand## Memory Referencesblocks.
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-dispatcharchival, 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 Reviewblock. 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 (theot-review-step6-proselivelock). 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'sgit-hook-fragment-dispatchwas a fourth.) - Preferred — run the
-
Verify invariants (cadence). If
sessions_since_last_invariant_check ≥ verify_invariants_every(orlast_invariant_checkis 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 setlast_invariant_checkto today + the latest session file. (Never-decay ≠ never-checked.) If not due, skip this step. -
Gate stalled threads (v4.40.0). For each unchecked Open Thread whose
sessions_since_last_usedexceedsthread_stale_window(a never-referenced thread counts fromcreated;memory-lintlists 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)(idclose-stalled-threads-<YYYYMMDD>— an id names the thing, never its kind,DECAY.md§1; a gate raised before v4.41.1 keeps itsot-id — in its ownthread-<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 Referenceswith 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 Referencestoo — 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. -
Stamp. Set
last_reviewto today + the latest session file name. -
Summarise. Write a
## Memory Reviewblock 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 Referencesas referenced this session — so naming an archived id there sets itssessions_since_last_usedto 0, re-arms the over-archival guard, and forces a false reactivation (it will demand you move the fact you just archived back).## Memory Referencesrecords 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 Reviewblock is not parsed as references, so archived ids belong there. (Learned the hard way: a review summary that listed its archived ids under## Memory Referencesthrew 13 spuriousover-archivedERRORs.) 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.
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).
When an archived id is named in a session (Referenced/Reactivated):
- move the fact from its
archive/<quarter>.mdback into the live layer (continuity.md; a thread back tomemory/open-threads/thread-<id>.md), - set
tier: active, refreshlast_used, incrementuses, - remove or annotate its
archive/INDEX.mdline, - 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.
## 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)- 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, oropen(f, "a")); to rewritecontinuity.md, read the whole file into a variable, then write. Neveropen(f, "w").write(open(f).read() + …)— opening in"w"truncatesfto empty before the inner read runs, so it silently wipes the file (this exact trap has wiped aversion.mdstamp and this repo's archive — 50 facts → 6 — once each). Same caution for anysed -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, sogit 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:(especiallycore) or a hand-setid. - Never edit past session logs — they are the immutable ledger this ritual reads.
- Stay within the repo's
memory/andarchive/; never touch~/,~/.claude/, Application Support, AppData, or system paths.