← Writing

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

RoleCreator and maintainer
WhenSep to Oct 2026
Stack
PythonOpenCodeoMLXQwen3.5-9BSWE-benchTerminal-Bench

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.

A stack on one Mac. The developer works in OpenCode's terminal or loopback GUI. OpenCode owns sessions, the Ask, Plan and Agent modes, permissions, tools and compaction, and calls oMLX over loopback, which serves the Qwen3.5-9B model with a 22 GiB ceiling. KRYN wraps the stack with an installer that supports rollback, a launcher and resource guard, a continuity plugin that records checkpoints and check evidence, and routing and diagnostics. A development-only acceptance controller sits outside the installed product.
KRYN wraps the stack without replacing the loop. The acceptance controller is a development tool, not part of what installs.

The pinned v1 profile, from the accepted profile:

Componentv1 setting
Agent loopOpenCode 2.0.10, terminal and loopback browser GUI
InferenceoMLX 0.6.4 on loopback, one heavyweight generation at a time
ModelQwen3.5-9B, 6-bit, about 8.2 GB of model files
Context and output96K configured context, 8,192 output tokens
Memory ceiling22 GiB for the inference process
Effort modesDefault (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 rollback restores 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 --continue resumes 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 /call runs one bounded coding child without changing the parent's mode.
  • Diagnostics. /report shows what the session actually observed, and kryn doctor --deep re-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.

CandidateWhat happened on the occupied 48 GiB machineVerdict
35B variantThree cold loads aborted; one warm start refused by the guard before any workDoesn't fit daily use
14B at 49K contextPassed a small task in 422 s, then the guard stopped its first UI trial at 626 sDoesn't clear sustained UI work
9B at 96K contextCompletes the supervised qualification belowShipped

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.

CapabilityTestOutcome
Coding, edit, testFrozen Task02 grader plus independent date and range probes2/2, no operator code correction
Refusing false successA check deliberately exits 7 for three controller roundsStayed blocked; completion never accepted
Compaction and restartThree real compactions and fresh processes on one projectGoal, failed checks and unverified status survived
Sustained use48.1 active minutes across Plan, Agent, browser and reviewBackend 2/2, server tests 5/5, browser 11/11, after one operator correction
Resource safetyDaily-use guard on a normally occupied machineWarning pressure once, zero swap growth, no abort
Install and rollbackClean wheel, update, rollback, uninstall, reactivatePassed 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.

Seven steps of one trial. Freeze the task, prompt, tool schema and grader hashes. A seed hash picks which arm runs first. Each arm runs once in a disposable workspace on a separate encrypted volume under a 900 second budget and the memory guard. The volume is detached so nothing the agent started survives. The patch is captured in a networkless sandbox. A hidden oracle or the official SWE-bench or Harbor evaluator grades a fresh copy. A receipt records matched status, strict acceptance, false completion and resource data, and is never edited afterward.
The model's own claim of completion is recorded at every step and used as an input at none of them.

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.

Strictly accepted over attempted, OpenCode with KRYN versus native OpenCode on the same model. TaskboardLite pilot 3 of 4 each. Task03 matched pilot KRYN 0 of 2, native 2 of 2. Staged three-turn task KRYN 2 of 3 stages, native 3 of 3. One retired ledger task KRYN 4 of 4, native 2 of 4. SWE-bench six-task subset KRYN 0 of 6, native 1 of 6. Terminal-Bench subset 1 of 2 each. All development data.
Baseline acceptance by track: ties on the public pilot and Terminal-Bench, a KRYN lead on one retired task, native ahead on Task03 and SWE-bench.
TrackKRYNNativeNote
TaskboardLite pilot3/43/4Tie on seen tasks
Task03, fully matched0/22/2One KRYN timeout, one edit to a protected test file
Staged three-turn task2/33/3Two compactions and two restarts; KRYN missed the last stage
Ledger task4/42/4One retired private task, partly matched
SWE-bench, 3 Lite + 3 Verified0/61/6Paired delta −16.7 points, 95% interval −50 to 0
Terminal-Bench 2.0, 3 tasks1/21/2Both 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.

Model requests on four tasks where both arms got the same grade. Terminal-Bench nginx logging, both passed: 11 requests each. Terminal-Bench sqlite with gcov, both failed: KRYN 23 requests, native 128 after 112 repeated searches. Pagination task, both 6 of 6: KRYN 8, native 12. Rate-limit task, both 7 of 8: KRYN 34, native 12.
The sqlite run is the clearest case: native OpenCode repeated the same search 112 times until it hit the request cap, while KRYN stopped at 23.

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.

Agent seconds per SWE-bench task for both arms. sympy calibration: KRYN resolved in 338 s, native interrupted at 530 s. Django: 158 and 202 s, both stopped. Sphinx: KRYN 369 s, guard stop with a partial patch that resolved; native 259 s, completed and passed. Matplotlib 97 and 108 s, both guard stops. scikit-learn 392 and 145 s, both guard stops. pytest: KRYN 29 s guard stop, native 211 s guard stop with a partial patch that resolved. Astropy 13 and 40 s, both guard stops.
On a laptop that was also being used as a laptop, 11 of the 12 frozen-subset turns ended at the memory guard.

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.

Three paired bars comparing the v1 layer with candidate C1. System prompt changes: 31 versus 0. New input tokens: 753K versus 300K. Strictly accepted: 3 of 4 versus 2 of 4.
The cache fix did exactly what it was designed to do, and the work got worse. Cheaper is not the same as better.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

© 2026 Pranav Mishra