# Memory Lifecycle Contract

The shared contract for how built-in memory stays bounded as it grows.
Every component that reads or writes the memory dir follows this:
`validate-memory.mjs`, `write-memory-file.mjs`, the `/cc-remember` and
`/memory` skills, the `memory-index-guard` hook, and (Stage 2) the
lifecycle verbs. Full design + cabinet critique: see
`.claude/plans/cross-store-memory-redesign.md` (act:02d15a70 = Stage 1,
this contract; act:45fb9169 = Stage 2, gated).

## The core principle

`MEMORY.md` is loaded **in full at every session start** — it competes
for the model's context window, so it has a hard budget (≤200 lines /
≤25KB). The disease this contract cures: the old rule required the
eagerly-loaded index to enumerate EVERY memory file (one line each), so
its size tracked total memory count and it overflowed forever — the
prune→overflow→prune treadmill.

The fix decouples the eager index from total volume: `MEMORY.md` is a
bounded **working set**, not a complete catalogue. Total memory can grow
without bound (disk and search both scale for decades); only what loads
eagerly is bounded. This is the same shape watchtower threads already
use — surface the current cursor, leave the history on disk.

## The three blocks of MEMORY.md

A well-formed `MEMORY.md` working set has three kinds of content:

1. **Pinned tier (decay-exempt).** Load-bearing knowledge a fresh
   session needs surfaced *unbidden* — hard constraints, identity, hard
   preferences, active-project pointers. These are valuable precisely
   because they are stable and often OLD; they must never decay out on
   recency. The defining test: *would a session that has never seen this
   fail without it, and would it not know to go looking?* (e.g. "CC
   hooks read tool input from stdin, not `$CLAUDE_TOOL_INPUT`".) Mark a
   pinned entry with `pinned: true` in its file frontmatter (the Stage-1
   default authority; revisit in Stage 2).

2. **Recency block.** The recent window of curated entries, newest
   first, filling the remaining budget to roughly 80% of the cap.
   Recency is a fine proxy for "likely relevant next session" for
   episodic material — and a BAD proxy for load-bearing constraints,
   which is exactly why they go in the pinned tier instead.

3. **Region pointers.** The cue-based-retrieval tier (see below) that
   lets everything outside the working set stay reachable without a line
   each.

## Reachability replaces enumeration

The validator invariant is no longer "every file has its own index
line." It is: **every on-disk memory file is reachable from
`MEMORY.md`** — by one of:

- a direct working-set or `MEMORY-archive.md` index line, OR
- a **region pointer** whose glob matches the file.

A file that is none of these is an orphan and fails validation.
"Present on disk" alone is **not** reachable — that test would be
vacuous (every file is grep-able), which would silently delete the
guarantee rather than relax it. The point of relaxing enumeration is to
allow *bounded* indexing (a region covers N files with one line), not to
allow invisible files.

## Region pointers

A region pointer declares that a whole class of files is reachable
without enumerating them. Format — the **backtick-quoted glob is the
only part the validator reads**; the rest is free-text guidance:

```
- region `lesson_*.md` → older lessons; grep the memory dir
- region `session_summary_*.md` → past session records; grep by date
```

Rules:

- **Globs use `*` and `?` only**, matched against the bare filename.
- **Region pointers are DERIVED, never authoritative.** They are
  regenerated from what's actually on disk (by hand in Stage 1, by the
  consolidation pass in Stage 2). A pointer whose glob matches **zero**
  files on disk is a stale, confidently-wrong retrieval cue — *worse
  than no map* — and **fails validation**. Fix the glob or remove it;
  never hand-edit a pointer to claim a region that isn't there.
- **Region pointers are for recoverable history you'd think to search**
  (old session narratives, cooled lesson archives). They are the WRONG
  home for latent constraints — those stay in the pinned tier, eagerly
  present, because their value is that you retrieve them *without*
  knowing to look.

## Spill-rate instrumentation (the Stage-2 gate)

`validate-memory.mjs` appends a working-set pressure sample to
`<memoryDir>/.memory-pressure.jsonl` (a dotfile — invisible to the
orphan scan) at most once per day, only when utilization is ≥90% of the
budget. This measures how often the working set actually runs near its
limit, so Stage 2 (the lifecycle verbs + a background forcing function)
is built only if the data shows recurring pressure on authored files —
not on a felt "it keeps overflowing" metaphor. Stage 1 alone stops the
acute treadmill; the sampler decides whether Stage 2 is warranted.

## Stage 2 (designed, gated — NOT yet built)

The lifecycle verbs and their forcing function are specified in the plan
but gated on the spill-rate data. When built they will carry these
already-decided constraints (recorded here so Stage 1 doesn't paint them
into a corner):

- **decay** — drop an entry from the working set; the file stays
  reachable (via a region pointer or the archive). Reversible.
- **consolidate** — merge a cluster into one gist that ALWAYS keeps a
  lossless `consolidated_from:` provenance pointer (lossy = dropped from
  the working set, never erased from disk). A cluster containing a
  pinned/load-bearing entry files to the inbox; it never auto-applies,
  because the *gist* becomes the retrieved truth and a wrong gist erases
  a load-bearing detail.

  **Evidence this verb has real work (act:421a8ab2, N5 of
  grp:retro-remeaning):** two blind reads of the maginnis durable-memory
  corpus (2026-07-19) found ~47-60% of sampled files involved in a hard
  duplicate — overwhelmingly outage-recovery extractions and the
  2026-07-14 read-pass, both bulk-written without a MEMORY.md index line
  (region-pointer-covered instead), which made the dedup structurally
  blind to them. `templates/scripts/memory-consolidation-scan.mjs` is the
  dry-run PROPOSAL half of this verb, built ahead of the full Stage 2 gate:
  it reuses the (now overlap-coefficient, dual-scored — see
  `proposeFolds` in `watchtower-queue.mjs`) fold detector over an actual
  memory directory's files instead of the pending inbox queue, and reports
  candidate pairs with a `consolidated_from` pointer. It never merges or
  writes — the human decides. Running the full MERGE half (rewriting
  files, folding gists) is still gated on the spill-rate data per this
  contract; the scan only proposes what the merge would act on.
- **supersede** — mark an entry false/obsolete so retrieval stops
  surfacing it as live truth (kept on disk for audit, with a reverse
  breadcrumb in its successor). Deferred until the frontmatter-parsing
  question is resolved (line-scan vs. a deliberately-added dependency —
  the repo currently refuses a YAML parser; see
  `lesson_parsefrontmatter_flow_sequences`). supersede is a reference
  graph: dangling targets, cycles, and re-pointing working-set/region
  references to the superseded slug must all be validated.
- **Cross-store fold** — for session-summaries carrying design
  reasoning, the durable target is a committed `.claude/methodology/`
  gist, NEVER the thread cursor (which holds understanding, not
  narration) and NEVER a delete-in-the-same-pass (the memory dir and
  threads are both local-only/ungit'd; append-verify-then-mark
  `folded_into:`, reclaim in a separate idempotent sweep).
- **Forcing function** — memory hygiene (decay/consolidate) extends
  Ring 2-slow's existing `runMemoryHygiene`; only drift-supersede is the
  Ring 4 memory pass. No new ring for hygiene. First passes run
  dry-run-with-report.
