Architecture Note · 15

The memory loop that does not grow

claude-vault · Agent Memory · August 2026 · Eric Tetzlaff

Every coding session with an AI agent starts from zero. The agent does not remember yesterday. It does not know which approach you already rejected, or which bug you already fixed, or why the database column is named the way it is.

There are two obvious ways to fix that, and both of them fail.

Read everything. Feed the agent the full history of the project at the start of each session. This works for a week. Then the history is longer than the context window, and every session costs more than the one before it.

Read the newest few. Feed the agent the last three notes. The cost stays flat, but the memory does not. A decision made in June falls off the back of the list in July and is gone. The system remembers the most recent work, which is rarely the most important work.

claude-vault is my answer to that trade-off. It is the memory layer my Claude Code sessions run on. This note describes what it is, not how I got there: the four stages of the loop, the rules each stage follows, and the two properties the whole design exists to guarantee.

The log of what happened grows forever. The understanding of what it means gets rewritten in place, and stays the same size.

The loop, end to end

Four stages run in a circle. A session resolves which memory belongs to it, reads that memory back, does the work, then folds what it learned into the memory it read from. The output of stage four is the input of stage one, next time.

Figure 1 — The full loop
01 Find the memory which project does this folder belong to? 01 WHERE YOU ARE the folder you opened a session begins with nothing but a path 02 THE RESOLVER vault-project.ps1 tries three in order, stops at the first hit 1 saved list · 2 the main git repo · 3 the folder name 03 THE ANSWER one project name every copy of a repo shares the same name 02 Read it back three things are loaded before you type anything 04 THE DIGEST _master.md everything known about this project, six sections, sent whole 05 RECENT DETAIL the 2 newest notes the last two sessions, not compressed yet 06 STANDING RULES patterns/*.md lessons that apply to every project, one line each 07 WHAT THE AGENT SEES one block, marked as old memory, so the next save never writes it back in 03 Do the work the agent starts warm, not cold 08 THE SESSION build · debug · decide the history is already in context, so nothing gets re-explained 09 COMMIT NOTES *-commit.md one written automatically per commit, 843 so far · compressed, not read directly 04 Write it down three files · three different ways to write 10 THE DIGEST _master.md REPLACE merged with the old one, then rewritten whole 11 THE RECORD <date>-<slug>.md ADD a new file each time, never edited afterwards 12 THE RULES FILE CLAUDE.md PATCH only the text between two markers ever changes _master.md · SAME FILE written in stage 4, read back in stage 2 — the repetition is the loop
How to read a card: the top line is what it is called here, the second line is the real file or script, the rest says what it does. Monospace means a genuine path on disk, and the ringed numbers are reference points used in the text below. Modules 04 and 10 are the same file — _master.md — rewritten in stage 4 and read back in stage 2. The rail down the left margin, interrupted by its own name plate, is that return trip. It is the only long connector in the figure because it is the only one that matters: that rail is the loop.

Stage 1 — Resolve: which memory belongs to this directory?

Before anything can be read, the system has to answer one question: which project is this? That sounds trivial. It is not, and getting it wrong is silent. The session still runs. It just runs with the wrong memory, or with none.

The resolver is a fallback chain. Three steps, in order, first match wins.

Figure 2 — Resolution order
01
Registry lookup
A JSON file maps a directory to a project. If I attached this exact folder to a project by hand, that answer wins over everything else.
explicit
02
git rev-parse --git-common-dir
Finds the main repository behind this directory. Called from inside a git worktree, it returns the primary repo, not the worktree.
inferred
03
Directory leaf name
The folder's own name, lowercased. The last resort, and the only step that can invent a new project.
fallback
The result then passes through an alias map, so several folder names collapse to one project. Reads span every alias folder. Writes only ever go to the canonical one.

Step 2 is the load-bearing one, and the exact git command matters. The obvious choice, --show-toplevel, returns the directory you are standing in. In a worktree — a second checkout of the same repository, used to run parallel branches — that is the worktree, not the project. Five worktrees of one repository produce five separate memories, each holding a fifth of the history, and none of them announcing that anything is missing. --git-common-dir returns the primary repository instead, so every worktree resolves to one project.

A worktree is a temporary pointer. Memory belongs to the project, not to the folder you happen to be standing in.

One rule holds this together: the resolver lives in exactly one file, and everything else calls it. Not because duplicated logic is untidy, but because duplicated logic drifts. Three copies of "which project is this" cannot be kept identical by discipline, and when they disagree the symptom is a memory that is quietly incomplete.

Stage 2 — Recall: what gets read back, and what it costs

A hook runs when a session starts. It reads three things and hands them to the agent as context. Each has a hard limit, set in code.

Figure 3 — The injection budget
SourceLimitWhy it is in the budget
_master.md 8,000 chars The rolling digest. Everything the system understands about this project, compressed. This is the layer that compounds.
2 newest notes 3 sections
600 chars each
Recent detail the digest has not absorbed yet. Only Summary, Key Decisions, and Next Steps are extracted.
patterns/*.md 25 lines Standing rules that apply to every project, one line each. Cheap enough to carry unconditionally.
Files whose names begin with an underscore are skipped when scanning for rules. The index file in each folder documents its own schema using a worked example — and a scanner that does not skip it will inject the documentation as if it were a real rule.

The whole block is wrapped in a marker: === VAULT RECALL ===. That fence is not decoration. It tells the write path, later in the same session, that this text is old memory rather than new work.

Without it the system develops a specific and nasty failure. The agent reads the digest, then at save time summarises the session — including the digest it just read. The summary of the summary goes back into the digest. Each cycle moves the memory one step further from what the notes actually said, and each cycle sounds more confident than the last. A memory that folds its own output back in without knowing it will drift, and drift quietly.

Two more guarantees the read path holds. It never blocks a session: if the resolver is missing, if a file is unreadable, if the vault directory does not exist, the hook degrades and exits cleanly. And it writes nothing, anywhere. Reading memory cannot corrupt memory.

The bug that made every project look empty

The hook loads the resolver by dot-sourcing it, which in PowerShell executes the other file inside the caller's own variable scope. Both files had a variable named for the vault root. The resolver's value silently overwrote the hook's, every lookup pointed one directory too high, and every project reported zero memory.

Both files were correct on their own. The defect existed only in the composition. The fix was mechanical — prefix every shared-scope variable — but the rule it produced is the useful part: test the composed path, never only the unit.

Stage 3 — The session, and the notes nobody reads

While work happens, a separate hook writes one small note per git commit. There are 843 of these files. They are the densest record in the store, and they are never injected directly — one note per commit would fill the context window on its own.

So they take a different route. They are input to the fold, not input to the session. The distinction is worth stating plainly, because it is the piece that most append-only memory systems are missing: a file being valuable is not a reason to read it at session start. It is a reason to make sure something eventually compresses it.

Stage 4 — The fold: three targets, three write modes

Saving a session writes three files in one pass. Each one is written in a different mode, and the mode is the design.

Figure 4 — Write targets
TargetModeRule
sessions/<project>/
<date>-<slug>.md
APPEND Never edited, never deleted. This is the audit trail, and the only thing a full rebuild is allowed to read from.
sessions/<project>/
_master.md
REWRITE Read the current digest, merge this session into it, write the whole file back. The watermark advances so the next fold reads only what is new.
<repo>/CLAUDE.md PATCH Only the text between two sentinel comments is replaced. Everything outside is preserved byte for byte. If only one sentinel is found, the script refuses to write.
The refusal case matters more than it looks. One sentinel means the file was edited by hand and the boundary is now ambiguous. A script that guesses where a managed block ends will eventually delete something a human wrote.

The middle row is the mechanism the whole system is named for:

digest(n) = merge( digest(n-1), notes newer than the watermark )

The digest is not a list of sessions. It is six fixed categories, and every fold rewrites all six.

Figure 5 — The six cells
Current State
Where the project actually stands right now.
How It Works
The architecture, in the terms the code uses.
Decisions
What was chosen, and what it ruled out.
Gotchas
Failures that cost real time once already.
Patterns
Rules general enough to reuse elsewhere.
Open Threads
Unfinished work, stated as next actions.
Six categories, roughly twelve lines each. A project has a limited number of things you can know about it, and that number is what the digest tracks.

That last sentence is the bound, and it is worth being precise about why it holds. The digest is keyed by category, not by recency. Its size does not follow how much was written down. It follows how many kinds of things are known. Write four hundred more notes about the same six categories and the digest gets more accurate, not longer.

The fold rules

RuleReason
Merge, never appendOne line per fact. Appending is what produces a log, and a log is the thing being replaced.
Newer winsOn a conflict, the later session is correct. Say so out loud when the change is material.
Drop supersededA reversed decision leaves the digest completely. It stays in the append log, where history belongs.
Cap every cellAt the limit, compress. Never spill into a seventh cell.
Never inventEvery line traces to a note. Counts are re-checked when used, never carried forward from memory.
State, not narrative"Parked on branch X awaiting merge" beats "we discussed whether to park it."
Advance the watermarkSo the next fold reads only what is new, and re-running with nothing new does nothing.

What the loop actually buys

The heaviest project in my vault has 665 note files, holding about 688,000 characters of raw history. Its digest is 9,074 characters. That is roughly 76 times smaller, and it is the version the next session actually reads.

665
Note files on the heaviest project
76×
Raw history vs. its digest
20/31
Project folders with a digest built

The ratio is not the interesting number, though. The interesting number is the one that does not move. Across projects holding anywhere from a single note to six hundred and sixty-five, every digest lands between about 2,200 and 9,100 characters. Priming a session costs about the same on a project I started yesterday as on one I have worked in for eight months — and on the old one, the agent gets all of the history instead of the most recent slice of it.

The technique is not new to this system. I first built it as a standalone experiment against a different memory store, where the two load-bearing ideas showed up: key the summary by category rather than by recency, and keep a watermark so each run only reads what arrived since the last one. This is that idea wired into the loop I use every day, rather than run as a batch job beside it.

The line between the script and the model

One boundary runs through the whole design, and it is the same boundary I use in every agent system I build.

The scripts do the mechanical work. Resolve the project. Read files and strip the byte-order mark. Find the sentinels. Replace the block between them, or refuse. Return an exit code that says which happened. All of it deterministic, all of it testable, none of it requiring a model.

The model does the merge. Deciding that this session's "we moved the launch date" supersedes a line written three weeks ago, that two differently-worded gotchas are the same gotcha, and that a paragraph can become a clause — that is a judgement about meaning. A deterministic merger cannot consolidate prose. It can only concatenate it, which is the problem being solved.

Give the model the judgement and the script the file handle. Neither one is good at the other's job.

Where it is honest to say this is incomplete

Three limits, stated plainly.

The size cap is requested, not enforced. The hook will inject at most 8,000 characters of digest. The largest digest in the vault is 9,074, so it is currently truncated at read time, with a marker telling the agent to re-compress it. A limit written in a prompt is a strong suggestion to a model. It does not become a guarantee until code truncates or rejects, and mine does not yet.

Coverage is partial. 20 of 31 project folders have a digest. The rest report that fact honestly at session start rather than pretending to be empty, which is the right behaviour, but it is still eleven projects running on the old shallow recall. And on the smallest projects the digest is not a saving at all: six cells have a floor cost of roughly 2,200 characters whether the project has one note or fifty. This technique earns its place on deep history, and nowhere else.

One user, one machine. This is a local git repository with no remote. It works for a single operator across many projects. Nothing here has been tested with two people folding the same digest at once, and the merge step — a model rewriting a whole file — is exactly the kind of operation that would need real conflict handling first.

What holds regardless of scale is the shape. An agent's memory should be two layers, not one. The log is append-only and grows forever, because history has to stay checkable. The understanding is rewritten in place and stays flat, because that is what you actually read back.

Do not keep every note you ever took. Keep a good summary, and revise it every time you learn something.

← Architecture Note · 14 All posts →