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.md recency 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.