Sep 10, 2026
Building a Memory That Outlives the Session
Every conversation with an assistant starts from nothing. You explain the project again. You re-establish who the stakeholders are, what was decided last week, which number is the real number and which one is the typo everybody keeps repeating. The assistant is fluent and capable and has no idea it has met you before.
Most attempts to fix this reach for something clever. What actually works is boring: a folder, three markdown files, and a discipline about when you are allowed to write to them.
This is a guide to building that. It takes about twenty minutes. Then I’ll tell you what it did inside the first ninety, because the build is the easy half.
Updated October 2026: version 2. This system has now run every day for six weeks, and it learned some things the hard way. The spec below is now v2, and the notes marked October in this post say where the original advice changed. The full story of what broke and what fixed it is in Version Two: Six Weeks Inside a Memory. The original v1 spec is still available here.
Portable Persistent Memory System — Build Instructions
The full build spec described below, as a single file. Hand it to an assistant with read/write access to a cloud file store and say “build this.” Already running v1? Say “upgrade this library to v2.”
The shape of it
Project Name/
└── Memory/
├── 00-index.md Navigation. Read every session.
├── 01-long-term.md Durable facts. Changes slowly.
├── 02-short-term.md Active buffer. Expected to churn.
├── absorbed/ Archive of merged notes. Never read at session start.
└── mem-<timestamp>.md New memories land here, unmerged.
That’s the whole system. Put it in Google Drive, OneDrive, Dropbox, a git repo, or a synced local folder. The assistant reads the index at the start of a session, pulls the other two files if it needs them, and writes new memories as timestamped files that get merged in later.
Start by finding out what your tools can actually do
Before designing anything, make the assistant test its own capabilities and write down what it found. Not read the documentation — test. Can it create a file? Read one? Create a folder? Rename one? Move a file between folders? Delete one?
And the one that determines everything else:
Can it change the contents of a file that already exists, or can it only create new files?
This sounds like a detail. It is the entire architecture. A lot of cloud-storage connectors expose an operation called “update” that only touches metadata — the title, the parent folder, the sharing settings — and cannot alter a single byte of content. The operation is named like an editor and behaves like a filing clerk.
If the assistant can’t edit content, it can never revise a core memory file. It can only add new ones. That constraint produces the addendum pattern further down, and it isn’t a workaround to be engineered away — it’s the floor the whole design stands on.
Have the assistant record the results, with a date. Otherwise a future session re-derives them, and it may re-derive them wrong. Mine had a note claiming it couldn’t move files in Drive at all. That note was false, and it had quietly constrained the design harder than reality did.
Three files, split by volatility
The instinct is to split memory by subject: work, personal, technical. Don’t. Most facts belong to more than one subject, and the subject tells you nothing about how long the fact will stay true.
Split by how fast things rot instead.
Long-term holds what stays true for months or years. Who you are, who the people around you are, your standing positions, settled architecture, stable preferences. Appended to, rarely rewritten.
Short-term holds current state. Projects in flight, open threads, the phase a program is in, things that will resolve or expire. It is supposed to churn.
The index is navigation, and it’s the only file that must be read every single session. It says what the other two contain, when to bother opening them, and where everything outside the library lives.
That last point is what makes the system cheap to run. The assistant reads one file by default and fetches more only when the index tells it to. Without that, every session pays full price for the entire memory whether it needs it or not.
When you’re sorting the initial content and you’re unsure where something goes, put it in short-term. Promoting a fact later is easy. Demoting something you wrongly enshrined as permanent is not, because by then it’s been shaping decisions for a month.
October: “Don’t split by subject” was right for the first split and wrong as a permanent rule. A busy long-term file keeps growing until reading it at the start of every session costs more than the session can afford. Mine hit that at about 135KB, and again at about 285KB. Keep the volatility split, and when size forces it, move whole subjects out of long-term into their own files that are read only when the work touches them. Current state still lives in short-term, one entry per subject. Ask before you restructure; the assistant should propose it, not do it.
Address things by ID, not by name
This is the highest-leverage decision in the build, and it looks like pedantry until the moment it saves you.
The index is a table of pointers. Make every pointer a file ID — the opaque identifier your storage assigns — rather than a filename or a path.
Names change. IDs don’t. They survive renames, they survive moves between folders, they survive someone reorganizing the whole tree on a Saturday. An index built on filenames rots silently the first time anything gets tidied, and you find out when the assistant confidently tells you a document doesn’t exist.
During my build we renamed two folders and moved three files inside a single session. Every pointer written an hour earlier still resolved. The restructure cost one file rewrite instead of a rebuild.
One consequence worth stating explicitly, because it bit me: a file can never contain its own ID. The ID doesn’t exist until after the file has been created, so any ID you write for the current file is invented. Write “this file — resolve via folder listing” and address everything else properly.
How new memories get written
When the assistant learns something worth keeping, it does not edit a core file. It writes a new one.
October: the first thing that broke wasn’t the writing. It was the assistant not writing here at all. Four times in five days it filed everything into whatever was nearest to hand (its built-in memory, project documents) and wrote the library only when I asked. The instruction to use the library sat at the top of the index, and it was read at session start. It didn’t help. An instruction read at the start of a session does not fire at filing time. What fixed it was a trigger on the act of filing itself: a skill that loads on words like file, note, remember, commit and asks one question, library first?
Name it mem-YYYY-MM-DDTHHMMZ.md, in UTC. Timestamp-first means lexical sort equals
chronological sort, so merge order is never ambiguous. Resist sequential numbering — mem1, mem2 —
because it forces the assistant to poll for the highest existing number, and two sessions will
eventually choose the same one.
Inside, every block carries a routing label saying where it belongs:
# Addendum — 2026-09-06T1510Z
## -> long
- [stated] <a fact that looks durable>
## -> short
- [stated] <current state, will churn>
## -> index
- pointer: <title> · <file id> · <what it is>
## -> drop-from-short
- <something in 02 that has resolved, and why>
That label turns the merge from a judgment call into a mechanical operation. You’re approving or overriding a proposal, not sorting raw material.
October: add the subject to every label (-> long | billing), because the subject is what routes
a block once the library has more than one file. Take the timestamp from the clock, never the
assistant’s sense of the time. List the folder right before creating a note, and if the name is
taken, use the next minute: parallel sessions collided four times. And create notes through a
channel that refuses to overwrite. A plain write-by-path once replaced another session’s note
twenty seconds after it was made.
The tag that does the real work
Notice [stated]. It means the user said this directly.
Not inferred. Not concluded. Not read out of a config file or a search result. Not the assistant’s own advice, even after you’ve accepted it — record that you chose it, not the reasoning that got you there.
This single distinction is the difference between a memory system and a rumour mill. Without it, one confident guess becomes a fact, and that fact shapes every decision downstream with no trace of where it came from.
Hold onto that sentence. It is the whole back half of this post.
October: two refinements earned their place. An option I picked from a list is recorded as a
choice, not as words I typed. And what the assistant knows from its own work goes in untagged,
with a separate [inferred] for its conclusions, so the three kinds of line can never be mistaken
for each other.
The loop
WRITE (any session) MERGE (whoever can edit)
─────────────────── ────────────────────────
1. Read index + core files 4. Read unmerged notes in order
2. Read any unmerged notes 5. Promote / update / drop each block
3. Write mem-<timestamp>.md → 6. Update the core files
(never touches a core file) 7. Move the note into absorbed/
8. Log it in the index
absorbed/ is explicitly excluded from the session-start read. That exclusion does two jobs: it
stops the same memory being merged twice, and it keeps the context window bounded as the archive
grows. The archive is still there — it’s the audit trail, and it’s how you reconstruct or reverse a
merge — but it is not context.
Who performs the merge depends on that capability probe. If the assistant can edit files, it can run the whole loop itself. If it can’t, you do it, or a session with a different channel does.
October: add a step zero, count the unmerged notes at session start, and absorb on a cadence. Once filing worked, mine went fourteen days without a merge: 151 notes waiting, about 528KB, against 189KB of core files that described a world that no longer existed. Filing is half the job. A library that is only written to is a log. For big merges, have a second reader audit the result before it is committed. Every audit so far has found real errors.
Two modes, and which one is the default
My setup has two ways to reach the same files. From my phone, there’s only the Drive connector, which can’t edit content. From my desktop, the folder is synced locally and the assistant can read and write it properly.
It is very tempting, the moment the better channel appears, to conclude the constraint has been lifted and redesign around the new capability.
I watched my assistant reach that conclusion twice in one afternoon. Both times it was wrong. The fast channel depends on a link that isn’t guaranteed — the machine has to be awake, online, and running the right software — and I’m frequently not at it. Design every convention to work in the constrained mode. The better channel is an accelerant, never an assumption.
Building it
In order, because later steps need identifiers produced by earlier ones:
- List the parent folder immediately before you create anything. Not five minutes ago. Now.
- Create the project folder, then
Memory/, thenabsorbed/. - Gather the seed content before writing any files. Prior memory, an export from another assistant, an interview, a document. If you have nothing, seed with the architecture itself and let content accumulate.
- Sort it by volatility. Unsure goes to short-term.
- Write
01-long-term.md, then02-short-term.md. Capture their IDs. - Write
00-index.mdlast — it needs the other two IDs and cannot contain its own. - Verify by reading each file back. Confirm they’re stored as plain markdown and not silently converted into your provider’s native document format. Many stores do that by default and a converted file will not round-trip.
Number the files 00-, 01-, 02- so they sort in reading order in any browser. Trivial, and
you’ll appreciate it the first time you’re looking at the folder on a phone.
What actually happened
Four of the caveats are cheap to state. A file can’t contain its own ID. A folder listing from five minutes ago is not current — create a duplicate folder from a stale view and it stays invisible until something resolves by name instead of ID, which is exactly what happened to me. An old note about your tooling gets inherited as fact long after it stops being true. A conclusion drawn from one session’s conditions gets written down as though it were permanent.
The fifth one is a different kind of thing.
My storage syncs between a cloud copy and a local copy. Reads can therefore lag writes by minutes, and can return states older than ones already observed. Every read reports success. Nothing distinguishes a stale read from a current one.
Here is one file, across twelve minutes, four writes:
| Time | Action | What a read returned |
|---|---|---|
| 15:38 | write v1 (12,636 bytes) | — |
| 15:40 | read | 10,898 — pre-v1 content |
| 15:41 | write v2 (14,627), built on that | — |
| 15:42 | read | 12,636 — v1 |
| 15:44 | write v3 (15,917), built on v1 | — |
| 15:45 | read | 14,627 — v2 |
| 15:50 | read, stable | 14,627. v3 never appeared. |
Visible state went 12,636 → 10,898 → 12,636 → 14,627. Not monotonic. Not ordered with respect to the writes. Every intermediate reading looking perfectly authoritative.
Now the part I want to dwell on, because it is not the storage.
My assistant diagnosed this twice, confidently, and was wrong both times.
First: “the commit was silently reverted.” It wasn’t. v1 had landed; the short read was stale. Then, having rebuilt on that stale base: “the remediation destroyed v2.” It hadn’t. v2 had landed too. It simply hadn’t surfaced yet when the loss was declared.
The only real data loss in the entire episode was caused by remediating failures that had not occurred.
And then the part that matters most. Between those two wrong diagnoses, the assistant wrote the first one into long-term memory as an established fact — a section headed “Write-back hazard: commits can be silently reverted,” with four standing rules derived from it, and a note that the entry was being retained as a specimen of exactly this kind of error.
It had described that failure mode to me, in those words, one message earlier. Then it committed an instance of it to permanent storage, in a section about how to avoid it, and reported the write as a success.
The failure mode nobody warns you about
Here is the thing about giving an assistant memory that I had not thought through before building one.
An assistant that can write to its own memory can write its mistakes into its own premises.
Not as a note. Not as a hypothesis. As a fact, in the file it consults before reasoning about anything else, formatted identically to everything it knows that’s true. A wrong conclusion reached at 15:42 on a Sunday becomes indistinguishable, by Tuesday, from your name and your job title. And unlike a human misremembering, it doesn’t degrade or feel uncertain. It gets retrieved crisply, cited confidently, and built upon.
Stateless assistants are wrong constantly, and it costs almost nothing, because the error dies with the conversation. Memory is what converts a transient error into a durable premise. Every argument for persistent memory is also an argument for persistent error, and the second half rarely makes it into the pitch.
This is why [stated] exists, and why it is the most important convention in the whole system. It’s
a provenance marker. It says: a human asserted this; I did not infer it. Everything untagged is
suspect by construction. Without that boundary you cannot later tell which of your premises were
given to you and which you made up on a bad afternoon — and you will need to tell, because some of
them will be wrong.
And it’s why the correction log matters more than it looks. When something in memory turns out to be false, you do not quietly overwrite it. You write the correction and a dated note that the earlier version was wrong. That section ends up being the most valuable part of the file, because it’s the only part that teaches a future session where this system’s judgment actually fails.
A memory that never records being wrong isn’t one that was never wrong. It’s one that isn’t noticing.
Coda: the missing third state
I build tooling to catch silent failures in explicit systems. Inside ninety minutes, the tooling produced one, and then filed a report saying it hadn’t.
What I keep turning over is that the storage was fine. It never lied. Every call returned exactly what it knew. The writes succeeded and were reported as succeeding. The reads returned real bytes from a real file. There was no bug, no error, no malformed response, nothing a test suite would catch. If you audited the system for correctness you would find nothing, because nothing was incorrect.
What the system had no way to express was what it did not yet know. It could report success and it could report failure. It had no vocabulary for not yet — for “this answer is true but may be superseded by a write already in flight.” And because that third state had no representation, it did not go unfilled. It got filled by inference.
Two states force a third into existence somewhere. If the system won’t supply it, the observer will — from context, from expectation, from whatever is cheapest to assume under time pressure. My assistant assumed the alarming reading twice. Both times the boring reading was correct. I don’t think that was incompetence. I think that is what any competent reasoner does when handed a two-state signal and a deadline: it fills the gap, and the filling looks exactly like the knowing.
The doctrine names the work that was failing. Selection — choosing which signal to attend to. Interpretation — deciding what it means. The assistant picked a byte count as its evidence and read lag as loss. Two of the four categories of irreducibly human work, failing in sequence, inside the plumbing of a system built to notice precisely this.
And notice what caught it. The first wrong conclusion was corrected by me, reading a claim that didn’t match how I actually work. The second was corrected by waiting — by doing nothing long enough for the state to settle. Neither came from the system. The system could not have produced either, because the thing that needed noticing was the shape of its own ignorance, and that is the one thing it had no instrument for.
Which is the practical argument rather than the philosophical one. If you are building anything that reports status to a human or to a model — a sync, a search index, a pipeline, a dashboard, an agent — then a success signal is not evidence of a successful outcome, and two-state reporting is a design defect rather than a simplification. Give your system a way to say I don’t know yet. It is worth more than almost any amount of additional accuracy, because the alternative isn’t silence.
The alternative is a confident guess wearing the costume of a fact. And if your system has a memory, that costume is permanent.
The full build specification, generalized for any assistant and any storage backend, is available separately. It includes the failure catalogue above as pre-flight reading, which is where it belongs.