Your AI agents' memory is a database
Memory shared by many agents across sessions quietly hit five classic database failures — bloat, concurrent writes, orphaned rows, schema drift, synchronized expiry. One was a single filename-compare away from silently overwriting the wrong records.
The problem
I build with AI coding agents — often several Claude Code or Codex sessions at once, each spawning sub-agents. The Claude sessions and their sub-agents share one persistent memory: a pile of plain text files the agents read and write across sessions, so a thing learned on Monday is still known on Friday. (Codex keeps its own store; cross-vendor facts travel through files in the repo.) It worked well enough that I stopped thinking about it. That was the mistake.
Memory isn't a notebook you jot in. The moment more than one agent reads and writes it across sessions, it is a database — and a database with no constraints is just a corruption waiting to happen. Over a few weeks one memory store quietly hit five classic database failure modes: bloat against a hard budget, concurrent writers, orphaned rows, schema drift, and synchronized expiry. None of them announced itself. One of them was caught one filename-comparison away from silently overwriting the wrong records.
This is the catalog — each failure, why it happens, and the fix — because if your agents remember anything across sessions, you have this database too, whether you're treating it like one or not.
How the memory is shaped
Two layers. There's one index file — a flat list of one-line pointers, one per record — and it is loaded into every session's context window, every time. Then there are the topic files: one topic each, a hundred-plus of them, holding the real detail (some a single line, some a few KB of accumulated context). Those are not loaded up front; an agent pulls the handful it needs on demand, by following a pointer from the index.
That split is the whole game, because context windows have a hard ceiling. Only the index spends context: it is capped at a couple of dozen kilobytes and loaded every session. The hundred-plus topic files (well over half a megabyte in total) cost nothing until they're recalled. Get the split right and memory scales for free. Get it wrong and the index eats your context window before the session even starts.
The index spends your context budget; the topic files don't. Two of the failures below are this boundary eroding; the rest are controls a database has and this store didn't.
The five failure modes
Here is the part worth stealing. Each one is a textbook database problem wearing a memory costume.
1 · Bloat against a hard budget
The index is supposed to be one short pointer per fact. But warnings are tempting to inline — "don't re-flag X, it's by design" reads as safer sitting right there in the always-loaded index than hidden in a file nobody opens. So pointers grow into summaries, summaries grow paragraphs, and the index drifts toward its ceiling. This is the working-set-versus-cache-budget problem: you can't fit the whole hot dataset in a fixed cache, so you must decide what's truly hot. The fix is a discipline, not a tool — the index holds a pointer (a hard length cap per line), the detail lives in the topic file, and you trust the recall layer to fetch it. Trimming one bloated index pulled it back under budget with room to spare.
2 · Concurrent writers — the one that nearly bit
Two sessions decided to tidy the same index at the same time. One was running a big edit keyed by line position — "replace line 14, line 22, line 30…". While it worked, the other session was also trimming, so lines shifted: the file went from 25KB to 22KB to 20KB underneath the first edit. Now line 14 wasn't the entry the edit had read — every position was pointing at a different record than when the plan was made. A position-keyed write into a file someone else is editing is the lost update problem in its purest form, and it was about to write dozens of updates onto the wrong rows.
What saved it was a cheap integrity check: before each write, compare the filename the pointer targets against the filename the edit expects. They no longer matched, so all 48 writes were refused instead of applied. That's optimistic concurrency control — verify the record is what you think before you commit — reinvented in a panic. The real fix is the same one databases settled on decades ago: don't key by position, key by a stable id; re-read the record right before writing; and prefer a single writer over a clever merge.
3 · Orphaned rows
That aggressive trim had a side effect: it dropped a batch of pointers out of the index while leaving the topic files on disk. A topic file with no pointer is unreachable — recall follows pointers, so a de-indexed file is a row no query can return. Dead, but still taking up space, and worse, invisible: some of the orphaned files were active work. This is a broken foreign key — a row whose parent reference was deleted. The fix is a referential-integrity sweep: list files on disk, subtract the ones the index points at, and for every orphan decide re-index (it's live) or delete (it's superseded). Don't let the set drift unaudited.
4 · Schema drift — the most dangerous one
The memory format changed over time. Older files tagged their metadata one way; newer files used a different shape. Fine — except the little audit that scans for stale rules had only ever learned to read the old shape. So when more than half the files migrated to the new format, the audit simply didn't see them. It wasn't erroring. It was reporting a clean, confident "0 stale" — while blind to over half of memory, including a stack of behavioral rules. A reader that silently skips records it can't parse, and a migration with no backfill, is a schema-drift outage. The audit was fixed to understand both shapes; a freshness date now comes from version-control history rather than a field that may not exist. The lesson generalizes past memory: a monitor that can't parse new data doesn't fail loudly — it passes silently, and a green check you can't trust is worse than a red one. The only defense is to test the check against known-bad input and confirm it actually fires.
5 · Synchronized expiry — the cliff
Most of the memory was seeded on a couple of busy days. The staleness rule flags anything older than thirty days for review. Put those together and the report is a cliff: near-zero stale for weeks, then dozens crossing the line within a day or two of each other — and the reflex, "renew them all," just rebuilds the same cliff thirty days out. This is a TTL thundering herd: a fleet of keys created together, expiring together. The fix is jitter — stagger the review dates, judge freshness by last touched rather than first created, and renew one rule at a time when it's actually still load-bearing, never in a batch.
| Database failure | How it shows up in agent memory |
|---|---|
| Working set > cache budget | Always-loaded index bloats past its context ceiling |
| Lost update / stale positional write | Two sessions trim one file; position-keyed writes land on wrong rows |
| Broken foreign key (orphan row) | De-indexed topic files unreachable by recall — incl. live work |
| Schema migration without backfill | Audit reads only the old format → false "0 stale" over half of memory |
| TTL thundering herd | Bulk-seeded memories all expire on the same day |
The principle, and a checklist
The single idea under all five: if an agent will remember something across sessions, give the store database primitives, not better prose. Stable keys. A schema version. Referential-integrity checks. Migrations that backfill. A conflict check before each write. A TTL policy with jitter. None of this is exotic — it's the boring discipline a DBA brings, applied to a directory of text files an LLM happens to be the client of.
The checklist I now run memory against:
- Pointers, not prose. The always-loaded index is a list of short pointers with a hard length cap; detail lives in on-demand files.
- One writer, re-read before write. Edit by stable id, never by line position; re-read the record immediately before writing; let the active maintainer finish rather than racing it.
- Sweep for orphans. On a schedule, diff files-on-disk against what the index points at; re-index the live, delete the superseded.
- Make the reader schema-aware — and prove it. Parse every format version; then feed it a known-stale fixture and confirm the check actually fires. A silent pass is the failure.
- Jitter the expiry. Judge by last-touched, jitter the review dates, and never bulk-renew.
One honest caveat. "Trust the recall layer" is a real bet: it assumes the lazy layer surfaces the right topic file at the right moment. When it doesn't, a fact exists but might as well not. That's the open risk of this design, and naming it is part of running it — the same way a cache is only as good as its hit rate.
I'm building BluffKing — a browser poker game made almost entirely by a fleet of Claude and Codex agents — in public, and this is the kind of plumbing that decides whether that fleet stays coherent or quietly rots. If you run long-lived coding agents, audit their memory for these five before one of them bites you. Follow along if you're building this way too.