Cairn Collects, Still Distills, the Vault Keeps
Every AI tool remembers its own chats and nothing across surfaces. So I built my AI a memory: a raw substrate that never summarizes, a nightly dream cycle, and a curator that deletes.
I use AI heavily. Claude Code in the terminal, Claude chat in the browser, plus a few years of Gemini and ChatGPT history sitting in exports. Every one of those tools remembers its own conversations. None of them remembers across surfaces, and the surfaces are where the thinking actually happens. So I was the integration layer. Context-switch between the terminal and the browser five times in a session and “let me re-explain” starts costing more than the actual work.
I built my AI a memory. It took three custom services, one open-source one, and a lot of arguing with myself about whether any of it was necessary. This post is the whole system, including the parts that broke.
It started with an insult to mem0
mem0 was not my first attempt at this. I started chasing persistent memory for personal AI back in August 2024. Plain markdown files first, then markdown with routing - which is where most people I know stop, and honestly, it works for most cases. I tried Mem next, looking for a dedicated memory tool at a point when almost nothing integrated cleanly with AI systems. It was too limited. Then mem0, which helped a lot. Each step was better than the last, and each one eventually hit a wall.
The wall for mem0 shows up in the record as a note from the night of May 3, 2026: “mem0 has bit us a couple times tonight. I honestly think it helps things overall, but sometimes limited… should we create something specifically for my memory needs?”
Immediately followed by the correct skepticism: is there something off the shelf? I’m already paying for Linear. Do I really want a third memory service?
What tipped it: I had already built this concept once, at my day job - a .NET/SQL Server app that tracked my Claude Code activity and generated weekly accomplishment reports. The schema, the hook points, “what counts as an event” - that intellectual cost was paid. The question was never “can this work.” It was “is this justified.”
The insight: raw is a feature
mem0 (the open-source memory layer a lot of people use) summarizes on the way in. You hand it text, it runs an LLM, extracts the facts worth keeping, merges them with what it knows. For facts, that’s exactly right.
For a stream of thought, it’s fatal. Once you compress at ingestion you can never recover the texture - the dead ends, the exact phrasing of a decision, the argument you almost won with yourself. The moment I realized “you’re not building a journal, you’re building the corpus your reporting already requires” the design locked in.
One non-negotiable rule: store raw, summarize only at query time.
That rule is what makes the bottom layer not a duplicate of mem0. They do different jobs. mem0 answers “what did I conclude?” The raw layer answers “what was I actually thinking on Tuesday?”
The stack
The memory path has three layers, each distilling the one below: cairn collects, still distills, the vault keeps. Orchard is a separate portfolio index alongside it.
Cairn is the substrate. Every conversation turn from every surface - Claude Code sessions, Claude chat exports, Gemini via Google Takeout, manual notes - stored verbatim in Postgres with pgvector, timestamped, embedded, searchable by meaning. As of late July 2026 it holds 60,367 events in 1.1 GB including indexes. The corpus reaches back to August 2024, though that floor is imported history: cairn itself was built in late May 2026, and the older material came in through Gemini and Claude export adapters. Which means the corpus covers the entire two-year hunt for a memory system, including the attempts that failed. Two years of thinking, smaller than one 4K movie. It never summarizes at write time. Ever.
The original day-job use case now falls out for free. One cairn_summarize_range call over a week in July turned a pile of scattered terminal sessions into a clean report. Chased a VNC problem on the M1 (SSH was fine, ruled out the pf firewall, traced it to hotspot bandwidth), then cleared a stale 51GB iPhone backup off the M2. Mundane work across two machines on the same evening, and I didn’t write any of it down. The substrate did.
Still is the dream cycle. On a schedule it reads cairn and the raw transcripts, scores what’s actually durable (a 1-10 rubric, only score 6+ gets proposed), and stages candidates. It runs nightly plus a throttled pass after each session ends - about 160 incremental runs in the last month, and 13 of the last 14 nightlies came back clean. It also does the thing I’m proudest of, which is deleting memories. More on that below.
mem0 is the vault. Durable facts, one namespace per project or persona so my music identities don’t bleed into my SaaS projects and nothing bleeds into the day job. It’s the one component I didn’t write - self-hosted, my database, my auth, but their engine. It’s deliberately the boring dependable part. The curator’s last uncapped audit counted 917 entries across 13 namespaces, 501 of them in personal alone. Only about 2% came through the dream pipeline so far, but the pipeline’s entries are visibly different in character: long “why, not just what” writeups where my hand-saves are mostly one-line facts. Fewer memories, denser ones.
Orchard sits alongside the memory path - a small custom MCP server tracking the portfolio. 101 project records, 51 of them alive, 35 shipped, and music is 47 of the total (five artist personas will do that). One structured record each: status, blockers, next action. The write path is designed to be milestone-only, and the data backs that up, if only just. The database was seeded in one bulk load in early May. In the two and a half months since, exactly one record has been edited (a music project called “404: Soul Not Found?”, which feels right) and one new project added. Two writes in eleven weeks is either admirable restraint or a system I haven’t stressed yet. Ask me in six months. Where it already earns its keep is reads - a fresh Claude session can answer “what should I work on this week” without guessing from stale training data (a guess that is always wrong for a portfolio this size).
The flow is one-directional: cairn to still to mem0/orchard. Raw experience during the day, consolidation during sleep, durable memory after. If that sounds like how human memory is supposed to work, that’s the point. Anthropic shipped a managed version of this idea (“agents dreaming”) in May. Part of why I built mine is that I wanted that capability on my own infrastructure, on a flat-rate subscription, not rented per token from someone’s API.
Nothing writes to memory directly
The central safety idea in still: no autonomous process ever writes to a real namespace. Everything lands in a shadow namespace first (sparkletext__dream, personal__ideas), and a review gate promotes it. Because mem0 auto-creates namespaces on first write, shadows cost nothing to set up.
The worst thing an unattended run can do is pile up candidates nobody approved. That property is what lets the whole pipeline run on cron without me babysitting it.
The gate itself started as a CLI diff and grew a small FastAPI dashboard - one card per namespace with pending counts, approve/edit/skip per entry. Approving takes seconds now, which matters, because a review gate you dread is a review gate you stop using.
The backlog is 247 entries as I write this. In early July it was 4. Nothing broke - the nightlies kept staging candidates and I stopped merging, so the shadows filled up exactly as designed. That’s the tradeoff of putting a human in the loop: the pipeline can run unattended forever, but the gate only moves when I move it. The safety property held perfectly and the throughput property is entirely on me. Any system that requires human approval eventually reveals whether the human shows up.
A semantic dedup gate checks every proposed memory against the primary and the shadow before staging. Over a two-week stretch in early July it rejected 18 of 47 raw candidates, about 28%, as already-known. Before that gate existed, the synthesizer would happily re-derive the same handful of facts every single night.
One record from this week tells the whole lifecycle in its metadata. Still ran on July 2 (the dream_run_id timestamp), distilled a Claude Code session into sparkletext__dream, and the merge landed in the primary namespace on July 8. The memory itself: Next 16 renamed middleware to proxy, so the edge middleware lives in src/proxy.ts and src/middleware.ts does not exist. Exactly the kind of fact a future session would burn ten minutes rediscovering.
The gate is not airtight. The pipeline once distilled a rule about an abandoned short-link domain and merged it into the primary namespace, where a hand-saved entry from two months earlier already covered the same ground. Not a byte-for-byte duplicate, which is exactly why it slipped through: the dedup gate compares a candidate against what’s already stored, and two entries can state the same fact in different enough language to clear the threshold. The gate catches re-derivations of itself well. Overlap with things I wrote by hand, in my own phrasing, is harder. That one’s still on the list.
The curator: memory that weeds itself
A vault that only grows becomes a vault you can’t trust. Duplicates pile up, facts go stale as code changes, and someone has to weed it. If that someone is me, I’ve just rebuilt the original problem with extra steps.
So once a week, an agent audits each namespace. The part that makes it work is that the agent is situated. For each project, the system launches an ephemeral Claude Code session with its working directory set inside that project’s actual repo. It can grep the code and run git. It judges memories against ground truth, not vibes.
First live run, against a namespace with 99 memories, it:
- read the actual rate-limiter source and disproved a memory claiming replies were “uncapped” (capped at 30/min)
- found a real project id where a memory claimed a placeholder
- caught two duplicates, one byte-for-byte
The best cut deserves its own paragraph. A memory claimed Vercel deploys required a specific git author email. The agent ran git log --format=%ae, saw all 15 recent commits authored under a different address, and cut the memory with the evidence attached. That memory wasn’t stale. It was wrong, and it would have sent a future session chasing a deploy failure that didn’t exist. A transcript-only consolidator could never catch that.
Six cuts out of 99, every one with cited evidence.
At full scale, a weekly sweep audits 900-plus memories across 13 namespaces. The July 17 run looked at 917, proposed 220 cuts, and executed 76.
That same run is also where the circuit breaker earned its keep. The agent came back wanting to cut 144 of 501 memories in my personal namespace, 29%, over the 25% ceiling that namespace carries. The breaker tripped and nothing was deleted. I don’t know for certain that those 144 cuts were wrong. That’s the point: neither did the system, so it refused. A limit you never hit is a limit you can’t trust, and this one had gone months without firing before it caught something.
Deleting from long-term memory autonomously is obviously dangerous, so the safety model has four layers. The agent gets no delete tools at all - it writes a verdict file, and deterministic orchestrator code performs the cuts. The orchestrator re-verifies the namespace didn’t change under the agent before acting. A circuit breaker refuses any run that wants to cut more than 40% of a namespace (25% for personal). And every cut is archived and restorable for 14 days before a purge job makes it real. I have never invoked a restore: every namespace’s tombstone count still matches its archive count exactly, which is its own small proof that the agent’s cuts have held up.
The judgment rules matter too. Default is keep. And there’s a Chesterton’s fence rule I’d argue is the most important line in the prompt: a memory recording a past failure (“we tried X, it broke, use Y”) is kept even when the code already reflects it. Those memories look redundant precisely because they worked. They exist to stop a future agent from re-litigating a solved problem.
War stories
The lessons that cost something:
The 506 MB query. Early on the display queries used SELECT *. With a 1536-dimension vector column, that’s about 12 KB per row you don’t need. A wide date-range query was moving roughly 506 MB over the wire and timing out at 300 seconds. Fix was boring and total: explicit column lists everywhere, never select the embedding unless you’re scoring with it. Timeouts became milliseconds. With vector columns, SELECT * is a trap.
The hybrid search that couldn’t use its index. The naive hybrid ranking (0.7 * vector + 0.3 * keyword in ORDER BY) forces a full table scan because Postgres can’t use the HNSW index on a computed expression. The fix is a two-stage query: pull a candidate pool fast using the raw vector index, then rerank only that small pool with keyword relevance.
The 100-record cap. mem0’s list call defaults to returning 100 records. At the time, my personal namespace had 496. Nearly 400 memories were invisible to every list-based tool until we exposed the limit parameter. A curator that only sees the first 100 and cuts “the rest” would be catastrophic. Any system that audits memory must see all of it. The cap bit again while fact-checking an earlier draft of this article: three namespaces read back as exactly 100, producing a bogus 559-entry floor until I checked the curator’s uncapped audit.
Don’t make the model transcribe UUIDs. The first curator protocol asked the agent to return a verdict per memory, keyed by UUID. On 99 memories it fumbled one or two, and the validator (correctly) refused the whole run. Now each memory gets a small integer handle and the agent returns only the cuts, by index, with a reason. The fix is visible in the run logs: the sweep before the change hit an id-mismatch and a wedged session, the sweep after it ran all 13 namespaces clean. Models are good at judgment and bad at being clerks. Keep the bookkeeping in code.
The cost model was an authentication bug. I initially concluded that claude -p meant metered API while an interactive tmux session used my Max subscription. That was wrong. Claude Code can authenticate either against Console (metered) or against a Pro/Max subscription, and which one you get depends on credentials, not on whether the invocation is interactive. Anthropic paused a planned June 15 change that would have moved claude -p onto a separate credit, so as of now non-interactive runs still draw from subscription limits. The trap is credential precedence: in a non-interactive scheduled job, an ANTHROPIC_API_KEY sitting in the environment wins silently. Some of my headless runs were reaching Console because of how they authenticated, not because they were headless. The curator still runs in detached tmux sessions, with each task passed by file because long prompts get mangled by send-keys, but that’s an operational choice rather than a billing optimization. The durable lesson: inspect credentials inside the scheduled job and log per-run cost, instead of inferring billing from invocation mode. Plan terms and auth options change. Check yours.
Also, plain HTTP beats a model in the loop for CRUD. Vault reads used to go through a headless Claude call and take 60-120 seconds. A thin REST surface on the same server took it sub-second, which is what made running the pipeline often affordable at all.
Is any of this novel?
Partially. Memory layers for agents are everywhere right now, and mem0 alone gets a lot of people most of the way. Anthropic’s managed dreaming does the consolidation loop as a product.
What I haven’t seen elsewhere is the combination of the four. A raw never-summarized substrate under the fact layer, shadow-namespace staging in front of it, and a situated curator that verifies memories against the actual repo before cutting them. The delete performed by code instead of the model. The part I couldn’t find anywhere else was the raw substrate. Conclusions are easy to come by. The raw material those conclusions came from, searchable by meaning, spanning every machine and every AI surface, nearly two years deep, is the thing I had to build.
The origin debate about whether to build cairn is itself preserved inside cairn. While drafting this post I couldn’t remember the exact sequence, so I queried the substrate - and it was all there, captured across three different AI surfaces. The system’s origin story is a query against the system.
Open sourcing it
I’m working out what to release. Current thinking:
Cairn is the obvious candidate. It’s self-contained, the value is generic (raw substrate, dumb adapters, idempotent ingestion, an MCP query surface), and there’s no equivalent I’ve found. Still is trickier - the ideas travel but the config is my life (namespace maps, routing rules, prompts tuned to my repos), so it probably ships as a reference implementation, or maybe just the curator and the tmux launcher as extractable pieces. Orchard is shaped like my portfolio and probably stays a blog post instead of a repo.
If you’ve built something similar, I’d most like to hear from anyone whose system has run long enough to hit the deletion problem. There’s a lot written about getting memories in and almost nothing about what to do when they go stale.