I run a paid infrastructure service. Alone. No co-founder, no on-call rotation, no senior engineer to escalate to. My only collaborator is Claude Code, and after about a year, my persistent memory has grown to 60+ entries.
Those entries have become more valuable than any runbook I've written. They've also taught me — painfully — what makes memory architecture work and what makes it quietly fail.
If you're running anything solo with an AI agent, here are five lessons I wish I'd burned into my brain on day one.
1. Write the why, not the what
The first instinct when you start using persistent memory is to log what you did. "Migrated service X from tool A to tool B." "Switched protocol from X to Y."
Six months later, when something breaks, that information is worthless. You don't need to know what you did — git log and git blame already tell you that. You need to know why you made that choice. What constraint forced it. What you ruled out.
Real example. The bad version of an entry I once wrote:
Switched the worker pool from Docker containers to systemd units on host.
Tells me nothing my git history doesn't. The rewritten version:
systemd units on the host instead of Docker containers on this VPS provider. Why: the provider runs aggressive kernel-wide OOM scoring across tenants; containers were getting reaped by oom-killer triggered by other customers' workloads. systemd processes survive because they're scored as system processes. How to apply: any VPS where
dmesg | grep -i oomshows kills from PIDs you don't recognize — don't run containers there, run systemd.
That one entry has saved me three rebuilds. Because the next time I'm tempted to "just dockerize it, it'll be cleaner," the memory entry says: no, you already learned this, you'll be back here in a week.
The pattern: always include Why: and How to apply: lines. If a memory entry can't answer those two questions, delete it.
2. Memory rots — prune or pay
About six months in, I did a memory audit. Of 60 entries, 14 referenced things that no longer existed. A file path that had been renamed. A function that had been deleted. An architecture I'd since replaced.
The cost wasn't just irrelevant context. It was actively misleading context. The agent would surface a memory entry that pointed to a file path, the file wouldn't exist, and the agent would either dead-end or fabricate.
I tried scheduled audits. Didn't work — I'd skip them. What worked: every time I read a memory entry mid-task, I make a snap call — still true? If yes, leave it. If no, edit or delete in that moment.
Memory isn't a backup. It's a live dependency. You either keep it in your operations loop or it becomes lore.
3. Feedback memories are the highest leverage. And the hardest to write.
My best memory entries don't describe systems. They describe what not to do.
"Don't run X on Y." "Avoid this approach because we tried it last quarter and it cost three nights of debugging." "This UI shortcut looks fast but breaks under condition Z."
These entries save the most time because they prevent re-discovering the same trap. But they're also the entries I have to actively force myself to write. Because when you've just spent four hours debugging something, the impulse is: I fixed it, I'm done, ship it. Not: let me sit here for another five minutes and document what I almost didn't notice.
I now ask, after any frustrating debugging session: did I just learn something my past self would have wanted to know? If yes — entry. Even if it's two lines.
These are the entries with the highest savings per character written. They are also the easiest to skip.
4. The index entry decides retrieval. Not the file.
I keep a MEMORY.md that's an index — title plus a one-line hook for each memory file. After dozens of entries, I realized something uncomfortable:
The index entry matters more than the file content.
Why? Because the index is what the AI sees first. The body of the file is only loaded when the agent decides to read it. And the agent's decision is based entirely on the index description.
If webhook_replay.md is described as "webhook stuff", the AI never opens it. If described as
Stripe webhook replay: idempotency-key collisions vs. event_id reuse; what to do when a partial DB failure leaves the worker in an unknown state; the one query that tells you if it's safe to re-fire
— the AI opens it in exactly the right moments.
The memory file might be 500 words. The index entry is 100 characters. The 100 characters do all the retrieval work.
When I write a new memory entry now, I spend more time on the index line than I do on the file body. This feels backwards. It isn't.
5. Indexes have a context budget
This one I had to learn the hard way: my agent truncates MEMORY.md after a certain line count. For me, lines after 200 silently drop out of context. They might as well not exist.
This means MEMORY.md isn't just an index — it's a layout problem. Which 200 lines deserve to live in every conversation? Which entries are good enough to keep on disk but not critical enough to load every time?
Pruning isn't only about deleting things that became wrong. It's about deciding what earns context.
My rule now: if I haven't read or used a memory entry in two months and it's not load-bearing, the index line moves down or out. The file stays on disk for archaeology. The index line is sacred real estate.
What I'm doing with all this
I'm packaging the schema I converged on — the memory templates, the MEMORY.md layout, the prompts I use to trigger high-signal writes, the audit rituals — into a kit for solo operators using Claude Code.
It's the kit I wish I'd had on day one. It's at solosre.dev.
If any of these failure modes feel familiar — if your AI memory has started to drift, mislead, or quietly stop pulling its weight — there's a structured way out of it. You don't have to learn this through 60 entries the way I did.
If you're running solo with Claude Code (or any agent with persistent memory), I'd love to hear what's worked and what's broken for you in the comments. The lesson nobody else will tell you is usually the one that saves the most pain.
Top comments (7)
Lesson 4 is the one I'd put on the wall, and it quietly undoes lesson 2. Files rot loudly: a stale path dead-ends and you catch it on the next read, which is exactly why your read-time pruning works. The index rots silently: a wrong one-line hook doesn't dead-end, it just makes the right file invisible, so that entry never gets opened and never gets the read-time check that would catch it. That makes the index both your highest-leverage and your highest-risk surface, and the one spot your audit-on-read habit can't reach. The cheap fix I landed on is to treat retrieval itself as the audit: when the agent does open a file, log whether the hook predicted what was actually inside, and the hooks that keep pulling the wrong file (or never pull a relevant one) become your rot signal, so index upkeep turns into a byproduct of normal work instead of the audit you skip. I built this exact shape out as cowork-os: it's for Claude Cowork, but it's plain Markdown so it ports straight to a Claude Code setup like yours, an index plus files, decisions that carry a status, and a Memory Update step that forces the reconcile at the end of each task. When a hook goes wrong, can you tell from the index alone, or only after you've opened the file and noticed the mismatch?
Silent index rot is the gap I left implicit. To your question: only after opening — index alone won't tell.
Curious how cowork-os anchors retrieval-as-audit's ground truth — when hook and body share description-language, the comparison risks circularity.
You are right that it is circular if the hook is the ground truth. The hook and the body are the same author in the same language, so comparing them only proves self-consistency, not that the entry still matches the world. So in cowork-os the hook is never the audit's truth, it is just retrieval, the thing that gets the right file open. The truth has to be the body checked against whatever the entry points at outside itself: the commit, the path, the config, a number with a source. That external referent is the only non-circular anchor, because it is the one part the same mind did not author as prose.
The shape is concrete: each entry carries a status and a pointer to that referent, and the Memory Update reconciles the body against the referent, not the body against its own hook. Honest limit, and I would rather say it than oversell: this is a human in the loop reconcile, not an automatic diff, and an entry with no external referent (pure judgment, nothing underneath it) has no anchor, so rot can still hide there. That is the boundary I have not closed yet.
It is MIT and plain Markdown if you want to lift the index plus status plus pointer shape into your Claude Code setup, and a star helps me know it is useful to someone running this for real. Out of your 60+, what fraction actually point at something external you could diff against, versus entries that are just your own prose with nothing underneath to catch them?
"Entries below this threshold effectively don't exist" is the line that lands hardest — that's stale-as-absent semantics arrived at from the memory-ops side rather than the decision-store side. Same shape: things you've stopped retrieving aren't pruned, they're functionally superseded without an explicit marker. Cheap at read time, expensive at cold start when you re-propose the thing past-you already rejected and present-you can't see why. The fix I keep landing on is making the obsolescence explicit at write time — a superseded_by pointer on the entry that fell out of retrieval, so the trail stays inspectable even when the live read ignores it. Active decay management is the right frame; the question is whether decay is silent or leaves a footprint.
"Stale-as-absent" is exactly the term I was missing.
Using an explicit superseded_by link would replace my current demote-and-forget strategy. When a new entry appears, it can bring the deprecated entry into context and mark it as superseded, instead of letting it silently disappear over time.
I'll give it a try.
"Stale-as-absent" works cleanly with explicit supersede because the deprecated entry stays queryable-by-lineage — you can answer "what replaced X and why" months later without it cluttering active reads. One watch when you try it: supersede chains (A → B → C) need to resolve to the latest from any link, otherwise old code paths re-anchor on a stale middle. Ping if it shapes up — curious what the surface area looks like in real use.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.