Series · The Quiet Machine · Part 8

Claude Code Lands, and the Memory Framework Gets a Cleanup

Installing Claude Code on the Windows box was one line:

npm install -g @anthropic-ai/claude-code

Anticlimactic. My local-first memory framework, the thing Claude Code would be driving, carried a bug that had survived every read for weeks: an environment variable resolver silently pinning one tool to localhost while I congratulated myself on offloading everything to the box’s GPU.

The framework itself is the part of this project I’m proudest of.

$0, fully local, and no generative LLM in the mechanical path

The framework is a personal RAG, grounding, and persistent-memory stack: seventeen small Python tools over one SQLite file and a local Ollama instance. Nothing leaves the machine, and no API key touches the hot path. The whole thing costs zero dollars a month to run, and the design rule that makes it trustworthy is a single line I refuse to cross:

No generative LLM in the mechanical path.

That distinction is load-bearing. Embedders and BERT cross-encoders are discriminative scorers (they rank, they don’t invent), so they’re allowed.

What’s banned is a chat model anywhere near indexing, verification, or state-writing, because a generative model that “helpfully” paraphrases your facts is a memory that corrupts itself and never tells you. I’d rather have a dumb deterministic write that never forgets.

  • Tier 0: deterministic, $0, no model at all. Doc↔code drift verification, near-duplicate detection, verbatim-faithfulness QC, the SQLite store itself, the health doctor. This is the bulk of the work.
  • Tier 1: local embeddings, near-free GPU. nomic-embed-text turns the doc vault and the codebase into vectors for semantic search.
  • Tier 2: local generation, near-free GPU. A 14B model does structured extraction, git-diff summaries, and bulk translation.
  • Tier 3: cloud Claude, minimized. The expensive model only ever adjudicates a small review queue, the judgment calls the deterministic layers couldn’t resolve alone.

Clean it while you mirror it

I was mirroring this framework onto the box anyway: synthesize → clean → improve → match. Read the whole thing end to end, research what’s changed since I wrote it, and port only the improved version, every pass grounded against the actual code, not vibes. Three upgrades shipped that night.

A: the env bug that explained everything

For weeks I’d told myself the stack ran on the box. Four of the five Tier-1/2 tools read OLLAMA_HOST and built their URL from it, so pointing that one variable at the box moved them.

But the indexer read a different variable, OLLAMA_URL, as a full URL, and ignored OLLAMA_HOST entirely. So every re-index kept embedding on the Mac itself, with no error to say so, while the other tools hit the box. That single inconsistency was the real reason “just connect it to the box” had never finished.

The fix: unify all five tools onto one resolver, so one variable moves the entire stack. The bug was embarrassing because it was invisible: nothing errored, it just did the slow, wrong thing.

B: Tier-2 to qwen3:14b, non-thinking

The three generation tools moved off the older 14B and onto Qwen3-14B, inside the same ~9GB Q4 footprint. The trick for a mechanical pipeline is forcing non-thinking mode:

# in the /api/generate body
{"model": "qwen3:14b", "think": False, "temperature": 0.1}

For verbatim extraction you don’t want the model narrating a <think> monologue into your output; you want clean, deterministic text that survives the Tier-0 faithfulness check. It did: clean “OK”, no reasoning leakage, full suite green at 27/27.

C: killing a mis-calibrated blend

My retrieval had a smell: results were fused as 0.75 * cosine + 0.25 * lexical. Cosine is bounded [0,1]; the “lexical” leg was a raw term-overlap count on an unbounded scale.

Mix a bounded and an unbounded signal with fixed weights and whichever raw magnitude is bigger silently wins, a textbook calibration bug. The fix is Reciprocal Rank Fusion, scale-free and tuning-free: it only cares about each doc’s rank in each list.

score = sum(1.0 / (60 + rank) for rank in (vec_rank, bm25_rank))

That required a real sparse leg, so I added an FTS5 bm25() index alongside the vectors, and hit a gotcha almost immediately. My first attempt used an external-content table, the elegant option that mirrors your main table without duplicating text. On delete, its trigger threw:

sqlite3.OperationalError: database disk image is malformed

The external-content contract is fussy about trigger ordering, and mine tripped it. Rather than fight it, I went standalone FTS5, a plain mirror table I keep in sync explicitly. Less clever, and it never throws.

After the rebuild the indices matched exactly (4309 = 4309 docs, 24217 = 24217 code chunks), and a code query pulled back the exact file I was hunting for, right at the top.

Same restraint as the BIOS tuning earlier in this series: exact brute-force cosine loop, no ANN index. At this corpus size (4309 docs) brute force is exact: 100% recall, zero tuning, at least until the corpus outgrows it.

What’s left after the cleanup

Somewhere in that cleanup, the thing stopped feeling like “one project’s tool.” Strip the project-specific vault path and what’s left is a general, reusable, local-AI setup: a tiered, $0, private memory-and-retrieval brain any of my work can plug into, on hardware that used to sit idle.

The cloud model became a scalpel I reach for on purpose, a few times a day at most.

The box’s next assignment had nothing to do with memory: turning it into a self-hosted GitHub Actions runner, so the same machine that had spent its days serving embeddings could start doing real CI work too.