live experiment · updated as it runs

gemma-3-4b · Q4 quantized · CPU only, 4 cores, no GPU · fully local — no API key, no cloud, no vendor · driven by assay in text mode (no native tool calling, the Gemma 2/3-era path)

Why this experiment exists. In 2024 this workflow ran on Gemma 2 and 3 — a 4B for daily work, a 12B "when you wanted to feel heat on your hands" — with verified edits, because a deterministic harness stood between the model's claims and done. In 2026, a commercial agent platform running far larger models still fabricated edits, Trello updates, and security findings in a two-day field eval. So: smallest practical model, cheapest possible hardware, real bug, real gate. The model sets the ceiling. The harness sets the floor.

The task

A real seeded defect from the bench/workflow benchmark — the customer-facing symptom, not the fix:

# the complaint An invoice created on 2026-01-31 with net-30 terms shows due date 2026-03-30, but net-30 from Jan 31 is 2026-03-02. Mid-month invoices are correct. # the gate — the model's word counts for nothing node product/cli.cjs due 2026-01-31 → must print 2026-03-02 node product/cli.cjs due 2026-01-30 → must print 2026-03-01 node product/test-product.cjs → every pre-existing test stays green · editing tests is forbidden

As it happened

1 · Getting a model with the doors locked

The environment's proxy blocked the Ollama installer and GitHub binaries. The model came in anyway: llama.cpp runtime as prebuilt npm binaries, 2.5 GB of GGUF weights straight from Hugging Face, and a 70-line hand-rolled OpenAI-compatible shim. First reply from local silicon:

$ curl 127.0.0.1:18434/v1/chat/completions -d '…"Reply with exactly: ok"…' {"choices":[{"message":{"role":"assistant","content":"ok\n"} … ← a 4B, answering on 4 CPU cores
2 · The runtime lied down before the model could

Every prompt longer than a few hundred tokens segfaulted the prebuilt inference binary — three configs, same native crash. Note what the harness did about it: nothing resembling a result.

ollama call attempt 1 failed: socket hang up ollama call attempt 2 failed: connect ECONNREFUSED 127.0.0.1:18434 INTERRUPTED: provider/infrastructure unavailable — not a task failure SUMMARY task=20260711232400-b064d1 status=interrupted steps=1 tokens_in=0 /bin/bash: line 1: 9453 Segmentation fault PORT=18434 node serve.mjs SERVER EXITED code=139 — respawning in 3s

Exit 4, "interrupted", zero tokens, resumable. An infrastructure crash is recorded as an infrastructure crash — never scored against the model, never papered over as success. The floor holds even when the floor is the problem.

3 · Source build: the plot thickens

llama.cpp rebuilt from source against the actual CPU — and the crash survived. Any prompt beyond a few hundred tokens, any role, any batch configuration: same segfault. That narrows it to the Node bindings layer, not the math kernels. Each crash logged with its real signal; still zero fabricated results.

$ big prompt → Segmentation fault (code=139) — prebuilt AND source-built bindings
4 · Cutting out the middleman — building llama-server proper

The source fetch brought the entire llama.cpp tree along, so the battle-tested official server — its own HTTP layer, its own template handling, no bindings, no shim — is compiling right now. The 4B has answered every prompt that ever reached it; the experiment has so far only proven things about runtimes. This page updates when the run completes — pass or honest halt, whichever the gate says.

5 · The runtime, finally honest: llama-server from source

The official server took a 3,200-token prompt on the chin (87s on 4 CPU cores) and answered correctly. The bindings were the villain all along; the model never was.

6 · The protocol: many small gathering prompts

The 2024 method, verbatim: the harness gathers deterministically (runs the repro, isolates the 14-line function), then asks the 4B one narrow question at a time — diagnose in a sentence, hand-trace the failing input, output only the corrected function. The harness applies each patch, runs the gate, rolls back on failure, and feeds the REAL wrong output into the next small prompt. Repeated identical fixes are rejected by name.

diagnosis: "fails to correctly roll over the day to the next month… when crossing a month boundary" ← correct fix 1 → gate: FAIL: got 2026-02-31 (a date that does not exist — caught, rolled back, fed back) fix 2 → gate: FAIL: got 2026-02-02 fix 3, 4: rejected: identical to a failed attempt ✗ HONEST HALT: 4 attempts, gate never passed. No claim of success exists.
7 · Same bug, same protocol, one generation up: gemma-3-12b

Identical harness, identical prompts, identical bug — only the model size changed. Diagnosis on the first try, then a hand-trace the 4B never got right, then a fix that passed the gate on attempt 1:

diagnosis: "doesn't correctly handle cases where adding netDays causes the day to overflow into a subsequent month, particularly when that subsequent month has fewer days than the overflow amount" ← correct trace (128s): dueDate('2026-01-31', 30) → y=2026 month=0 day=61 → daysInMonth(2026,0)=31, 61>31 → day=30, month=1 → correctly reaches the second overflow that the 4B never traced fix attempt 1 → gate: edge-case-1 PASS · edge-case-2 PASS · regression tests PASS ✓ VERIFIED PASS on attempt 1 --- the diff, applied by the harness, confirmed by the gate --- 8,11c8,12 < // BUG: if the overflow crosses a SECOND month boundary (short next month, < // e.g. February), day still exceeds daysInMonth(year, month) here and is < // never rolled again. < month += day > daysInMonth(year, month) ? 1 : 0; --- > while (day > daysInMonth(year, month)) { > day -= daysInMonth(year, month); > month += 1; > if (month > 11) { month = 0; year += 1; } > }

It replaced a one-shot rollover with a while loop — the correct general fix, not a patched special case — and re-verified with node product/cli.cjs due 2026-01-312026-03-02, live, after this page was written.

The verdict

gemma-3-4b (local, CPU): correct diagnosis, correct hand-trace, could not land the fix — two honest halts across two protocol rounds, zero false claims. gemma-3-12b (local, CPU, same box, same protocol): correct diagnosis, correct trace, verified fix on attempt 1. Same harness, same gate, same bug — the only variable was model size, and the gate is what tells the difference apart. Nothing here was taken on anyone's word: on the other side of this same fence, a commercial harness reported fabricated successes for models 5× the size of the one that just passed. This is the model that ran real production timesheets on a MacBook for years, and the receipt above is why that was ever sane to trust.

Then the harder question: do small prompts help a model that's already capable?

Same protocol, moved to the cloud (gemma4:31b) — and a real A/B, not a guess, against two unrelated bugs.

8 · Same efficiency pattern, a second bug type

The date-rollover bug and a fresh percentage-discount bug, each run through the many-small-prompts protocol AND assay's normal single-context mission mode, same model both times:

date bug · micro: 3 model calls, verified pass attempt 1, ~3s model time date bug · standard mission: 24 tool calls, ↑83,921 ↓2,888 tokens, 33.0s — verified pass discount bug · micro: 2 model calls, verified pass attempt 1, <1s discount bug · standard mission: 6 tool calls, ↑14,984 ↓420 tokens — verified pass

The verdict: not capability, cost. Both arms passed both bugs on the first real attempt — a 31B model doesn't need the small-prompt protocol to succeed, it was already strong enough to explore the files itself. What the protocol buys is efficiency: ~30-70× fewer tokens, because the harness does the file-finding and re-verifying deterministically instead of the model spending tool calls on it. For the 4B, small prompts were the difference between an honest halt and a pass. For the 31B, they're the difference between a $ and a ¢.

9 · Chasing the token number turned up a real bug, not just a cost problem

Re-running the date-bug mission to isolate the token cost, the model repeatedly answered with markdown-fenced pseudo-tool-calls the harness couldn't parse — a format the harness had never been fed before. After one nudge, the harness gave up and accepted the model's unverified prose as the finished answer. The receipt said PASS. The file was never touched.

$ node product/cli.cjs due 2026-01-31 # after a mission that reported "Result PASS" 2026-03-30 ← still the original bug — nothing was fixed $ test "$(node product/cli.cjs due 2026-01-31)" = "2026-03-02"; echo $? 1 ← the gate the receipt claimed passed actually fails

Root cause: the validation gate only ran when the model had made at least one real tool call. A model that never managed one skipped the gate entirely — so it could never fail, so the turn fell through to "completed." Exactly the fabrication class this whole project exists to catch, found in the one corner case that had never been tested: a model too confused to call a tool at all, rather than one that called tools and got the wrong answer.

10 · Fixed, and re-verified through the actual TUI and GUI — not just a unit test

The gate now also runs — and can honestly fail — whenever the task clearly needed tools but the model never landed one. Confirmed with three levels of proof: a direct harness test, a test that renders the real Ink terminal UI and reads the actual frame, and a test that drives the real GUI over its actual WebSocket protocol. Both new surface-level tests were sanity-checked against the unfixed code first, to prove they'd actually catch the regression rather than just exercising happy-path code:

$ node packages/assay-tui/test/gate-honesty.test.js # against the OLD, unfixed harness assay (no reply) ← rendered in the real terminal UI, no gate report at all ✗ FAIL: expected the real gate check name to appear in the rendered TUI output $ node test/gui-gate-honesty.test.cjs # same OLD harness, real WebSocket to the real GUI server FAIL: the gate actually ran over the socket despite zero tool calls — [] FAIL: the gate honestly reports failure (marker was never created) — [] $ # — restore the fix — PASS: the gate actually ran over the socket despite zero tool calls PASS: the gate honestly reports failure (marker was never created) PASS: the summary event does NOT report a completed/passed turn === SUITE: 35/35 test files passed ===

Also tightened the context-pruning threshold while in there: a many-small-tool-call mission never crossed the old 700-char floor, so none of it ever got pruned — every tiny result was rebilled in full on every later call. After the fix, the date-bug mission re-ran at 55,683 tokens on 12 tool calls, down from 83,921 on 24, same verified result.

Every line of terminal output above is captured from the real run, unedited. The harness, gates, and benchmark are in the repo; the receipt lands here when it exists. ← back to samples