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.
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
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:
Read, Edit, Write, Bash, and so
on.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
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
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.
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
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.
| Tier | Brain analogue | Lives in | How it reaches the model | Written by |
|---|---|---|---|---|
| T0 Working | Phonological loop | the live conversation | it is already there | every interaction |
| T1 Episodic | Hippocampus | memory/short-term/YYMMDD-*.md | auto-loaded if dated within 7 days | session debrief (§06) |
| T2 Semantic | Cortex | memory/<type>_<topic>.md | the agent opens it, prompted by its index row | debrief, consolidation, direct teaching |
| T3 Situational lens | Domain schema | lenses/*.md | a hook prints it when a matching tool call happens | humans only |
| T4 Always-on | Worldview | AGENTS.md, identity.md | auto-loaded, before any user input | humans 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:
- The auto-loaded index carries a one-line summary of each memory it lists — enough to judge relevance without opening anything.
- Working on a task, the agent recognises a summary as relevant and issues a
Readtool call for that file. That is the "cue": a judgment, not a query. - Reading it reinforces it (§08), so the act of finding a memory useful is exactly the act that keeps it findable.
- 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
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 class | Lives until | Examples |
|---|---|---|
| Durable | indefinitely; versioned in git, edited deliberately | identity, operating rules, lenses, reference documents |
| Curated | indefinitely, but actively maintained — corrected, merged, re-ranked | the memory corpus (T1 + T2) |
| Initiative | the end of the piece of work that created it, then archived | scratch analysis, working plans, intermediate exports, screenshots |
| Session | the end of one conversation | whatever 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
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
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:
| Destination | Holds | Shape |
|---|---|---|
T1 short-term/YYMMDD-facts.md | What happened, with provenance — dates, numbers, commits, sources | append-only; one file per day, many sessions may append to it |
T1 short-term/YYMMDD-residue.md | The texture — what was salient, what was left unresolved, where things were trending | append-only, same dating; deliberately not facts |
T2 <type>_<topic>.md | A durable pattern, when the session produced one | edited 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
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
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:
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
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
access_count + 1exp(−days / stability)0.6 × importancestability 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++andstability = min(stability × 1.6, 365)fire only when at least 20 hours have passed sincelast_reinforced— a separate timestamp fromlast_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.
| Band | File | Budget | Cost |
|---|---|---|---|
| Hot | MEMORY.md | 12,000 chars (~3K tokens) | paid every cold start |
| New | MEMORY.md | 2,500 chars, born ≤14 days | paid every cold start |
| Cold | MEMORY.md | 4,000 chars | paid every cold start |
| Archive | MEMORY-archive.md | unbounded | paid 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
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:
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
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.
| Hook | Fires on | Job |
|---|---|---|
update_memory_access.py | Read\|Edit\|Write\|MultiEdit | the two-strength update and the spacing gate (~35 ms) |
inject_lens.py | Edit\|Write\|Bash\|Agent\|… | prints matching lens text to stderr, once per session per lens |
allow_memory_writes.py | Edit\|Write\|MultiEdit | approves 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
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.
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_count0,stability14, 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 checkoutof 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
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.
short-term/260728-facts.md with provenance — the date, the endpoint, and the
29 false verification failures it had already caused.stability grows 14 → 22.4 days. The memory is now measurably harder to
lose.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.
11 Decisions and rejected paths
Each row is a fork we actually stood at. The right-hand column is the reason, not a rationalisation after the fact.
| Decision | Alternative rejected | Why |
|---|---|---|
| Markdown files + text search | Graph or vector database first | Policy is substrate-agnostic. Building storage first blocks the valuable part on infrastructure with no urgency. |
Adaptive per-file stability | One fixed half-life for everything | A global half-life cannot distinguish a well-rehearsed memory from a crammed one. That distinction is the model. |
| Spacing gate on reinforcement | Increment on every read | Without it a single grep-heavy session fabricates durability. |
Separate last_reinforced | Reuse last_accessed for both | One field cannot both track recency and gate reinforcement; sharing it silently disables the gate. |
| Bounded index + roll-off | Unbounded list of everything | Unbounded reached 168 KB and grew linearly with the corpus. |
| Dated files, kept forever | Rolling file, oldest truncated | Truncation deletes. The principle is never delete, only shift access cost. |
| Permissive writes + real decay | Strict gatekeeping on what may be remembered | The failure mode of strictness is losing something that mattered; the failure mode of permissiveness is a few hundred wasted bytes. |
| Human-only lens promotion | Auto-promote on access frequency | Auto-promotion turns one error into a permanent interpretive distortion. |
| One daily wake | A high-frequency tick | Housekeeping does not need it, and frequent unattended thinking has diminishing returns against real cost. |
| Scripts detect, model judges | Let the model do the whole pass | Model calls spent re-deriving what a linter checks for free. |
| An awareness-shelf floor | Pure utility weighting | Instrumental objects produce visible output, so unconstrained weighting starves the useless-but-true sits. |
| Lenses with declared triggers | More rules in the always-on file | Always-on context is a scarce shared resource; a narrow trigger is nearly free. |
| Derived index, regenerate on conflict | Hand-merge conflicts | Derived files have a generator; hand-merging generated output is strictly worse. |
| Borrow patterns from agent frameworks | Adopt one as the platform | Replaces 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
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 files | 634 — 264 feedback · 173 project · 158 reference · 26 process · 13 user |
| Hot band | 31 rows / 11,909 chars (budget 12,000) |
| New band | 12 rows / 2,464 chars (budget 2,500) |
| Cold band | 18 rows / 3,982 chars (budget 4,000) |
| Rolled to archive | 573 |
| Index size | 21 KB, down from 168 KB |
| Stability distribution | 504 at base (14d) · 119 mid (30–99d) · 8 high (100–364d) · 3 at the 365d cap (= 634) |
| Never reinforced | 491 files (81%) |
| Working set — reinforced at least once | 116 files; highest access_count is 13 |
| T1 short-term files | 27 active in the 7-day window |
| Dream-journal entries | 15 (14 consecutive nights) |
| Lenses / meditation objects | 9 / 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
Readonly, 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:
| Variant | Hot slots displaced | Behavioural rules pushed out | Stalest file left in Hot |
|---|---|---|---|
| Uncapped | 15 of 38 | 8 | 62 days |
| Capped at 3 reinforcements | 1 of 38 | 0 | 18 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
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
| Item | Why it matters |
|---|---|
| Fit the constants against real data | Every 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 linters | Registry-versus-filesystem drift, index staleness, metadata age. The hygiene arm is the cheapest value in the system. |
| Token-dense memory style | Memory 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
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.
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.
- Letta — github.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.
- Graphiti — github.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
| Script | Role |
|---|---|
agent_paths.py | resolves the agent's home and every memory subdirectory; the reason nothing is hardcoded to one agent |
update_memory_access.py | hook — the two-strength update and spacing gate |
inject_lens.py | hook — lens trigger matching and injection |
allow_memory_writes.py | hook — pre-approves memory writes |
rerank_memory_index.py | single writer of the index; scores, bands, rolls off |
dream_cycle.py | nightly deterministic driver; cues the reflective wake |
dream_select.py | weighted meditation rotation and the awareness floor |
dream_journal.py | reflection-residue store: write / recent / decay |
consolidation_scan.py | detects un-graduated T1 and weak or stale T2 |
memory_self_check.py | hygiene linter; detection plus safe auto-fixes |
regen_current_state.py | single writer of the in-flight list |
sweep_workstreams.py | close-detection and age-out for project memories |
bootstrap_memory.py | install — directories, frontmatter backfill, starter library, first index |
install_memory_hooks.py | install — registers the hooks and the two timers |
verify_memory_install.py | install — proves the policy is actually wired, not merely that files exist |
test_memory_ranking.py | tests — the 26-test regression suite, run against a synthetic fixture corpus in a temp dir, never the live one |
backfill_memory_created.py | migration — 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.py | migration — 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.py | migration — 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
| Constant | Value | Meaning |
|---|---|---|
BASE_STABILITY | 14 d | decay constant for a never-reinforced memory |
STABILITY_GROWTH | 1.6× | multiplier per spaced reinforcement (~7 reach the cap) |
STABILITY_CAP | 365 d | ceiling while the substrate is markdown |
SPACING_GAP_HOURS | 20 h | minimum gap for a touch to count as reinforcement |
IMPORTANCE_WEIGHT | 0.6 | score points per unit of human-set importance (default 0) |
HOT_CHAR_BUDGET | 12,000 | auto-loaded band, about 3K tokens |
COLD_CHAR_BUDGET | 4,000 | the listed-but-not-loaded band |
NEWBORN_CHAR_BUDGET | 2,500 | reserved slots for the New band, added to the auto-load total |
GRACE_DAYS | 14 d | how long a memory counts as newborn, keyed on created: |
NEWBORN_SUMMARY_CHARS | 160 | newborn rows are triage, so truncated; doubles the band |
MEDITATE_EVERY_HOURS | 22 h | at most one sit per nightly cycle; below 24 so a daily timer always qualifies |
BUSY_MINUTES | 10 min | a memory write this recent means a human is probably active |
SPACING (rotation) | 21 d | days-since-last-sat saturation point |
AWARENESS_FLOOR | 3 sits | force an awareness object if none chosen this recently |
GRAD_AGE_DAYS | 7 d | age at which un-consolidated T1 residue is considered overdue |
STALE_T2_DAYS | 120 d | age 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
- How Rowan Works — the wider agent infrastructure this memory system sits inside.
- The Pvragon AI Workspace — the four-layer architecture, retention tiers, and the directive/orchestration/execution split.
- Portable Agent Package — the transplantable subset of the workspace.
- Pvragon/ai-workspace-reference — public reference implementation, including the example agent memory layout.