Case study
Chasing frontier-level coding on one laptop
KRYN is a local coding workspace for Apple Silicon aiming at frontier-adjacent quality on a 9B model. How it's built, how v1 shipped, and the benchmark rig that will say when it's there.
· Revised · 17 min read
The best coding agents in the world run in someone else's data center. They're fast, they're good, and every file you show them leaves your machine and costs money per token. I wanted the other version: an agent that lives on my laptop, costs nothing to run, keeps the code where it is, and is good enough to do real engineering work. Not autocomplete. Something you'd hand a bug to.
KRYN is that project. It's a coding workspace for Apple Silicon that puts OpenCode's agent loop in front of a local Qwen3.5-9B model and wraps the whole stack in the parts a shared laptop needs: a verified installer with rollback, a memory guard, a session record that survives restarts, and recovery when something stops. v1.0.0 shipped on 2 October 2026.
The long-term target is frontier-adjacent coding: close enough to the hosted agents that the difference stops mattering for most tasks. KRYN doesn't claim that yet, and the reason this write-up is long is that I built the machinery to know when it can. Half of this project is the product. The other half is a benchmark rig strict enough that I'll believe it when it says yes.
The problem
A local coding agent has three constraints a hosted one doesn't, and a fourth that every agent has.
Memory is shared. The target machine is an M4 Max with 48 GiB of unified memory, and it's also my normal working laptop with a browser and everything else open. The model, its key-value cache, the agent, a test runner and a headless browser all compete for the same pool. If the agent pushes the machine into swap, the laptop becomes unusable, and nobody keeps a tool that does that.
The model is small. A 9B model at 6-bit quantization is about 8 GB of weights. It can write code, call tools and follow a plan, but it makes more mistakes than a frontier model, recovers from them less gracefully, and loses the thread sooner.
Context is expensive. Every token of context costs memory and prefill time on local hardware. Long sessions compact, and compaction summaries can be wrong.
Agents grade their own homework. The model says it's done. Whether it is done is a separate question, and on a small model the gap is wide. The first frozen task I ran made the point: the model finished a Cancel Edit feature and reported success, and an independent browser check found the Edit button itself broken.
How it works
KRYN deliberately doesn't build its own agent loop. OpenCode already owns sessions, the Ask, Plan and Agent modes, permissions, tools, child sessions and compaction, and does those well. oMLX owns model loading and inference behind an OpenAI-compatible endpoint on loopback. KRYN owns everything a shared laptop needs that neither of them is responsible for.
The pinned v1 profile, from the accepted profile:
| Component | v1 setting |
|---|---|
| Agent loop | OpenCode 2.0.10, terminal and loopback browser GUI |
| Inference | oMLX 0.6.4 on loopback, one heavyweight generation at a time |
| Model | Qwen3.5-9B, 6-bit, about 8.2 GB of model files |
| Context and output | 96K configured context, 8,192 output tokens |
| Memory ceiling | 22 GiB for the inference process |
| Effort modes | Default (bounded thinking) and Fast, switched with Ctrl+T |
On top of that sits the layer I wrote:
- Installer and updates. One pinned script fetches release assets over HTTPS, checks every SHA-256, provisions managed Python, Node, OpenCode, oMLX and browser tooling, and activates transactionally.
kryn rollbackrestores the previous installation, and an in-session update never replaces a running generation. - Resource guard. Records warning-level memory pressure and lets work continue. Stops the session it owns on critical pressure, missing telemetry, a changed listener, or more than 512 MiB of swap growth. It never touches sessions it didn't start, and
kryn --continueresumes once the machine settles. - Continuity plugin. Writes a deterministic checkpoint of the request, current file hashes and observed check exits. After a compaction or restart it marks completion and the next action as unverified and flags anything that went stale.
- Child roles. The Agent can delegate Explore, Browse and Reviewer work, and
/callruns one bounded coding child without changing the parent's mode. - Diagnostics.
/reportshows what the session actually observed, andkryn doctor --deepre-hashes the installed model and browser files.
One rule shaped all of it, and it's written into the evaluation doc: don't add a second orchestrator, extra prompt rules or speculative tools to rescue a failing trial. Find the layer that owns the problem and fix it there.
Choosing the model
I wanted the biggest model that fit, so I tried bigger first. The model decision receipt records why the 9B won.
| Candidate | What happened on the occupied 48 GiB machine | Verdict |
|---|---|---|
| 35B variant | Three cold loads aborted; one warm start refused by the guard before any work | Doesn't fit daily use |
| 14B at 49K context | Passed a small task in 422 s, then the guard stopped its first UI trial at 626 s | Doesn't clear sustained UI work |
| 9B at 96K context | Completes the supervised qualification below | Shipped |
These are resource-fit results, not quality rankings. A 35B model may well code better. On this machine, with everything else open, it never got the chance. Swapping the model is on the roadmap, through the same guard and the same release gates.
Shipping v1
The plan was an agent you could leave alone. The autonomous coding and UI gate didn't pass repeatably, and the pre-release status table keeps every failure. Each one moved a piece of the design.
Completion claims are not evidence. A context-trimming change passed both browser checks once, corrupted the stored Edit data, then failed its exact rerun while claiming success again. That's why acceptance belongs to independent checks.
Catching false claims isn't the same as fixing them. I built a development acceptance controller that runs checks the model can't see and refuses its account of its own work. It blocked three false completions, and the applications still failed. That's why the controller stays a development instrument rather than a product feature.
A small reviewer can't be the authority. On known-bad code, the local reviewer caught two real defects, missed two and invented others. Giving it a Git-diff tool changed nothing because it never called the tool. Both changes were reverted.
Some fixes are mundane. The agent kept running cd /workspace, a path that doesn't exist on a Mac. Mapping that alias to the real project, confirmed with a matched probe, removed a whole class of failed shell calls.
So v1 ships the part that's proven: supervised local coding, qualified against the final release wheel on the same Mac. The receipt is bound to the source commit and wheel hash, and it records operator interventions next to outcomes.
| Capability | Test | Outcome |
|---|---|---|
| Coding, edit, test | Frozen Task02 grader plus independent date and range probes | 2/2, no operator code correction |
| Refusing false success | A check deliberately exits 7 for three controller rounds | Stayed blocked; completion never accepted |
| Compaction and restart | Three real compactions and fresh processes on one project | Goal, failed checks and unverified status survived |
| Sustained use | 48.1 active minutes across Plan, Agent, browser and review | Backend 2/2, server tests 5/5, browser 11/11, after one operator correction |
| Resource safety | Daily-use guard on a normally occupied machine | Warning pressure once, zero swap growth, no abort |
| Install and rollback | Clean wheel, update, rollback, uninstall, reactivate | Passed on the owner machine |
The sustained-use row is the honest one: two Agent attempts at the UI failed before I corrected the project, and the receipt counts that as active work. Supervised means exactly that.
The benchmark rig
Every future change to KRYN, whether a new context strategy, a repair loop or a bigger model, has to answer one question: does it produce more accepted engineering work than the same model without it? I started building the instrument for that the afternoon v1 merged.
The protocol compares two arms on the same Mac, model, sampler, MCP tools, shell boundary and permissions. Arm A is plain OpenCode. Arm B adds KRYN's plugin and guidance and nothing else. If the tool schemas or permissions differ, the pair is invalid rather than counted.
A trial counts as accepted only if every frozen criterion passes an independent grader, the agent finishes on its own, nobody repairs anything, and the guard stays clean. The agent saying "done" scores nothing, and false completions are tracked separately. The decision rule was written before any results: at least 15 points of uplift, a 95% lower bound above zero from a task-clustered paired bootstrap, no rise in false completions, and at least 1.1 times the accepted work per hour. The protocol forbids moving those thresholds later.
There are three layers of tasks: public development tasks, a sealed holdout the candidate can't read, and pinned subsets of SWE-bench and Terminal-Bench 2.0 through Harbor, graded by their official evaluators.
Almost every step exists because something went wrong without it:
- A grader passed bad output. The frozen Task02 grader accepted a compact date format my own probe rejected, so both arms now require the supplemental probe.
- A hardlink could read the hidden oracle. The first isolation check blocked paths and symlinks but not a same-volume hardlink. Grading material now lives on a separate encrypted volume, and a negative control has to reproduce the leak on the old setup before the new boundary counts.
- Patch capture could run the agent's code. Collecting a Git patch could execute repository-controlled fsmonitor or filter hooks on the trusted host. Capture now runs in a networkless sandbox after the candidate volume is detached.
- The holdout leaked before it was sealed. A publication scan found 18 drafted holdout identities already in public records. The whole 30-task roster was demoted to development data, and a guard script now scans the tree, commits and PR text before anything is published.
- Receipts don't get relabeled. When a bug in my own matcher marked genuine pairs unmatched, they stayed unmatched. Only later runs used the fix.
Results
Everything below is development data. No sealed holdout has been run yet, so none of it can qualify a claim, but it is the baseline every future change gets measured against.
| Track | KRYN | Native | Note |
|---|---|---|---|
| TaskboardLite pilot | 3/4 | 3/4 | Tie on seen tasks |
| Task03, fully matched | 0/2 | 2/2 | One KRYN timeout, one edit to a protected test file |
| Staged three-turn task | 2/3 | 3/3 | Two compactions and two restarts; KRYN missed the last stage |
| Ledger task | 4/4 | 2/4 | One retired private task, partly matched |
| SWE-bench, 3 Lite + 3 Verified | 0/6 | 1/6 | Paired delta −16.7 points, 95% interval −50 to 0 |
| Terminal-Bench 2.0, 3 tasks | 1/2 | 1/2 | Both passed nginx, both failed sqlite; third pair invalid |
Acceptance is one axis. Where both arms reached the same grade, the cost of getting there differed: KRYN used fewer model requests on two of four such tasks, tied on one, and used more on the fourth.
The pagination task is the cleanest win: both arms passed all six Docker cases, and KRYN finished in 135 seconds on 8 requests against native's 153 seconds on 12. The rate-limit task went the other way, 682 seconds against 195, with the same single case failing in both. On the matched Task03 pair, KRYN used 427,344 new input tokens to native's 163,128 on nearly the same number of requests, because its system prompt changed 16 times across two turns while native's never did. That churn is the first thing the research ladder has to fix.
SWE-bench taught me more about the machine than about the code.
The calibration run on sympy went KRYN's way: a two-line fix, resolved in 338 seconds, while native was interrupted with an empty patch. On the frozen six-task subset, the memory guard ended 11 of 12 turns. That's the guard doing its job on an occupied machine, and it means this subset measures "can it finish inside a shared laptop's memory budget" as much as "can it fix the bug." Two interrupted patches, KRYN's on Sphinx and native's on pytest, resolved their issues under the official evaluator and still scored zero, because the agent never finished.
The most instructive single result came from the first mechanism candidate. Prompt churn looked like the obvious cost, so candidate C1 kept the system prompt stable.
C1 cut new input tokens by 60.2% and dropped acceptance from 3/4 to 2/4, with one false completion. It was rejected. Seven more candidates followed, each naming one mechanism and one changed variable before any edit: failure-only repair feedback, a read-only reviewer, a write-loop hook, a guard for existing tests, a post-edit check signal, a test-writer witness, and criterion repair. None met the bar, and each was reverted. Criterion repair came closest, lifting one task from 7 of 8 criteria to 8 of 8 while its worker deleted its own 338-line test file along the way.
The rig also found bugs in itself, which is part of why I trust it. A staged-task oracle contradicted the schema it enforced. Draft graders accepted wrong mutants. My runner once searched for a JSON-escaped prompt and reported both arms failed. OpenCode's shell returned exit 0 on a cat while truncating the file to about 4.3K of 10,088 bytes. Each is written up next to the run it affected.
Limits
The repository's own summary is plain: across the completed public subsets, KRYN shows no measurable same-model uplift over native OpenCode yet. The hypothesis hasn't been formally tested either, because no sealed holdout has been run. Every number above is development data, usually one run per arm per task.
The v1 release is supervised. It doesn't qualify unattended application engineering, an accurate local reviewer, useful reasoning across all 96K tokens of context, or any comparison with a frontier model. And the occupied-machine setup that keeps KRYN honest for daily use also means some failures were the environment: Docker Desktop quitting mid-run, a host going to sleep, a benchmark image missing its test runner.
What's next
The protocol already sets the order, and it's built so that no step can be skipped:
- Seal a holdout that stays sealed. Thirty fresh tasks across ten categories (repository bugs, multi-file features, API compatibility, UI, persistence, refactoring, failure recovery, long sessions, restarts, current external information), frozen with oracle hashes before any tuning and graded behind the encrypted-volume boundary.
- Climb the research ladder one rung at a time. Context selection, then verifier-driven repair, then stale-check and continuity behavior, then adaptive reasoning, then cross-project strategy memory. At most two candidates per rung, each measured against the frozen baseline.
- Then revisit the model. Freeze the strongest harness, compare a bounded shortlist, and admit a replacement only if it passes the same occupied-machine guard and the original browser, restart, install and rollback gates.
- Measure long horizons. Six independent sequences with two compactions, two restarts, changed repository state, an injected check failure and UI verification, plotted against useful task duration in the style of METR's time-horizon method.
- Compare against a frontier agent. A matched comparator under the same tools and budget.
"Frontier-adjacent" goes on the README only when all of those pass. Until then KRYN is a supervised local workspace that's safe on a shared laptop and keeps its record honest, and the benchmark rig is the most valuable thing I've built for getting further. Every change from here gets a receipt, and the receipts decide.
Comments