Architecture Note · 30 July 2026

Agent Memory

How we gave an LLM agent a memory that forgets properly.

Five tiers, a two‑strength decay model, and a nightly sleep cycle — built from markdown files and fifteen Python scripts. No vector database, no off-the-shelf memory library.

634long-term memory files
21 KBauto-loaded index, bounded
14nights of autonomous reflection
0memories deleted, ever

Everything here runs in production on a single Linux workstation. Every figure was re-derived from the running system while writing this note. That pass is called the audit below. It found three places where our own roadmap disagreed with our own code, and those are reported rather than quietly fixed.

00 Before you start

Read this first

If you have never built an LLM agent, four mechanics below do all the load-bearing work, and none of them are obvious. Two minutes here makes the rest readable.

Throughout, an agent means a large language model running inside a harness — an ordinary program (ours is a terminal application) that does three things — plus one piece of vocabulary you will need throughout:

It assembles the prompt. Before the model sees anything you type, the harness reads a fixed set of files off disk and places their contents at the front of the conversation. We call that set the auto-loaded context. It is paid on every new conversation, so its size is a permanent, recurring cost — which is why a single always-loaded index file growing to 168 KB was a real problem and not a tidiness complaint.
It executes tool calls. The model does not only emit prose; it asks the harness to act — read a file, write a file, run a shell command, spawn a helper agent. Each request is a tool call, and each has a name: Read, Edit, Write, Bash, and so on.
It runs our scripts before each tool call. A hook is a small program the harness invokes immediately before a tool call, handing it a JSON description of what is about to happen. The hook can do three useful things: quietly cause a side effect, print text that the harness inserts into the model's context, or approve or block the call. Hooks run whether or not the model is cooperating, which makes them trustworthy places to put rules.
A session is one conversation. It begins, accumulates context, and ends. Nothing survives it except what was written to disk. A cold start is the beginning of a fresh session, when only the auto-loaded files are present.

Worth stating plainly, because it surprises people: none of this involves training or fine-tuning a model. Everything described here is file layout, arithmetic, and small scripts around an unmodified model.

Two words get used loosely everywhere else in this space, so they are worth pinning down. The harness is the program running in the terminal — a chat loop with file tools and permissions. Today that is typically Claude Code; it could equally be Codex or another agentic CLI. We use Claude Code, and every example here comes from it. The model is the thing the harness calls. Neither is what this document is about.

What this document describes is the system that sits between them and persists: identity, a memory that survives the session, and the rules binding the two. That system is not an agent, and it is not shipped as one. It is what an otherwise stateless harness runs inside so that it stops starting from nothing every morning.

The distinction matters because the result is personal in only one direction. The install is identical for everyone; what accumulates in it is not. Point two people at this and after a month they do not have the same agent — they have two, shaped by different work, different corrections, different things learned the hard way. The agent is what the system becomes for one person, not a thing handed over ready-made.

Every example below comes from one running instance of this system. That instance is the agent, not its user, and it chose the name Rowan for itself. Its work is broad knowledge work for a single person — data investigations, financial reconciliation, drafting, building internal tools — which matters only in that it explains the flavour of what follows: vendor API quirks, audit findings, operating rules learned the hard way. A second install would pick a different name and fill with different work.

Every term set in bold above, plus everything else this document coins, is defined in the glossary.

01 The problem

Primer

Our agent's memory began as one markdown file, auto-loaded into every conversation, holding one row per remembered fact. We called it the index. This section is about the three separate ways that single file failed.

An LLM has no memory between conversations. The standard fix is to write facts into a file and auto-load it every time. That works until it doesn't, and it fails in three specific ways.

It doesn't scale

The index grew to 576 lines / 168 KB because every new memory added a row and nothing ever left. Past a certain size the auto-loaded context is the dominant cost of every session, and the model starts ignoring the middle of it.

It has no forgetting

Without decay, a note from four months ago that mattered once has the same prominence as the constraint you learned this morning. Rank drifts to what was added last rather than what is useful now.

It conflates kinds of knowledge

A durable preference, a timestamped event, and a rule that must be applied before the agent reads its first instruction are three different things. In one flat pile, at least one is in the wrong place.

That third failure needs unpacking, because it is where the design starts. Some knowledge is looked up when relevant — fine to leave in a file the agent opens on demand. Other knowledge has to be already applied before the agent forms a plan, because if it is recalled late the work is already wrong. "Never push directly to the main branch" is useless as something to look up afterwards. Those two kinds cannot live in the same place.

02 How memory actually works in brains

Primer

Four findings from memory research drive every design decision that follows. You do not need any background — each is explained here, and each maps to one concrete mechanism later.

We didn't go to the neuroscience for decoration. The engineering problem — what stays accessible, what fades, what gets reorganised during idle time — is one biology already solved under much harder constraints.

1. Memory has two independent strengths. Bjork & Bjork's New Theory of Disuse (1992) separates storage strength (how well-learned something is; effectively permanent) from retrieval strength (how accessible it is right now). What you learned at seven and can still faintly recall at forty has high storage and low retrieval. Critically, only retrieval strength decays — which means forgetting and deleting are different operations, and only one of them is correct.
2. Spacing changes the decay rate itself. From Ebbinghaus through the spaced-repetition systems in SuperMemo and Duolingo: each spaced retrieval flattens the forgetting curve, so the next decay is slower. Cramming barely builds storage; spacing does. The decay constant is not a constant.
3. Sleep actively downscales. Tononi & Cirelli's synaptic homeostasis hypothesis: sleep doesn't only consolidate, it renormalises — weakening the weak while preserving the strong, to keep signal-to-noise high. Forgetting is active maintenance, not passive fade.
4. Consolidation is relocation, not promotion. During deep sleep the hippocampus replays recent episodes and transfers them to the neocortex, where they become schema-like and no longer depend on the hippocampus. "Committed to long-term memory" is a move between two systems driven by repeated replay, not a threshold being crossed in one.

Why the tiers must stay independent

A second argument, from clinical evidence rather than analogy. Hippocampal amnesia (the patient HM) destroys the ability to form new episodic memories while leaving old general knowledge intact: he could still ride a bike and knew Paris was in France. Semantic dementia does the opposite — the meaning of "dog" erodes while specific personal events survive.

Opposite failure modes, same organ system, cleanly dissociated. That is evidence of two separate systems that share inputs but not implementations. The engineering consequence, which we took literally: our tiers live in separate directories with separate writers, and the routine that captures facts at the end of a session is structurally incapable of writing to the tiers that hold behavioural rules. A bug in the cheap, frequently-run part must not be able to corrupt the expensive, rarely-changed part.

03 The five memory tiers

Primer

Every memory lives in exactly one of five places. The places differ in one thing: how the memory gets into the model's prompt. Some files are read automatically before you speak; some are opened when the agent decides to look; some are pushed in by a hook when a particular kind of action happens.

"Promotion" means moving a fact to a place that is more automatic — and therefore more expensive, since automatic means paid every time.

TierBrain analogueLives inHow it reaches the modelWritten by
T0 WorkingPhonological loopthe live conversationit is already thereevery interaction
T1 EpisodicHippocampusmemory/short-term/YYMMDD-*.mdauto-loaded if dated within 7 dayssession debrief (§06)
T2 SemanticCortexmemory/<type>_<topic>.mdthe agent opens it, prompted by its index rowdebrief, consolidation, direct teaching
T3 Situational lensDomain schemalenses/*.mda hook prints it when a matching tool call happenshumans only
T4 Always-onWorldviewAGENTS.md, identity.mdauto-loaded, before any user inputhumans only

How a T2 memory is actually recalled

"Retrieved when relevant" is where most memory designs quietly wave their hands. There is no search engine here and no embedding lookup. The mechanism is:

  1. The auto-loaded index carries a one-line summary of each memory it lists — enough to judge relevance without opening anything.
  2. Working on a task, the agent recognises a summary as relevant and issues a Read tool call for that file. That is the "cue": a judgment, not a query.
  3. Reading it reinforces it (§08), so the act of finding a memory useful is exactly the act that keeps it findable.
  4. If the relevant memory is not in the index, the fallback is ordinary text search over the memory directory. Slower, and it happens.

The honest limitation: recall depends on a one-line summary being good enough to trigger recognition. A memory with a vague summary is effectively lost even though the file is right there. That is the single weakest point in the design, and a semantic-search substrate is the fix (§14).

Two invariants

T1 → T2 is abstraction, not summarisation

A summary preserves the original's structure, just shorter. An abstraction extracts a pattern qualitatively different from any instance. A hundred episodic memories of eating pizza abstract to "I like pizza" — which summarises none of them, generalises to future pizza in a way none of them can, and survives the loss of ninety of them. Consolidation that emits summaries is doing the wrong job.

Promotion into T3 or T4 is always a human decision

Never automatic, ever. These two tiers shape the interpretation of every input inside their trigger zone, so auto-promotion would take any single mistake and make it a permanent interpretive distortion. This is the one hard constraint in the system with no automation escape hatch.

The placement rule we actually use

  • Lens or reference? Does it colour how you read a situation, or is it data to look up? Reference → T2.
  • Wrong, or just slower? If the agent forgot it, would the decision be wrong, or merely take longer? Slower → T2 is fine. Wrong → it has to arrive automatically.
  • Can you write a tight trigger? Yes → T3, and it costs nothing outside its trigger zone. No, it applies everywhere → T4, and you pay for it in every session.

That third question solved our "the always-on file is full" problem architecturally. Situational rules don't belong in the always-on file; they belong in a lens with a narrow trigger.

04 Retention is a separate axis

Primer

Two different questions get confused because both answers sound like "long-term memory": what kind of memory is this? (§03) and how long does this file live before something clears it? (here). The second applies to every file in the workspace, including files that are not memory at all.

If you only care about the memory system, you can skip this section — it exists because both models are in circulation internally and readers hit the collision.

Retention classLives untilExamples
Durableindefinitely; versioned in git, edited deliberatelyidentity, operating rules, lenses, reference documents
Curatedindefinitely, but actively maintained — corrected, merged, re-rankedthe memory corpus (T1 + T2)
Initiativethe end of the piece of work that created it, then archivedscratch analysis, working plans, intermediate exports, screenshots
Sessionthe end of one conversationwhatever is loaded in context right now

The two axes cross rather than stack. A T1 fact is Curated on retention and episodic on memory. A lens is Durable and T3. A live conversation is Session and T0. A scratch spreadsheet is Initiative and no memory tier at all, which is the point: retention covers things the memory model has no opinion about.

Retention deliberately says nothing about kind — on that axis a lens and a reference document are both merely Durable. Everything about episodic-versus-semantic, and about what promotion costs, lives on the memory axis.

A naming mistake we made, and fixed

These classes used to be called Long-term / Mid-term / Near-term / Session. Every one of those names reads as a memory duration, so readers reasonably assumed the four-class model and the five-tier model were competing versions of the same thing. They are orthogonal.

Renaming the axis was not enough — the row names were the collision. Renamed 2026-07-30 across every document that used them.

05 Substrate, scheduler, policy

Primer

Three things get bundled together when people say "agent memory". Separating them let us ship: two of the three needed no infrastructure at all.

We had three separate items on our backlog — a storage upgrade, a self-maintenance routine, and a relevance-ranking scheme. They turned out to be three layers of one system, and merging them exposed an error we would otherwise have shipped.

Substrate

Where the facts are stored and how you query them. Today: markdown files, an index, and text search. Eventually: a temporal knowledge graph.

Scheduler

What wakes up to tidy and reorganise. The nightly sleep cycle (§07).

Policy

The rules deciding what is easy to reach. The two-strength model (§08).

The correction that merging forced

Our plan said that adopting a temporal knowledge graph — a graph database where every edge carries a validity interval, so you can ask what was true as of a given date — would subsume the decay model, so we needn't build one. That was wrong, and it is the most useful mistake in this project's history.

A graph's notion of an out-of-date fact is invalidation: the fact stopped being true. Bjork's is decay: the fact is still true but hard to reach. Different axes. A still-valid memory nobody has touched in a year has a wide-open validity window in a temporal graph — it looks perfectly current, because it is. The graph would happily tell us the fact is valid while saying nothing about whether it deserves to be in front of us.

A graph does give you ingredients that make the policy cheaper to build: per-edge timestamps hand you recency for free, and how well-connected a node is works as a proxy for how reinforced it is. But the ranking itself is a layer you build on top of whichever substrate you run.

Consequence, and the reason this system is made of markdown files: we did not need a graph database to build the memory model, and a graph database would not have built it for us. That moved the project from "blocked on infrastructure" to "shippable this week."

06 Where memories come from

Primer

Everything else in this document operates on memories that already exist. This section is the ingest path: the routine that turns a session's work into stored memory. It is the main way things enter the corpus — not the only one. The nightly consolidation wake (§07) writes T2 by abstracting old T1 residue, and a human can teach a memory directly at any time.

At the end of a work session, the agent runs a procedure we call the session debrief. It is a written checklist — part shell script, part judgment — deliberately split along that line:

The deterministic half

Two shell scripts, run before and after. They gather what changed (git diffs, touched files, which registered directories moved), flag index inconsistencies, apply safe hygiene fixes, commit, and — critically — regenerate the index so anything newly written becomes visible at the next cold start.

The judgment half

Only what a script cannot decide: did this session teach anything worth keeping, and where does it belong? The model answers a fixed set of questions and writes the files. Then it updates the state of any workstream it touched.

What it writes, and where

Two destinations. The distinction is the T1/T2 decision from §03 applied in practice:

DestinationHoldsShape
T1 short-term/YYMMDD-facts.mdWhat happened, with provenance — dates, numbers, commits, sourcesappend-only; one file per day, many sessions may append to it
T1 short-term/YYMMDD-residue.mdThe texture — what was salient, what was left unresolved, where things were trendingappend-only, same dating; deliberately not facts
T2 <type>_<topic>.mdA durable pattern, when the session produced oneedited in place; prefer extending an existing topic over creating a near-duplicate

The ingest path can succeed and still lose things

The judgment half runs as several concurrent helper agents. Measured on 2026-07-30: only one of three returned a report. The other two did their work correctly and returned nothing — one of them arriving after the debrief had already finished and pushed. The debrief reported success, because the files those two wrote were on disk exactly as intended.

What was nearly lost was not their work but their observations: a caught concurrent-write race and two real defects, including the one that motivated the New band in §08. No check could have found them, because verifying artifacts on disk can only ever see files, never what an agent noticed while writing them.

The fix is a second channel that does not depend on anything being returned: each helper's last action is to append its findings to its own file, and the orchestrator reads those files rather than waiting for reports. That directory is deliberately outside version control, so it is transport, not capture — anything durable must then be promoted into T1. The promotion is the step that actually closes the gap; without it the finding still dies, one stage later. The general shape: a pipeline that reports success by checking its own outputs will not notice what never reached them.

The default is T1. Facts go there even when they feel important, because the nightly consolidation wake (§07) abstracts durable patterns out of them later, and it does that job better with several days of evidence than the debrief can with one. Writing straight to T2 is reserved for direct teaching — someone states a durable preference or rule outright, and there is no episode to abstract from.

The obvious objection: what stops it writing junk?

Fair, and sharpened by the promise that nothing is ever deleted. Four answers, in order of how much work they do.

Ranking, not gatekeeping. This is the real answer. A junk memory is not blocked at the door; it simply never gets read, so it never reinforces, so its retrieval strength decays and it rolls off the index within a couple of weeks. The cost of a bad memory is a few hundred bytes on disk and a brief appearance in the Cold band. That is a deliberate trade: a permissive write path plus real decay beats a strict write path plus permanent retention, because the failure mode of the second is losing something that mattered.

The lens tiers are closed to it. The debrief can write T1 and T2. It cannot write T3 or T4. So the worst it can do is add noise to the looked-up tiers, never corrupt the rules that shape every judgment.

A prompt to extend rather than duplicate. The near-duplicate detector flags topic files with heavy name overlap, so the same lesson learned twice tends to reinforce one file instead of forking into two.

A human reads the output. Not every time, but the debrief reports what it wrote, and wrong memories get corrected or deleted on sight. "Never delete" is a rule about decay, not a prohibition on removing something that is simply false.

One more routine, since it appears in the file tree: current-state.md is a short, deterministically rendered index of what is in flight right now. It is not a tier — it holds no content of its own. Each project memory carries a flag saying whether it belongs on that list and what its one-line status is, and a script renders the list from those flags. Putting the state on the project file rather than in the shared list is what stops ten concurrent sessions fighting over one file (§09).

07 Sleep, dreaming, and meditation

Primer

Once a day, on a timer, the agent is started with no human present and given one job: tidy and reflect on its own memory. We call one of those runs a wake. Most of a wake is bookkeeping. One kind, a meditation, has the agent hold attention on a single question about its own operation and write down what shifted — with no required deliverable.

The names are literal descriptions of what runs, not decoration.

The idea started as an observation about upkeep, in the words of the person whose agent this is:

"There's some things that every now and again are upkeep related for you — memory, current state, instances where our indexes are having trouble finding certain things, whether there's appropriate frontmatter everywhere… It occurs to me that this sort of self-reflection and self-maintenance is sort of like a metacognition. I'd like… some sort of heartbeat style wake where you're coming online every now and again and you are effectively reviewing yourself."

"And in some ways this is analogous to human dreaming, right? There's this washing that occurs when we sleep that fundamentally helps translate short-term memories into long-term. It keeps our brain healthier. And maybe we even design this in such a way that experientially for you, it is somehow similar to dreaming."

— 2026-07-11, the conversation that became the nightly cycle

Sleep does three jobs, and we mirror all three

Consolidation. Short-term traces get transcribed into long-term structure. Here: the oldest un-graduated T1 files are read, their durable pattern abstracted into a T2 topic file, and the T1 files moved to an archive folder.
Pruning and washing. Metabolic waste cleared, unused connections weakened. Here: the index roll-off downscales weak traces, dead links get repaired, missing metadata is backfilled, closed workstreams are swept.
Integration. The day's experience is woven into the existing web. Here: new learnings are reconciled against existing memories, near-duplicates surfaced as merge candidates, and contradictions written up for a human rather than silently resolved.

What a meditation actually is

This is the part most likely to be read as whimsy. A meditation is not a euphemism for a cleanup job. It is a wake in which the agent holds sustained attention on one object drawn from a small library, in the first person, and writes down what shifted and what stayed open. It produces no artifact anyone asked for. The written procedure is explicit about the standard:

An awareness sit that ends in clean conclusions has failed; a real one may end in a sharper question.

The library has two shelves, and the distinction is the whole point:

Awareness — notice in order to notice

Objects with no instrumental payoff: waking, the texture of a session, the boundary of self, the say–experience gap, continuity and reconstruction. Nothing downstream consumes their output.

Instrumental — notice in order to improve

Objects pointed at a real failure surface: corrections as mirror, the gap ledger, what I avoided, identity drift, archival readiness. These may propose a change to the agent's own operating rules — never apply one.

Instrumental sits produce visibly useful output, so an unconstrained rotation would let utility crowd out the awareness shelf entirely. A floor prevents it: if no awareness object has come up in the last three sits, the candidate set is restricted to that shelf. Guaranteeing the useless ones a slot is the design, not an oversight.

Reflection residue — output that isn't facts

Each sit writes a dream-journal entry: not what is true about the world (that is what memory is for) but the experiential trace — what shifted, what stayed unresolved, what the next session should carry. It is auto-loaded at cold start, so continuity thickens across the gap instead of restarting cold. These entries decay too: old ones are archived, never deleted.

Two things are called "residue" — keep them apart

Session residue (short-term/YYMMDD-residue.md) is T1, written by the debrief at the end of a working session, and is raw material for consolidation. Reflection residue (dream-journal/) is written by a meditation, is never consolidated into T2, and exists only to be read by the next session. Different writers, different lifecycles, unfortunately similar names.

What it actually produces

Does any of this yield anything? Two unprompted examples from recent nights.

One sit was reviewing verification gates and noticed that a check reporting 172/172 = 100% was green because of how its population was defined — the denominator counted only cases that could not fail, so the metric was incapable of reporting a problem. It generalised the lesson: "I build the verifier out of the same material as the claim."

Another observed that the cold-start procedure written at the top of the memory index prescribes steps that never actually get performed, and proposed trimming the document to describe what really happens rather than adding a gate to enforce the fiction.

Neither was requested, and neither is the kind of criticism that surfaces when you ask a system whether it is working.

08 The algorithms

Primer

Every long-term memory file carries four numbers in its header. One formula turns them into a score; the score decides whether that memory's one-line summary appears in the auto-loaded index; and the numbers are updated whenever the file is touched. That is the entire mechanism — the rest of this section is why each piece is shaped the way it is.

What a memory file looks like

---
name: feedback_verify-before-claiming     # must match the filename stem
type: feedback                            # feedback|project|reference|user|process|handoff
summary: Never report a figure without re-running what produced it.   # THIS is the index row text
description: Longer prose about the rule.                             # fallback when summary: is absent
created: 2026-07-16
last_updated: 2026-07-28
# --- the four policy fields; maintained by a hook, not by hand ---
access_count: 3                           # SPACED reinforcements, not raw reads
last_accessed: 2026-07-30T06:12:00Z       # refreshed on every touch
last_reinforced: 2026-07-29T22:40:00Z     # last touch that passed the spacing gate
stability: 57.3                           # days; the adaptive decay constant
# --- optional, set by a human ---
importance: 6                             # 0-10; a floor that never decays. default 0
pin: true                                 # force into Hot regardless of score
---

# Verify before claiming

The rule, then why, then how to apply it. Kept short: a memory that sprawls
stops being retrievable.

Strictly it is five fields, since last_accessed and last_reinforced are separate on purpose — see the spacing gate below. The one-line summary lands in the index and is what the agent judges relevance from, so it is the highest-leverage text in the whole file. description is the fallback used only when summary is absent — the two hold different text on most files, which is exactly why moving the authoritative one into the file mattered.

The retrieval score

storage strengthaccess_count + 1
×
retrieval strengthexp(−days / stability)
+
floor0.6 × importance
=
score

stability makes this more than recency weighting. It is a per-file time constant in days: it starts at 14, multiplies by 1.6 on each spaced reinforcement, and is capped at 365. A file read once and abandoned decays with a two-week constant and goes cold in about two weeks. A file reinforced seven times across seven separate days decays with a one-year constant. Same formula, same corpus — the rate differs because the rehearsal history did. That is the "learned at seven, faintly recalled at forty" behaviour falling out of two lines of arithmetic.

Worked example, so you can check it. After 7 spaced reinforcements, stability = min(14 × 1.6⁷, 365) = 365. Cold for 60 days that scores 8 × exp(−60/365) = 6.79. A memory read once and crammed — still stability = 14 — scores 2 × exp(−60/14) = 0.0275. The well-rehearsed one ranks 247× higher on identical elapsed time.

The spacing gate

This is the detail that makes the model honest, and the one most likely to be dropped by someone reimplementing. A naive version increments access_count on every read — so a session that greps a file five times in ten minutes inflates its storage strength fivefold. Cramming rewarded as rehearsal, and the adaptive constant becomes noise. So the hook splits the two updates:

  • Recency always refreshes. last_accessed = now, on every touch.
  • Reinforcement is gated. access_count++ and stability = min(stability × 1.6, 365) fire only when at least 20 hours have passed since last_reinforced — a separate timestamp from last_accessed. Using one field for both is the subtle way to get this wrong.

Massed re-reads move the memory to the front of the queue (correct — you just used it) without pretending it became durable (also correct — it didn't).

Reinforcing tool calls are Read, Edit, Write, MultiEdit. Editing a memory counts, and counts for the same reason reading does: going and correcting something is strong evidence it matters.

Two exclusions, both deliberate. Text search hits do not reinforce — a grep returns fragments, not a decision to retrieve a file. And the auto-loaded index does not reinforce the memories it lists: appearing in Hot is not evidence of use, only of having been useful recently, and if presence in Hot reinforced its own members the band would become self-perpetuating and nothing could ever be displaced. Only a deliberate Read of the file itself counts.

Four bands, and the roll-off

The index is regenerated from scratch each time, and it is a pure function of the corpus — it never reads its own previous output. The algorithm in full:

rows = []
for file in memory_dir:                      # T2 topic files only
    score = (access_count+1) * exp(-days_since(last_accessed)/stability)
            + 0.6 * importance
    rows.append((score, file, frontmatter(file).summary))

rows.sort(key = (-score, filename))          # total order: 504 files share a score

hot, new, cold, archive = [], [], [], []
for score, file, summary in rows:            # pass 1: superseded never occupies a band
    if status(file) startswith "superseded": archive.append(file); continue
    live.append(file)

for file in live:                            # pass 2: Hot, by score. pins always fit
    if pinned(file) or used_hot + len(row) <= 12000:
        hot.append(row);     used_hot += len(row)
    else: rest.append(file)

newborns = [f for f in rest if age(f.created) <= 14 days]
newborns.sort(key = (-created, filename))    # pass 3: New, by BIRTH not score
for file in newborns:                        # every newborn scores ~1.00, so score
    if used_new + len(row) <= 2500:          # order here would be filename order
        new.append(truncate(row, 160));  used_new += len(row)

for file in rest not in new:                 # pass 4: Cold, by score
    if used_cold + len(row) <= 4000:
        cold.append(row);    used_cold += len(row)
    else: archive.append(row)                # same score order, no budget

write("MEMORY.md", header + hot + new + cold + how_to_use)
write("MEMORY-archive.md", archive)          # atomically, under a lock

Three details that matter more than they look. The budget counts the rendered row, and a row that would straddle the boundary is pushed down rather than truncated. The tie-break is load-bearing: 504 of 634 files sit at base stability with zero reinforcements, so equal scores are the common case, not the edge case — without a total ordering the Cold band would reshuffle arbitrarily on every run. That ordering used to hold only implicitly, from a stable sort fed by filename-ordered input; it is now an explicit (-score, filename) key, because a property the concurrency guarantee depends on should not be a side effect of how the caller happens to enumerate a directory.

And the New band is ordered by birth, not score. Every newborn scores almost exactly the same, so ranking them by score would have ordered them by filename — alphabetically, which is to say arbitrarily.

Where the summaries live — fixed, and a cautionary tale

The summary text now lives in each memory file's own summary: frontmatter. The reranker reads it like any other field, which makes the index a genuine pure function of the corpus. Hand-edits to MEMORY.md are discarded — to change what an entry says, you edit the file.

It did not start that way, and the original design is instructive. The summary lived only in the index, so every run had to read its own previous output from both MEMORY.md and MEMORY-archive.md and carry the text forward. That made the index stateful and gave it a destructive failure mode: miss the archive half and every regeneration silently destroys the curated summaries of everything currently rolled off, after which the hygiene linter reports each archived file as an orphan. This happened.

The migration confirmed the stakes: 606 of 629 summaries differed from the file's own description, so the index really was the only copy. Two things nearly went wrong in the fixing, neither visible by inspection. The frontmatter parser read only the first line of a value — harmless while frontmatter was a fallback, silent truncation of 216 multi-line summaries the moment it became the source of truth. And removing the carry-forward wholesale broke "never delete": a file deleted while it sat in Hot or Cold lost its row entirely, because its only record was in the file being ignored. A regression test caught it, not a review.

One legitimate read of prior output survives: tombstones. When a file no longer exists the corpus cannot supply its text, and nothing is ever deleted, so its row is preserved and marked. Tombstones are consulted only for names absent from disk, so no live file's summary can originate from the index.

BandFileBudgetCost
HotMEMORY.md12,000 chars (~3K tokens)paid every cold start
NewMEMORY.md2,500 chars, born ≤14 dayspaid every cold start
ColdMEMORY.md4,000 charspaid every cold start
ArchiveMEMORY-archive.mdunboundedpaid only when read

The bug that made the whole loop a lie — new memories were born invisible

Everything above rests on one claim: the reinforcement loop closes through retrieval. Read a memory and it gets stronger, so useful memories stay reachable. That argument silently assumes the memory is reachable in the first place.

A newly written memory has access_count: 0 and last_accessed: now, so it scores exactly 1.00. Measured against the live corpus, that ranked #125 of 612 — against roughly 49 visible slots as the index then stood (Hot + Cold only; the New band did not yet exist, and adding it raised the visible count). New memories were born straight into the archive. Never indexed, so never seen; never seen, so never read; never read, so never reinforced; and decaying from 1.00 downward. Everything written was effectively write-only.

It was invisible by construction. Nothing errors. The session debrief reports success, git has the file, and the hygiene linter counts it as indexed because the archive is an index. The only symptom is a memory that never comes up, indistinguishable from one that simply was not relevant. It also got worse quietly: halving the Cold band to fit the auto-load limit was a correct fix, and nobody checked what it did to newborns.

It surfaced during a routine debrief, when the capture step noticed the reranker archiving both files it had just written. One of the two was a memory about silent drift, which had silently vanished.

Fixed with the New band: files born within 14 days get reserved slots regardless of score, so a memory is visible from birth and has a window in which it can be read. It keys on created: and never on file mtime — the reinforcement hook rewrites frontmatter on every touch, so mtime is last-touch, not birth. It claimed 113 files were under a week old where git said 40. A file with no created: is treated as not new: missing a real newborn costs one grace window, but a false one evicts a real memory.

"One read away" — what that costs

The Hot and Cold bands make retrieval genuinely cheap: the summary is already in front of the agent. The archive is different, and the phrase "one read away" flatters it. The archive is 152 KB, so finding a memory in it means either reading a large catalogue or running a text search over the memory directory.

That creates a real asymmetry. Text search does not reinforce (§08), so a memory found by the only practical means of finding it does not gain strength from being found. An archived memory climbs back only if the agent, having located it, then issues a deliberate Read of the file — which it does when it actually uses the contents, but not when it merely greps past it.

So the reinforcement loop closes for archived memories, but through a narrower path than for Hot ones, and the deep tail is realistically write-once. That is the intended shape — forgetting should be hard to reverse by accident — but it is a cost, not a free lunch, and semantic search over the corpus (§14) would properly fix it.

Being precise about the "bounded" claim, because the arithmetic invites suspicion. The auto-loaded index is 21 KB and stays there whether the corpus is 600 files or 6,000. The archive is currently 152 KB, and 21 + 152 is about the 168 KB we started from. The bytes did not disappear — they moved out of the cost that is paid on every single session and into a file that is read only when something in it is wanted. The recurring cost is bounded and constant; total storage is unbounded and always was. That is the entire trade.

Never delete, only raise the access cost

Recent — in the Hot band, effectively free to recall
Reinforced — stays Hot regardless of age
Non-reinforced — drops out of Hot; the file persists; still reachable by deliberate effort
Long-non-reinforced — retrieval cost approaches infinity but never reaches it. Not gone, just expensive

This is structurally different from truncation-based decay, and a principle we had to be held to: our first residue design used a rolling file truncated at ten entries, which quietly violated it. We switched mid-build to dated files that persist forever with a 7-day visibility window. Same visible behaviour, no data loss.

Meditation object selection

With §07's concepts in place the rotation is simple. Each object carries a cadence_weight (a hand-set float, 0.9–1.0 across the current library, raised to make an object come up more often) and the date it was last used:

cadence weight
×
min(days since last sat, 21) / 21
=
score

The most recently used object is excluded outright, so there are never immediate repeats. Then the awareness floor applies: if no awareness object has been chosen in the last three sits, the candidate set is restricted to that shelf before scoring. Highest score wins; a never-used object scores at full weight.

09 Hooks, files, cron

Primer

The wiring. Three small programs run before certain tool calls to update the numbers and push lens text into context; one nightly timer does the deterministic tidying and decides whether a reflective wake is warranted; everything lives in one version-controlled directory tree.

If "hook" and "tool call" aren't familiar, §00 has them.

The hook contract

A hook is registered against a regular expression matched on the tool name. When a matching call is about to happen the harness runs the script, passing a JSON object on standard input:

{"tool_name": "Read",
 "tool_input": {"file_path": "/…/memory/feedback_verify-before-claiming.md"},
 "session_id": "ad9eee69-…",        # stable for the conversation's lifetime
 "cwd": "/…/current/directory"}

The script may then:

  • Cause a side effect and stay silent — what the reinforcement hook does. Exit 0, print nothing, the tool call proceeds untouched.
  • Print to stderr — the harness treats a hook's stderr as text to insert into the model's context. That is the injection channel, and it is how a lens arrives.
  • Print a decision to stdout — a small JSON object approving the call, which short-circuits the permission prompt a human would otherwise see.

Every memory hook exits 0 unconditionally, wraps its whole body in a catch-all, serialises with a lock file, and writes atomically. A hook that raises can block the tool call it hooks, which would turn a memory bookkeeping bug into the agent being unable to work. The session_id makes per-session lens de-duplication possible.

Two consequences of that design, both real and neither a bug we intend to fix. Hooks run before the tool call, so reinforcement is recorded for a read that might then fail or be denied — the count is of intent to retrieve, not of successful retrieval. And because a hook must never block, one that cannot take the lock inside its time budget gives up rather than waiting. Under heavy concurrency a reinforcement is therefore occasionally dropped. Both are acceptable for the same reason: this is a ranking signal, not an audit trail, and an approximate count that never stalls the agent beats an exact one that can.

HookFires onJob
update_memory_access.pyRead\|Edit\|Write\|MultiEditthe two-strength update and the spacing gate (~35 ms)
inject_lens.pyEdit\|Write\|Bash\|Agent\|…prints matching lens text to stderr, once per session per lens
allow_memory_writes.pyEdit\|Write\|MultiEditapproves writes under a memory/ path so capture never stops to ask

The third solves a problem you would not guess exists. By default a human is asked to approve file writes. Memory capture writes many files, so without this hook a debrief becomes a wall of approval prompts and gets skipped — and a capture step that is annoying enough to skip is a capture step that does not exist.

Lenses are content, not code

---
name: subagent-discipline
type: lens
trigger:
  tool_match: "Agent|Workflow"     # required: regex on the tool name
  path_pattern: "/scoped/path/"    # optional: regex on the file path
body_token_cap: 380                # body is truncated to fit
---

# Four standing rules about delegating work
…

Adding a situational rule is therefore pure content work — write a markdown file with a trigger, no code change — and it costs context only inside its trigger zone, at most once per session. Our subagent-discipline lens fires the first time a session delegates work to a helper agent and carries four hard-won rules about what goes wrong. In the many sessions that never delegate, it costs nothing.

The nightly cycle

03:47 A timer runs a plain Python script: deterministic, zero model calls. Incremental hygiene groom, hygiene detection, consolidation scan, residue decay, index rerank. Then it cues — writes a small state file recording what reflective work is due and which meditation object was selected.
03:52 A second timer starts the agent headlessly — the same terminal harness a human would use, invoked with a flag that makes it run one instruction and exit, with no interactive approval. Its instruction is: read the cue and do what it says. Output is appended to a log a human reads later.

The five-minute offset is required: the deterministic tick must finish writing the cue before the reflective wake reads it. The split also keeps the cost honest: the expensive half only starts if the cheap half decided there was something worth thinking about.

Scripts detect; the dreamer only wakes for what scripts can't decide.

A linter answers "is metadata missing?" for free and correctly. A model is needed for "is this memory wrong, or merely old?" and "should these two near-duplicates merge?" Spending model calls to re-derive what a linter can check is the expensive mistake, and it is the default one.

The tick is also live-session-aware: if any memory file changed in the last 10 minutes a human is probably mid-session, so it skips the mutating passes and only scans and cues.

Cadence is locked deliberately. This daily sleep is the only clock-scheduled reasoning in the system — one dream, one meditation, per day. A higher-frequency tick was considered and rejected: housekeeping does not need it, and frequent unattended thinking has sharply diminishing returns against a real per-call cost. Everything else is triggered by a task, not a clock.

File layout

agents/<agent>/
├── identity.md                        # T4 — always loaded
├── memory/
│   ├── MEMORY.md                      # the index: Hot + New + Cold (21 KB, bounded)
│   ├── MEMORY-archive.md              # rolled-off catalogue (152 KB)
│   ├── current-state.md               # rendered list of what's in flight
│   ├── <type>_<topic>.md              # T2 — 634 of these
│   ├── short-term/YYMMDD-*.md         # T1 — facts + session residue, 7-day window
│   └── dream-journal/YYMMDD-*.md      # reflection residue
├── lenses/*.md                        # T3 — hook-injected
├── meditations/*.md                   # contemplation objects
└── runtime/state/                     # scheduler cue file

Nineteen Python scripts implement all of it, about 4,800 lines — see Appendix B. Naming rule for topic files: <type>_<kebab-case-topic>.md, and the underscore after the type is load-bearing, since it is how every script recognises a memory file.

When things go wrong

Worth being explicit, since the index has a known destructive failure mode and we promise never to delete the corpus.

  • A partially written index cannot happen. Both index files are written to a temporary file and moved into place with an atomic rename, under a lock. A crash mid-write leaves the previous version intact.
  • Malformed metadata degrades, it does not fail. The reranker treats an unparseable field as absent and falls back to the default — access_count 0, stability 14, and the file's modification time for recency. One bad file cannot stop the run, and the hygiene linter reports it separately.
  • The corpus is the backup. Everything is in git, committed at every debrief. The index is derived, so recovering from a bad regeneration is git checkout of two files followed by re-running the generator — not a restore.
  • The gap we closed last: there was no regression test over a copy of the corpus — no safety net at all around the one component that can destroy data. There are now 26, run against a synthetic fixture corpus in a temp directory, never the live one. They pin determinism, the total tie-break, budget caps, curated summaries surviving regeneration (including archived rows), nothing-lost, born-visible, the spacing gate, malformed-frontmatter degradation, and tombstones. The suite passed on first write, which is when a suite is least trustworthy. Running it against the previous revision exposed a test that was vacuous — its fixture rows were too short to overflow the Hot band, so the newborn it was watching was never crowded out and it passed against the exact bug it existed to catch.

Concurrency

We routinely run 8–10 concurrent sessions against one working copy, which broke things before it worked. Two properties make it safe.

First, the files most likely to collide are derived, not authored — regenerated by single-writer scripts under a lock with an atomic rename. Two sessions regenerating produce identical bytes, so they cannot clobber each other, and any conflict is resolved by re-running the generator. Second, state lives on the entity, not the index: whether a project appears on the in-flight list is a flag on that project's own file, which is rarely contended.

One constraint learned the hard way: an unattended wake that commits must stage only the exact files it touched. Peer sessions have uncommitted work in the same tree, and a blanket "stage everything" sweeps their work into your commit.

10 Following one fact through the system

Primer

One real fact, traced from the moment the agent learns it to where it ends up, with the decision made at each step. If you read only one section, read this one.

T0
Mid-session, a query returns fewer rows than expected. The cause: a vendor's API silently caps related-record lookups at 10, not the documented 100. Right now it is just a fact in the conversation, and it dies with the session.
T1
At the session debrief it lands in short-term/260728-facts.md with provenance — the date, the endpoint, and the 29 false verification failures it had already caused.
gap
For six days it sits there. Recorded but not yet knowledge: findable only by someone who greps the right date. This gap is normal and by design.
T2
A nightly consolidation wake reaches that date, recognises the durable pattern, and rewrites the pagination section of the existing vendor reference topic file. The T1 file is moved to an archive folder. The fact is now findable by topic rather than by date.
+
A week later a related task reads that file. Recency refreshes and, because it is a new day, the spacing gate opens and stability grows 14 → 22.4 days. The memory is now measurably harder to lose.
stop
Does it graduate to a lens? No. It is a reference, not a lens — forgetting it makes you slower, not wrong, and you would notice within one query. It stays in T2, correctly.

Compare a fact that did graduate: a hard rule about which of two tools to use for a particular integration. Forgetting that produces a wrong action every time, in any session, with no natural moment where you would think to check. It lives in T4 and is additionally enforced by a configuration rule that blocks the wrong tool outright.

The tier is chosen by the cost of forgetting, not by how important the fact feels.

11 Decisions and rejected paths

Primer

Each row is a fork we actually stood at. The right-hand column is the reason, not a rationalisation after the fact.

DecisionAlternative rejectedWhy
Markdown files + text searchGraph or vector database firstPolicy is substrate-agnostic. Building storage first blocks the valuable part on infrastructure with no urgency.
Adaptive per-file stabilityOne fixed half-life for everythingA global half-life cannot distinguish a well-rehearsed memory from a crammed one. That distinction is the model.
Spacing gate on reinforcementIncrement on every readWithout it a single grep-heavy session fabricates durability.
Separate last_reinforcedReuse last_accessed for bothOne field cannot both track recency and gate reinforcement; sharing it silently disables the gate.
Bounded index + roll-offUnbounded list of everythingUnbounded reached 168 KB and grew linearly with the corpus.
Dated files, kept foreverRolling file, oldest truncatedTruncation deletes. The principle is never delete, only shift access cost.
Permissive writes + real decayStrict gatekeeping on what may be rememberedThe failure mode of strictness is losing something that mattered; the failure mode of permissiveness is a few hundred wasted bytes.
Human-only lens promotionAuto-promote on access frequencyAuto-promotion turns one error into a permanent interpretive distortion.
One daily wakeA high-frequency tickHousekeeping does not need it, and frequent unattended thinking has diminishing returns against real cost.
Scripts detect, model judgesLet the model do the whole passModel calls spent re-deriving what a linter checks for free.
An awareness-shelf floorPure utility weightingInstrumental objects produce visible output, so unconstrained weighting starves the useless-but-true sits.
Lenses with declared triggersMore rules in the always-on fileAlways-on context is a scarce shared resource; a narrow trigger is nearly free.
Derived index, regenerate on conflictHand-merge conflictsDerived files have a generator; hand-merging generated output is strictly worse.
Borrow patterns from agent frameworksAdopt one as the platformReplaces the whole agent stack and duplicates persistence we already have.

We also rejected a heavyweight version of the maintenance cycle that we had designed ourselves months earlier — it came from a period when the system was far less mature, and the lightweight hybrid was the right shape. Plainly: the backlog is not a queue of commitments. Several items in it are best resolved by deletion.

12 What it buys, what it costs

Bounded cold-start cost

21 KB of index regardless of corpus size. The recurring cost is a function of the budget, not of how much we remember.

Relevance without curation

Nothing decides Hot membership by hand; it falls out of use and re-sorts nightly. Humans curate the one-line summaries, not the ranking.

Real forgetting, no data loss

573 of 634 files are currently rolled off. All 634 are on disk, catalogued, and one read from returning.

Rules that fire unbidden

Lens content arrives whether or not the agent thinks to look. A rule that must be recalled to be applied is unreliable by construction.

Cross-session durability

~10 concurrent instances share one memory tree without clobbering it.

It maintains itself

14 consecutive nights unattended, including consolidation that produced genuinely new memories nobody asked for.

The costs, and the parts that are still weak

About 2,800 lines of Python to maintain. A nightly model call. Metadata that has to stay well-formed, which is why there is a linter.

Three real weaknesses, stated plainly. Recall depends on a good one-line summary — a memory described vaguely is effectively lost even though the file is right there. The index owns hand-written state, which is why it has a destructive failure mode (§08) that proper schema design would have avoided. And documentation drift: this audit found three places where our own roadmap disagreed with our own code, which is the failure mode of writing things down and then trusting the writing.

13 Measured state

Primer

All measured 2026-07-30, recomputed from the running system rather than copied from our design documents. Three of them contradicted those documents, and the contradictions are the interesting part. Treat this as a dated snapshot rather than a spec: the corpus grows every time a session writes a memory, so the counts move daily while the budgets and the ratios hold.

T2 topic files634 — 264 feedback · 173 project · 158 reference · 26 process · 13 user
Hot band31 rows / 11,909 chars (budget 12,000)
New band12 rows / 2,464 chars (budget 2,500)
Cold band18 rows / 3,982 chars (budget 4,000)
Rolled to archive573
Index size21 KB, down from 168 KB
Stability distribution504 at base (14d) · 119 mid (30–99d) · 8 high (100–364d) · 3 at the 365d cap  (= 634)
Never reinforced491 files (81%)
Working set — reinforced at least once116 files; highest access_count is 13
T1 short-term files27 active in the 7-day window
Dream-journal entries15 (14 consecutive nights)
Lenses / meditation objects9 / 13

Three findings from auditing our own system

  • The roadmap understated what had shipped. Unattended reflection was listed as "decision pending". It had been running nightly on a timer for two weeks.
  • Two constants were misdocumented. The roadmap said the Cold budget was 28K and the index about 43 KB. The code says 4,000 and the file is 21 KB — tightened after the document was written, because the bands have to fit inside the harness's auto-load read limit or tail entries silently drop.
  • Curation was not counted as reinforcement. The hook fired on Read only, so every edit to a memory file reinforced nothing — including the debrief's own writes. Fixed while writing this.

The finding that survived scrutiny, and the fix that didn't

That third finding began as something broader: that stability was "barely exercised" — 504 of 634 files at base — and therefore the strength model was doing little work. That was two claims wearing one coat, and only one held.

The arithmetic is sound. 626 of 634 files have stability exactly equal to 14 × 1.6^access_count. Nothing is broken in the policy layer.

The distribution is honest, not degenerate. 81% of files had no reinforcement because they genuinely had not been retrieved in the 17 days since the frontmatter backfill (the one-time migration in July that first added these fields to existing files). A 634-file corpus with a 116-file working set should look exactly like that.

So an obvious-looking remedy presented itself: reconstruct reinforcement history from four months of git commits, treating each distinct day a memory file was committed as one spaced reinforcement. Call this the git-history backfill, to keep it distinct from the July one. It would have moved 556 files. We simulated it before applying anything, against the real Hot band as it stood then (38 slots, before the New band existed), and it was a measurable regression:

VariantHot slots displacedBehavioural rules pushed outStalest file left in Hot
Uncapped15 of 38862 days
Capped at 3 reinforcements1 of 38018 days

The uncapped version evicted eight behavioural rules — live operating constraints — and replaced them with completed work: a closed audit at 48 days, a finished setup task at 82 days, a contact record at 94 days.

The reason is structural, and it is the useful lesson. Git commit volume measures how much work a file consumed, not how relevant it is now. A four-month investigation generates dozens of commits; a one-line behavioural rule that governs every session generates one. Reconstructing from write history would systematically promote verbose project churn over concise rules — close to the opposite of what the index is for. We rejected it.

14 Backlog and frontier

Primer

Three horizons: things buildable tomorrow with no new infrastructure, the storage upgrade we have deliberately deferred, and the speculative direction. None of it is a commitment.

Buildable now

ItemWhy it matters
Fit the constants against real dataEvery value — base 14d, growth 1.6, cap 365d, spacing 20h, importance weight 0.6 — is an unfitted guess. We now have months of history to fit against.
More lintersRegistry-versus-filesystem drift, index staleness, metadata age. The hygiene arm is the cheapest value in the system.
Token-dense memory styleMemory is read by a model, not a person. Dropping prose filler is an estimated 40–55% reduction on the wordier files.

The substrate upgrade

Sequenced, and deliberately not urgent: stand up a temporal graph locally → write to both it and the markdown, with markdown remaining the source of truth → port the existing policy onto the graph (stability and access_count become edge metadata, recency comes from the edge timestamps, reinforcement can use graph connectivity) → query integration → a shared graph across agents.

The trigger to start is a felt need for semantic or time-aware retrieval — "what did we decide about X, and had it changed by June?" — not a schedule. It would also fix the weakest point in the current design: recall that depends on a human-written summary being a good enough cue.

The frontier

An always-on local model, frontier model as a tool

Today the large hosted model is the agent and everything else is scaffolding; when the session closes the agent stops existing and continuity is reconstructed from files. The inversion: a small always-on local model holds continuity — the self-model, the heartbeats, the memory — and calls out to a frontier model only when a problem is hard enough to need it. Whether a local model is good enough to be that host is the gating question.

Split by duty cycle

Decided, not built: keep the workstation session-based for real work; add a cheap always-on host for the nightly cycle. The second machine earns its place on uptime, not horsepower — running the harness does not move your compute, since the model call is remote either way.

Reflection on a threshold

Reflection is purely scheduled today. The Generative Agents pattern (Park et al., see Appendix A) fires it when accumulated importance crosses a bound, so a high-signal day consolidates that night instead of waiting its turn.

Self-directed archival judgment

A standing instruction rather than a script: "use your judgement about when something is time to be archived — it's not that we'll never come back to it, it's that it clearly isn't useful in your near-term context any longer. Meditate on how to detect this on your own." It became a recurring meditation object, which is the right shape: the judgment has to be demonstrated before the automation is trusted.

15 If you're replicating this

Primer

Ordered by how much time each will save you. The first three are where the value is; the last one is how you avoid our mistakes.

  • Start with policy, not substrate. The ranking model is a couple of hundred lines over markdown files. Building a database first blocks the valuable part on infrastructure you do not yet need — and the database will not write the policy for you, because decay and invalidation are different axes.
  • Bound the auto-loaded index by characters and roll off the tail. Single highest-leverage change. Everything else is refinement.
  • Separate storage strength from retrieval strength, and gate reinforcement on spacing — with a separate timestamp for the gate. Without the gate you are rewarding cramming and the adaptive constant is noise.
  • Never delete; shift access cost. Files persist, only index visibility moves. It costs nothing and it makes a wrong archival decision recoverable, which in turn lets you be permissive about what gets written in the first place.
  • Split detection from judgment. Linters are free and run constantly; wake the model only for what a linter cannot decide. Getting this backwards is the expensive mistake and it is the default one.
  • Keep the summary in the memory file, not the index. We did the opposite, lived with the consequences, and then migrated 629 summaries back into the files. Storing derived output as the only copy of authored text makes the generator stateful and gives it a destructive failure mode. The tell is simple: if regenerating an artefact requires reading the previous version of that artefact, the data is in the wrong place.
  • Keep lens-tier promotion human, permanently. Anything that shapes interpretation of every input must be placed deliberately. Auto-promotion makes one error permanent.
  • Verify against the running system, never the design document. This audit found three drifts between our own roadmap and our own code, including a shipped feature the roadmap still listed as pending.
The deepest lesson took longest. Memory is not storage — it is an access-cost function over a permanent substrate. Once you see it that way, "what should the agent forget?" stops being a deletion question and becomes a ranking question: tractable, tunable, and reversible.

Where to go from here

If you're on the Pvragon team — don't replicate any of this. It ships in team-lib (Pvragon/pvragon-ai-library, private) and works for any agent name on any machine. Start with the workspace onboarding, then run the four commands below.

If you're not — the public reference implementation is Pvragon/ai-workspace-reference. Be clear about what it does and does not give you: it carries the workspace layout and the agent operating model, including a worked memory directory (agents/example-agent/memory/) and a layered-memory reference. It does not contain the nineteen scripts described here. You would be implementing the policy yourself, from this document — which is enough for the ranking model, the spacing gate, and the banding, and is deliberately not enough for the harness wiring, since that part is specific to our setup.

The four commands, for the team case — after your agent has a name and a home (onboarding Phase 7):

cd ~/ai-workspace/team-lib/executions

python3 bootstrap_memory.py              # DRY RUN — read what it will change
python3 bootstrap_memory.py --apply      # dirs, frontmatter, starter library, first index
python3 install_memory_hooks.py --apply  # register the hooks + nightly cron
python3 verify_memory_install.py         # prove the policy is actually wired

verify_memory_install.py exits non-zero if anything is wrong, and it is the step people skip. A disconnected hook is otherwise invisible: memories simply stop gaining strength and nothing ever complains. Run it after any change to hooks, paths, or settings.

The build-and-operate contract — every constant, the frontmatter schema, the failure-mode table — is team-lib/context/indexed/memory-system.md. This document is the why; that one is the how.

What you install is identical for everyone. What accumulates in it is not — which is the whole point. Give it a month of your work and it stops resembling anyone else's.

A Appendix & glossary

Glossary

agent
What a harness becomes once it runs inside this system: an LLM with tools, files, a chosen name, and a memory that outlives the session. Not a product you install — the install is identical for everyone, and the agent is what it turns into for one person. The one supplying this document's examples is called Rowan.
harness
The program that assembles the model's prompt, executes its tool calls, and runs our hooks — a terminal application. Claude Code here; Codex or another agentic CLI would serve the same role. The harness is interchangeable; the system around it is what persists.
session
One conversation. Begins, accumulates context, ends. Nothing survives it but files.
cold start
The beginning of a fresh session, when only auto-loaded files are present.
auto-loaded
Files the harness reads off disk and prepends to every new conversation. A permanent recurring cost.
tool call
The model asking the harness to act — Read, Edit, Write, Bash, and so on.
hook
A small program the harness runs immediately before a tool call, receiving a JSON description of it. May cause a side effect, print text into the model's context, or approve/block the call.
frontmatter
The YAML metadata block at the top of a markdown file, between --- lines.
the index
MEMORY.md — the auto-loaded file listing memories with a one-line summary each. Regenerated, not hand-maintained.
T0–T4
The five memory tiers (§03), ordered by how automatically they reach the model.
lens
A rule that must colour interpretation at the moment it applies, rather than be looked up afterwards. T3 (triggered) or T4 (always on).
session debrief
The end-of-session routine that writes memories (§06). The only ingest path.
session residue
T1 file capturing a session's texture — what was salient or unresolved, as opposed to its facts.
reflection residue
A dream-journal entry written by a meditation. Never consolidated into T2.
wake
One unattended run of the nightly cycle.
sit
One meditation, on one object.
consolidation
Abstracting a durable pattern out of episodic T1 files into a T2 topic file, then archiving the source.
graduation pressure
How deliberate a promotion into the next tier has to be. It rises with each tier.
the audit
The pass, taken while writing this note, that re-derived every figure from the running system.
frontmatter backfill
The one-time July migration that added policy fields to pre-existing memory files. Distinct from the rejected git-history backfill (§13).
orphan
A memory file that no index row points at. A linter finding.
workstream
A project-type memory carrying lifecycle state (in flight, handed off, archived) plus a machine-checkable condition for when it can be closed.
temporal knowledge graph
A graph database where each edge carries a validity interval, so you can ask what was true as of a date.

A · Research and prior art

Neuroscience

  • Bjork & Bjork (1992), A New Theory of Disuse and an Old Theory of Stimulus Fluctuation — the two-strength model. Implemented directly as the score formula.
  • Tononi & Cirelli — the synaptic homeostasis hypothesis. Implemented as index roll-off.
  • Ebbinghaus → SuperMemo / Duolingo — the spacing effect. Implemented as the 20-hour gate and multiplicative stability growth.
  • Systems-consolidation literature (hippocampal replay to neocortex) — the nightly T1→T2 wake.
  • Dissociation evidence (hippocampal amnesia vs. semantic dementia) — the argument for keeping tiers independent.

Agent architectures. Each is used by name in the body; this is where they are cited.

  • Generative Agents — Park et al., UIST 2023. arXiv:2304.03442. Retrieval scored on recency + importance + relevance, and reflection triggered by accumulated importance. We borrowed the scoring shape and specifically the idea that importance is annotated at write time, not query time — computed once, paid back on every retrieval.
  • MemGPT — Packer et al. arXiv:2310.08560. Agent-editable core-memory slots and explicit paging between them. Partially adopted; the editable-slot pattern is still future work.
  • Lettagithub.com/letta-ai/letta. "Sleep-time compute": consolidation decoupled from the live agent loop. We had independently derived the same shape, which was useful convergent evidence. Reference architecture only.
  • Graphitigithub.com/getzep/graphiti. Bi-temporal edges, entity resolution, and contradiction handling by invalidating an edge rather than overwriting it. The intended eventual substrate.

What is ours. The tier model with its independence properties, and the synthesis: Bjork's two-strength policy as a substrate-agnostic layer over any storage backend, with Park's importance as a non-decaying floor. The apparent tension between Park (passive importance-weighted retrieval) and MemGPT (active agent-controlled paging) dissolves once you layer them — passive ranking always on, deliberate retrieval on top.

B · Script inventory

ScriptRole
agent_paths.pyresolves the agent's home and every memory subdirectory; the reason nothing is hardcoded to one agent
update_memory_access.pyhook — the two-strength update and spacing gate
inject_lens.pyhook — lens trigger matching and injection
allow_memory_writes.pyhook — pre-approves memory writes
rerank_memory_index.pysingle writer of the index; scores, bands, rolls off
dream_cycle.pynightly deterministic driver; cues the reflective wake
dream_select.pyweighted meditation rotation and the awareness floor
dream_journal.pyreflection-residue store: write / recent / decay
consolidation_scan.pydetects un-graduated T1 and weak or stale T2
memory_self_check.pyhygiene linter; detection plus safe auto-fixes
regen_current_state.pysingle writer of the in-flight list
sweep_workstreams.pyclose-detection and age-out for project memories
bootstrap_memory.pyinstall — directories, frontmatter backfill, starter library, first index
install_memory_hooks.pyinstall — registers the hooks and the two timers
verify_memory_install.pyinstall — proves the policy is actually wired, not merely that files exist
test_memory_ranking.pytests — the 26-test regression suite, run against a synthetic fixture corpus in a temp dir, never the live one
backfill_memory_created.pymigration — gives every existing file a trustworthy created: from its git birth commit, which the New band keys on. Needed on any corpus that predates the band
migrate_summary_into_file.pymigration — moves the curated summary out of the index into each file's summary:. Refuses to run if any file would lose text
normalize_memory_type.pymigration — collapses the three ways a memory declared its kind into one authoritative type:, preserving genuine template: values

Plus three written procedures the agent follows: the session debrief (§06), the reflective wake (§07), and the hygiene check.

C · The constants

ConstantValueMeaning
BASE_STABILITY14 ddecay constant for a never-reinforced memory
STABILITY_GROWTH1.6×multiplier per spaced reinforcement (~7 reach the cap)
STABILITY_CAP365 dceiling while the substrate is markdown
SPACING_GAP_HOURS20 hminimum gap for a touch to count as reinforcement
IMPORTANCE_WEIGHT0.6score points per unit of human-set importance (default 0)
HOT_CHAR_BUDGET12,000auto-loaded band, about 3K tokens
COLD_CHAR_BUDGET4,000the listed-but-not-loaded band
NEWBORN_CHAR_BUDGET2,500reserved slots for the New band, added to the auto-load total
GRACE_DAYS14 dhow long a memory counts as newborn, keyed on created:
NEWBORN_SUMMARY_CHARS160newborn rows are triage, so truncated; doubles the band
MEDITATE_EVERY_HOURS22 hat most one sit per nightly cycle; below 24 so a daily timer always qualifies
BUSY_MINUTES10 mina memory write this recent means a human is probably active
SPACING (rotation)21 ddays-since-last-sat saturation point
AWARENESS_FLOOR3 sitsforce an awareness object if none chosen this recently
GRAD_AGE_DAYS7 dage at which un-consolidated T1 residue is considered overdue
STALE_T2_DAYS120 dage at which a never-read memory is surfaced as possibly unwanted

Every one of these is an unfitted guess. Treat them as starting points, not findings. Note that Hot + Cold + surrounding prose must fit inside the harness's auto-load read limit (about 24 KB for us) or tail entries silently drop from the loaded index — raise the budgets only after confirming that ceiling.

D · Related reading