TLDR: A memory routing table is a second source of truth — and second sources lie. Build a contradiction pass, or it rots silently.
The Architecture (30-Second Recap)
Apollo (my AI agent, built on the Anthropic Claude SDK) runs a 3-tier memory system.
MEMORY.md is Tier 1: a routing table, 80-line hard cap, one-line pointers to memory files. It tells Apollo what exists and where — so it can pull detail on demand without loading everything at boot.
I even wrote it in the Excalidraw system map: "The map, not the territory."
That line came back to bite me.
Phase 2 Built the Linter
A few weeks into the architecture work, I added /mem-lint — a memory health check (a shell script plus a local-Ollama call, my on-device LLM) that runs mechanical passes (orphans, dead links) and a contradiction pass against the vault.
Phase 2 shipped clean. Moved on.
Phase 4A: The Relative-Path Trap
When the Phase 4 cleanup pass ran, the first thing it surfaced was pointers that weren't pointing where they claimed to.
The trap was subtle. MEMORY.md stores its links as relative paths — memory/feedback_*.md, memory/ref_*.md. Fine when you're reading the file manually.
But the boot skill says "read every file listed." And in a headless subprocess — like when Apollo runs inside a Claude Code context — those relative paths resolve against the wrong base.
I anchored the top-level identity files. Boot worked.
Then it broke again.
Turns out boot was still reading the relative links from inside MEMORY.md and re-breaking identically. The pointer looked fine. The resolution was wrong.
The real fix: the skill had to explicitly resolve every memory/... link against Apollo's absolute project base before reading. One extra line. Phase 4A done.
I "fixed" this twice before I actually fixed it. (Classic.)
Phase 4B: The Contradiction Pass Found Real Drift
Phase 4B was the harder half.
/mem-lint's contradiction pass came back with entries that disagreed with each other — the routing table said one thing, the pointed-at file said something else. Not broken links. Wrong links.
The mechanism: mechanical checks first (orphans, malformed paths), then Ollama reads pairs of entries and proposes patches for contradictions. Propose-only, never auto-apply — I review before anything merges.
The commits were boring. The insight wasn't.
Why This Actually Matters
If you build a routing table, you've built a second source of truth.
Second sources lie. Not on day one — on day 60, after 40 sessions of incremental updates, when an old pointer still says the thing it said in April and the actual file has moved on.
A map that's wrong is WORSE than no map. Apollo would read the pointer, believe it, and navigate confidently toward a file that contradicted it.
/mem-lint isn't glamorous. It runs in under 10 seconds and mostly comes back clean.
But when it doesn't — that's the rot it caught before Apollo navigated by a lie.
Build a linter for your routing table. Or skip it, and find out the hard way what "the map is not the territory" actually means.
P.S. The full Phase sequence:
hot.mdrecency cache →/mem-lint→ rerank eval (hard no from the gate) → pointer cleanup. Phase 3 stung — but it saved me weeks of index overhead for zero measured recall gain. Sometimes the right ship is the one you don't build.