The quest
The quest

Engineering log · Sovereign local tier

The quest to Gemma 12B.Investigations that never leave my laptop.

The quest Run Spotlight's full gated pipeline — orchestrator, investigator, fact-checker, human gates — on a 48 GB laptop and a 12-billion-parameter open model. No API calls. No per-token bill. Nothing leaves the machine.

Spotlight has shipped with local models from the start — but speaking candidly, they never worked well for me. Capable models wanted a bigger machine than my 48 GB MacBook, and the ones that fit drowned mid-investigation.

So I embarked on a quest. Weeks of iteration and an embarrassing number of tokens later — it happened. Along the way it threw off efficiency leaps any newsroom can reuse: workflows that repeat, costs that stay flat, sources that never leave the room.
This is how.
👇

The scoreboard · same 12B, same laptop

Before → after the quest

Turn 1 — time to a correct, gated investigation brief
205 s
74 s
2.8×
Time to first token, per conversation
~34 s
~0.46 s
74×
Full gated pipeline
stalls in loops, dies mid-methodology executes research cycles, stops at gates
A research cycle, end to end
hours — or never minutes

How each number happened is the story below.

Six unlocks. Each one forced by the failure the previous one exposed. The pattern behind all six: almost every "the model can't do this" was an engineering problem wearing a capability costume.

01
The context floor

Dynamic skill loading — collapse the floor

Where it started: the frontier-shaped harness loaded every skill body and the full tool catalogue before the first user word.

  • The damage: ~55,000 tokens of floor. ~34 s to first token. The context window half-spent at "hello".
  • The fix: dynamic loading. Skills are discovered by name + description; bodies load on invocation.
Context floor — tokens before the first user word
~55,000 tok
~817 tok
67×smaller
Prompt processing — time to first token
~34 s
~0.46 s
74×faster

(Paying a cloud per token? The same change is ≈25× cheaper per turn.)

Unlocked 67× smaller floor 74× faster first token
02
The RLM

But it wasn't enough. The 12B still collapsed. Every scrape dumped ~40,000 tokens of boilerplate into a context the model stops behaving in past ~16k.

Scrapes never enter context raw

So I dove into the RLM — a research language model: a second, tiny model living beside the orchestrator (Gemma e4b on its own llama.cpp). Its whole job: read the noise so the investigator doesn't have to.

  • The problem: a scraped page ≈ 40,000 tokens of chrome around ~600 tokens of leads.
  • The fix: every fetch pipes through the RLM automatically → a compact, source-linked lead list. The raw page stays on disk as a .raw sidecar — compression never destroys evidence.
  • Tip for builders: wire distillation into the runtime, not the skill list. Anything a small model must do to survive shouldn't depend on its judgment — that's a harness job.
Early benchmark — Wikipedia: Ethereum Foundation, in-harness
172,402 B raw
1,919 B distilled
−98.9%per fetch

Double duty: the conversation itself was crashing the model

Lean fetches weren't the whole story. The transcript alone — reasoning, delegations, tool results — was enough to take the 12B down after a single gate. The RLM fixes that too: past a fold threshold it rewrites the conversation into a structured checkpoint. The phase artifacts on disk are the state; the transcript is disposable.

Orchestrator context at a stalled gate — measured on a real run
40,180 tok · thrashing
~10,200 tok after the fold
smaller

The model that had been looping uselessly delegated correctly on its very next turn. Depth kills discipline; compaction restores it.

flowchart LR
  F["fetch / search / browse
raw page ~40k tok"] --> R["RLM · Gemma e4b
resident on llama-server :8095"] R --> L["source-linked lead list
~600 tok"] --> A["agent context"] F -. "kept on disk" .-> P[".raw sidecar
provenance + citations"] T["conversation past fold threshold
12b ~16k · 26b ~24.5k · 31b ~28.5k"] --> R R --> K["structured checkpoint
goal · progress · decisions · next"] --> A
drag to pan · pinch / ⌘-scroll to zoom
One tiny resident model, two jobs: distill everything fetched, fold the conversation at thresholds.
Unlocked −98.9% per fetch 40k → 10k folds
03
The harness

😑 Still not enough! Fetches were lean — but the failure log kept filling. And most of the "model failures" weren't even the model. The harness was lying to it.

Harness engineering — one harness, real subagents

So I dove into the harness — a Flue app built on Pi.

Four failure classes, four structural fixes:

Delegation
prompt-promised — a 12B recurses task→task→task until the run dies real child sessions + a never-re-delegate guard
Tool calls
wrong path resolution → the model falls back to raw curl and looks stupid abstract verbs bound to absolute paths by a runtime adapter
Crashes
a crash or a closed lid lost the investigation SQLite-durable sessions — the same id resumes at the gate
Eval honesty
dev harness ≠ shipped harness → phantom "model failures" one harness — the app the evals test is the app you install

And the context overhead dropped again. The OSINT tool catalogue used to sit in permanent context — 11,800 tokens the orchestrator re-read on every single turn. It's now a SQLite full-text index on disk: one shell query when the model needs a tool, zero tokens when it doesn't.

Tool catalogue — tokens carried into every turn
11,800 tok, always in context
0 tok — 12,500 tools queried on demand
→ 0resident tokens
flowchart LR
  U["Reporter
(gates)"] <--> O["Orchestrator
12B · tuned"] O -- "task" --> I["Investigator
child session"] O -- "task" --> F["Fact-checker
child session"] I -- "fetch / search / browse" --> S["Integration seams
Crawl4AI · SearXNG · dev-browser"] S -- "distill" --> R["RLM · e4b
own llama.cpp"] O -. "compaction summaries" .-> R O === LS["llama-server :8080
2 resident slots"] R === LR["llama-server :8095"]
drag to pan · pinch / ⌘-scroll to zoom
The sovereign topology: two llama.cpp servers, three agent contexts, one human.
Unlocked 3 isolated agent contexts eval == production catalogue → index
04
The tune

😤 STILL. NOT. ENOUGH!! The plumbing was honest, the context lean — and the model WOULD NOT hold the procedure. Re-activating skills it had already read. Researching instead of delegating. Blowing straight through gates!

Model fine-tuning on Spotlight procedures

Here I learned about procedure tuning — a different animal from the capability tuning I'd always done:

  • Capability tuning teaches knowledge: domain facts, formats, styles. Useless here — Gemma-4 12B already knows what a commercial registry is.
  • Procedure tuning teaches behaviour: activate the methodology skill once · advance on a bare "approved" · delegate instead of researching itself · stop at the gate. Behaviours are learnable from a tiny dataset — if the data is shaped right.
  • What went into teaching it:
26 training examples 14 real gold trajectories 12 targeted correction continuations real runs · human-approved gates assistant-masked loss ~$10 of rented GPUs

How many orchestrator submissions to finish?

Tuned 12B · v5 pass after guarded recovery 9 · pass
26B-A4B pass after one report-phase bounce 11 · pass
31B reference clean full-pipeline pass 6 · pass

Same real Ethereum Foundation investigation · lower is better · every gate approval and guardrail-recovery turn counts. The base 12B reached 35 submissions with report repair still unfinished. Wall time for the successful RunPod runs: tuned 12B v5 35.5 min; 26B 20.5 min. The 31B duration was not captured.

v1 QLoRA · full-text loss · 3.00 Learned the style — 2.8× faster to a correct first-turn brief — then stalled the pipeline. Tool outputs dilute the signal. ✗ stalled
v2 assistant-masked · 0.79 Learns decisions, not tool noise. Then an adapter merged in a mismatched stack produced garbage GGUF. ✗ corrupted merge
v2-fixed matched train/merge stack Coherent again — a milder stall, and every eval learning banked. A five-second coherence check now runs before quantisation. △ milder stall
v3 first shipping orchestrator The first public local-tier release. Total spend across every iteration: about ten dollars. ✓ ships
v4 3/4 focused probes Simulated trajectories drilling the exact failure modes the evals exposed. It reasoned about report repair correctly, but fumbled the file execution. △ report asterisk
v5 · final masked loss · 0.751 The final 12B adapter. The model owns report framing, priority, and prose in a checked editorial plan; safe code renders all three deliverables. ✓ release gate

The models: gemma4-12b-spotlight-orchestrator-v5-GGUF (Q4_K_M, ~7 GB — final) and its v5 LoRA adapter, trained from 14 real gold trajectories and 12 targeted correction continuations.

Unlocked loss 3.00 → 0.79 ~$10 total training
05
Serving

🤬 AND EVEN THEN IT KEPT DYING!!! At the methodology gate. Mid-investigation. EVERY. SINGLE. EVAL. I started to despair — until the server logs confessed: this time it wasn't a model capability issue…

KV-cache and serving — the invisible half of local

failed to find available cells in kv cache · failed to restore state with size ~971MB
The model was fine. The serving layer was wiping its memory on every delegation. The fix took me deeper into how the model is served than I ever planned to go. A couple dozen iterations later, this is the serving recipe I landed on — learning what each of these levers even was along the way:

Resident slots the fix --parallel 2 --no-cache-idle-slots
Orchestrator + active subagent each keep a live slot. Nothing saved or restored on delegation. And one slot is worse than two.
✓ 0 crashes
KV quantisation 2× context, same RAM --cache-type-k/v q8_0 + --flash-attn on
≈½ the KV memory per token, near-lossless.
✓ ½ memory
Context geometry headroom --ctx-size 81920 ÷ 2 slots = 40,960 tokens per agent — past anything the bounded pipeline reaches. ✓ 40,960 / agent
Reasoning budget think briefly, then act --reasoning-budget 400 (12B)
Unbounded thinking spends the whole output budget and returns an empty reply. 800 for the 26B, 1024 for the 31B.
✓ no empty turns
RLM serving no thinking --reasoning-budget 0
A distiller must answer, not think.
✓ instant distills

API tiers are exempt: the harness detects the tier and serves frontier models unrestricted. The constraints live exactly where the constraint exists — on the device.

Unlocked 0 KV save/restores 2 resident slots
06
Sovereignty

🚀🥲 It ran! It was fast! But it still wasn't sovereign — every fetch and every search went through a paid API key. One last dependency to cut.

Cutting the last commercial dependency

Search and scraping ran on Firecrawl — a paid, closed API on the critical path of a tool meant to run locally and privately.

  • The swap: fetchCrawl4AI (Apache-2.0, local Playwright browser) · searchSearXNG (self-hosted metasearch). Firecrawl demoted to optional fallback.
  • The surprise: the gap didn't just close. On our 8 hardest registry and civic-monitoring cases, the open stack leads.
Crawl4AI open source · local · now the default 100%
Scrapling open source · stealth fallback 100%

Scraping success · 8 hostile registry/civic cases · benchmarks.buriedsignals.com

(Firecrawl's miss is instructive: on one municipal homepage its main-content filter stripped the entire page down to image markdown.)

Search tells the same story. SearXNG — self-hosted, no key, no per-query bill — edges out the commercial APIs on source quality:

SearXNG open source · self-hosted · now the default 92%

Search source-quality · 8 investigative queries, scored on reaching canonical sources · benchmarks.buriedsignals.com

Unlocked 0 paid keys on the critical path 100% on hostile cases
Quest achieved

And then… 🎉 SUCCESS!!!

The goal, reached

Quest achieved

Full gated pipeline 12B · Q4 · 48 GB laptop ~$10 total training $0 per run 0 commercial APIs Nothing left the machine

Full-investigation benchmark · same Ethereum Foundation brief · grounded from run artifacts and deterministic validators

31B reference full invariant-9 pipeline Completed the investigation with independent adversarial re-scrapes and good phase discipline. ✓ passes
26B-A4B 17.1 min active · 20.5 min wall Fact-check evidence passed first try. It skipped report drafting after an invalid delegation; the deterministic guard bounced once, then both validators passed. ✓ passes after 1 bounce
12B · v5 20.9 min active · 35.5 min wall Completed the real investigation and independent fact-check. Guarded recovery repaired the evidence paths and report plan; the model-authored framing and deterministic render now pass every evidence and report validator. ✓ passes after guarded recovery

The deterministic checks enforce phase completion, intact evidence anchors and safe rendering. They do not decide whether a source proves a claim; the investigator, independent fact-checker and human gates still do that.

Context floor (skills + tools)
55,000 tok · 34 s
817 tok · 0.46 s
67×
A scraped source in context
~40,000 tok raw
~600-tok lead list
66×
Conversation depth
40k+ · thrashing
folds to ~10k
KV memory
f16 · save/restore crashes at ~29k q8_0 · 2 resident slots · zero restores
Orchestration discipline
skill re-activation loops · empty turns reason → delegate → synthesise → stop at gate
Search & scraping
paid API key on the critical path Crawl4AI + SearXNG · 100% on hostile cases

In the final v5 E2E, the tuned 12B reviewed a real corpus, delegated the investigation and independent fact-check, and produced the correct leadership, legal-form and domicile findings. It still needed guarded recovery to repair evidence paths and finish the report plan. The resulting fact-check, model-authored framing and rendered report all pass their validators.

To be precise: not "a 12B is as good as a frontier model." It's that a 12B inside a deeply engineered procedure runs gated multi-agent investigations at usable speed, with the human at the gates. Every optimisation transfers upward — the 26B and 31B tiers inherit the same harness with looser leashes and run better for it. Tune the procedure for the smallest model; the bigger ones get it for free.

The recipe, for newsrooms 1 — Collapse the context floor: dynamic skill loading, indexes not payloads.
2 — Bound the context: automatic distillation of everything scraped; fold conversations into checkpoints; artifacts on disk are the state.
3 — Engineer the harness: real subagents, absolute paths, durable sessions, one harness for eval and production.
4 — Tune the procedure, not the capability: real trajectories plus targeted correction continuations, trained with masked loss. Budget: tens of dollars.
5 — Serve deliberately: resident slots, quantised KV, capped reasoning.
6 — Cut the paid dependencies: open-source search & scraping, benchmarked past parity.

Own the model. Own the harness. Own the data. That's sovereignty — and it now fits on a laptop.