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-methodologyexecutes research cycles, stops at gates
✓
A research cycle, end to end
hours — or neverminutes
✓
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.)
Unlocked67× smaller floor74× 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
4×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 fetch40k → 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 diesreal child sessions + a never-re-delegate guard
✓
Tool calls
wrong path resolution → the model falls back to raw curl and looks stupidabstract verbs bound to absolute paths by a runtime adapter
✓
Crashes
a crash or a closed lid lost the investigationSQLite-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.
Unlocked3 isolated agent contextseval == productioncatalogue → 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 examples14 real gold trajectories12 targeted correction continuationsreal runs · human-approved gatesassistant-masked loss~$10 of rented GPUs
How many orchestrator submissions to finish?
Base 12B repeated loops · operator forcing · no valid finish35+ · stalled
Tuned 12B · v5 pass after guarded recovery9 · pass
26B-A4B pass after one report-phase bounce11 · pass
31B reference clean full-pipeline pass6 · 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.
v1QLoRA · full-text loss · 3.00Learned the style — 2.8× faster to a correct first-turn
brief — then stalled the pipeline. Tool outputs dilute the signal.✗ stalled
v2assistant-masked · 0.79Learns decisions, not tool noise. Then an adapter merged
in a mismatched stack produced garbage GGUF.✗ corrupted merge
v2-fixedmatched train/merge stackCoherent again — a milder stall, and every eval learning banked. A
five-second coherence check now runs before quantisation.△ milder stall
v3first shipping orchestratorThe first public local-tier release. Total spend across every
iteration: about ten dollars.✓ ships
v43/4 focused probesSimulated trajectories drilling the exact failure modes the evals
exposed. It reasoned about report repair correctly, but fumbled the file execution.△ report asterisk
v5 · finalmasked loss · 0.751The final 12B adapter. The model owns report framing, priority, and
prose in a checked editorial plan; safe code renders all three deliverables.✓ release gate
🤬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 slotsthe 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 quantisation2× context, same RAM--cache-type-k/v q8_0 + --flash-attn on
≈½ the KV memory per token, near-lossless.✓ ½ memory
Context geometryheadroom--ctx-size 81920 ÷ 2 slots = 40,960 tokens per agent —
past anything the bounded pipeline reaches.✓ 40,960 / agent
Reasoning budgetthink 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 servingno 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.
Unlocked0 KV save/restores2 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:fetch → Crawl4AI
(Apache-2.0, local Playwright browser) · search → SearXNG
(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 default100%
Unlocked0 paid keys on the critical path100% on hostile cases
✦
Quest achieved
And then… 🎉SUCCESS!!!
The goal, reached
✦ Quest achieved ✦
Full gated pipeline12B · Q4 · 48 GB laptop~$10 total training$0 per run0 commercial APIsNothing left the machine
Full-investigation benchmark · same Ethereum Foundation brief · grounded from run artifacts and deterministic validators
31B referencefull invariant-9 pipelineCompleted the investigation with independent adversarial re-scrapes and good phase discipline.✓ passes
26B-A4B17.1 min active · 20.5 min wallFact-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 · v520.9 min active · 35.5 min wallCompleted 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
4×
KV memory
f16 · save/restore crashes at ~29kq8_0 · 2 resident slots · zero restores
paid API key on the critical pathCrawl4AI + 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.