[{"data":1,"prerenderedAt":534},["ShallowReactive",2],{"blog-list":3},[4,89,381],{"id":5,"title":6,"body":7,"date":79,"description":80,"extension":81,"meta":82,"navigation":83,"path":84,"seo":85,"stem":86,"tag":87,"__hash__":88},"blog\u002Fblog\u002F2026-06-17-nitro-on-bun.md","Why we run Nitro on Bun in production",{"type":8,"value":9,"toc":72},"minimark",[10,14,27,32,44,59,63,66],[11,12,6],"h1",{"id":13},"why-we-run-nitro-on-bun-in-production",[15,16,17,18,22,23,26],"p",{},"AiChat's server runs on Bun in production — ",[19,20,21],"code",{},"nitro.preset: 'bun'",", so the process is\n",[19,24,25],{},"Bun.serve",". The interesting part wasn't the runtime. It was the build.",[28,29,31],"h2",{"id":30},"the-build-doesnt-run-on-bun-on-purpose","The build doesn't run on Bun — on purpose",[15,33,34,35,39,40,43],{},"The Nitro server bundle (Rollup) is a known Nuxt memory hog — it peaks around 7.5 GB\nregardless of runtime. DigitalOcean App Platform ",[36,37,38],"strong",{},"hard-caps every build at 8 GB",",\nun-raisable. Node's ",[19,41,42],{},"--max-old-space-size"," throttles the build heap so it fits under\nthat cap; Bun's JS engine has no equivalent throttle, peaks higher, and risks an\nexit-137 OOM on the builder.",[15,45,46,47,51,52,55,56,58],{},"So the Dockerfile builds on Node and runs on Bun. The build ",[48,49,50],"em",{},"engine"," doesn't change\nthe artifact — ",[19,53,54],{},"preset: 'bun'"," emits the same ",[19,57,25],{}," output either way — so the\nproduction server is still 100% Bun.",[28,60,62],{"id":61},"the-lesson","The lesson",[15,64,65],{},"\"Full Bun\" is a runtime decision, not a build one. Match the tool to the constraint:\nBun where it wins (the server), Node where the platform forces your hand (a memory-\ncapped builder). The user never sees the difference.",[67,68,69],"callout",{},[15,70,71],{},"If we ever outgrow the 8 GB build cap, the escape hatch is building the image in CI\nand deploying from a registry — which sidesteps the cap entirely.",{"title":73,"searchDepth":74,"depth":74,"links":75},"",3,[76,78],{"id":30,"depth":77,"text":31},2,{"id":61,"depth":77,"text":62},"2026-06-17","Identical artifact, a Bun runtime, and a build cap that forced an honest decision.","md",{},true,"\u002Fblog\u002F2026-06-17-nitro-on-bun",{"title":6,"description":80},"blog\u002F2026-06-17-nitro-on-bun","Infra","sbsBfThN8l8oUJx_Nwoz1VE1K5foFPBENt3ntbr7sQM",{"id":90,"title":91,"body":92,"date":373,"description":374,"extension":81,"meta":375,"navigation":83,"path":376,"seo":377,"stem":378,"tag":379,"__hash__":380},"blog\u002Fblog\u002F2026-07-06-operator-loop-for-unattended-agents.md","An operator's loop for running coding agents unattended",{"type":8,"value":93,"toc":366},[94,97,108,111,115,118,121,124,127,131,134,220,223,227,230,250,257,260,264,267,270,334,337,340,344,347,359,362],[11,95,91],{"id":96},"an-operators-loop-for-running-coding-agents-unattended",[15,98,99,100,107],{},"I built ",[101,102,106],"a",{"href":103,"rel":104},"https:\u002F\u002Fgithub.com\u002Ftcashel\u002Fforge",[105],"nofollow","forge"," over about five weeks of free time: a harness for running coding agents unattended. You hand it a spec, walk away, and come back to a draft PR that a second model has already reviewed. It's roughly 24k lines of TypeScript on Bun, still pre-release, and no longer just mine: a few people at work use it now, and the PR-review piece has been pulled out into work's own tooling.",[15,109,110],{},"The thing I keep coming back to is that none of the individual pieces are clever. A git worktree is just git. \"Two critics\" is two model calls. A synthesizer merges text. What makes the system useful is the composition of those boring pieces into a single loop you only touch once. This post walks through how the loop fits together, and the one place it is still weak.",[28,112,114],{"id":113},"what-actually-bites-you-when-an-agent-runs-unattended","What actually bites you when an agent runs unattended",[15,116,117],{},"Two failure modes cause nearly all of the pain, and neither of them is the model writing bad code.",[15,119,120],{},"The first is the vague spec. A vague spec doesn't fail loudly: the agent builds the wrong thing, correctly, and you don't find out until you're already reading the diff. You can't lint your way out of bad requirements.",[15,122,123],{},"The second is trusting the result. An agent runs, declares victory, and opens a PR praising its own change. If the same agent that wrote the change also decides the change is good, you haven't removed the review work; you've moved it onto yourself, after the fact, which defeats the reason for walking away in the first place. It's the same separation-of-duties argument auditors make: the person who does the work shouldn't be the one who signs off on it. Don't let an agent grade its own homework.",[15,125,126],{},"So the loop is built around those two problems specifically: review the input before it runs, and never let the writer be the reviewer.",[28,128,130],{"id":129},"the-shape-of-the-loop","The shape of the loop",[15,132,133],{},"The human touches this once, at the spec. Everything after that runs on its own.",[135,136,140],"pre",{"className":137,"code":138,"language":139,"meta":73,"style":73},"language-mermaid shiki shiki-themes github-dark","flowchart TD\n    A[\"I write a spec\"] --> B{\"Spec review\"}\n    B --> C1[\"Critic A · model 1\"]\n    B --> C2[\"Critic B · model 2\"]\n    C1 --> S[\"Synthesizer\u003Cbr\u002F>merges both critiques\u003Cbr\u002F>into one improved spec\"]\n    C2 --> S\n    S --> H{{\"I approve the spec\u003Cbr\u002F>(the only human gate)\"}}\n    H --> I[\"Implementer builds\u003Cbr\u002F>in an isolated git worktree\"]\n    I --> PR[\"Draft PR\"]\n    PR --> R[\"Reviewer · a different model\"]\n    R --> F[\"Implementer fixes\u003Cbr\u002F>against the review\"]\n    F -->|\"up to ~2 passes\"| R\n    R --> Done[\"PR ready for me to merge\"]\n","mermaid",[19,141,142,150,155,160,166,172,178,184,190,196,202,208,214],{"__ignoreMap":73},[143,144,147],"span",{"class":145,"line":146},"line",1,[143,148,149],{},"flowchart TD\n",[143,151,152],{"class":145,"line":77},[143,153,154],{},"    A[\"I write a spec\"] --> B{\"Spec review\"}\n",[143,156,157],{"class":145,"line":74},[143,158,159],{},"    B --> C1[\"Critic A · model 1\"]\n",[143,161,163],{"class":145,"line":162},4,[143,164,165],{},"    B --> C2[\"Critic B · model 2\"]\n",[143,167,169],{"class":145,"line":168},5,[143,170,171],{},"    C1 --> S[\"Synthesizer\u003Cbr\u002F>merges both critiques\u003Cbr\u002F>into one improved spec\"]\n",[143,173,175],{"class":145,"line":174},6,[143,176,177],{},"    C2 --> S\n",[143,179,181],{"class":145,"line":180},7,[143,182,183],{},"    S --> H{{\"I approve the spec\u003Cbr\u002F>(the only human gate)\"}}\n",[143,185,187],{"class":145,"line":186},8,[143,188,189],{},"    H --> I[\"Implementer builds\u003Cbr\u002F>in an isolated git worktree\"]\n",[143,191,193],{"class":145,"line":192},9,[143,194,195],{},"    I --> PR[\"Draft PR\"]\n",[143,197,199],{"class":145,"line":198},10,[143,200,201],{},"    PR --> R[\"Reviewer · a different model\"]\n",[143,203,205],{"class":145,"line":204},11,[143,206,207],{},"    R --> F[\"Implementer fixes\u003Cbr\u002F>against the review\"]\n",[143,209,211],{"class":145,"line":210},12,[143,212,213],{},"    F -->|\"up to ~2 passes\"| R\n",[143,215,217],{"class":145,"line":216},13,[143,218,219],{},"    R --> Done[\"PR ready for me to merge\"]\n",[15,221,222],{},"Two details matter more than the diagram suggests. The spec review happens before any code exists, which is the cheapest place to catch a mistake. And the post-PR review is a separate step run by a different model than the one that wrote the code. By the time a PR reaches me it has been reviewed twice, once as a spec and once as a diff.",[28,224,226],{"id":225},"every-part-is-deliberately-boring","Every part is deliberately boring",[15,228,229],{},"If you pull the loop apart, there is no single impressive component:",[231,232,233,241,244,247],"ul",{},[234,235,236,237,240],"li",{},"The worktree is plain ",[19,238,239],{},"git worktree",". Each run gets its own branch and working directory.",[234,242,243],{},"The two critics are two model calls with different models behind them. They reliably catch different things: one flags the edge cases I left out, the other flags where I over-scoped the work. Different models have different blind spots.",[234,245,246],{},"The synthesizer reconciles those two critiques into one improved spec, so I approve a single sharpened version instead of refereeing a pile of conflicting notes.",[234,248,249],{},"The reviewer and the fixer are, again, just model calls in a short loop.",[15,251,252,253,256],{},"Most of those 24k lines aren't the agents at all. They're the unglamorous part: state that survives a crash, atomic writes so two runs can't corrupt shared files, and a safety machine that decides what's safe to clean up. That safety machine taught me the one rule I'd carry to any version of this: ground truth is the real system state, not your database. forge reads ",[19,254,255],{},"git worktree list"," to decide what exists, and the database is only annotation. A database drifts and git doesn't; the moment a harness trusts its own bookkeeping over reality, it starts deleting things it shouldn't.",[15,258,259],{},"The part I didn't expect is where the leverage concentrated. The highest-value place to spend attention turned out to be the input, not the output: a spec two models have already argued over is worth more than any amount of cleanup after the fact. The parts are commodity; the loop is what's worth building, because it puts your attention in exactly one place.",[28,261,263],{"id":262},"the-gap-full-agent-isolation","The gap: full agent isolation",[15,265,266],{},"This is the part I'm not happy with yet, and the reason forge is still a tool I watch rather than one I'd point at anything sensitive.",[15,268,269],{},"A worktree isolates the repository: the branch and the files for that run. It does not isolate the agent. The process still runs with my machine's access: my filesystem outside the repo, my network, my credentials. \"Isolated run\" today means \"isolated git state,\" not \"isolated blast radius.\"",[135,271,273],{"className":137,"code":272,"language":139,"meta":73,"style":73},"flowchart LR\n    subgraph today[\"Isolated today\"]\n      W[\"git worktree\u003Cbr\u002F>branch + files for this run\"]\n    end\n    subgraph gap[\"NOT isolated yet: the gap\"]\n      P[\"agent process\"]\n      N[\"network access\"]\n      M[\"rest of the machine\u003Cbr\u002F>+ my credentials\"]\n    end\n    W -.->|\"but the agent still reaches past it\"| P\n    P --- N\n    P --- M\n",[19,274,275,280,285,290,295,300,305,310,315,319,324,329],{"__ignoreMap":73},[143,276,277],{"class":145,"line":146},[143,278,279],{},"flowchart LR\n",[143,281,282],{"class":145,"line":77},[143,283,284],{},"    subgraph today[\"Isolated today\"]\n",[143,286,287],{"class":145,"line":74},[143,288,289],{},"      W[\"git worktree\u003Cbr\u002F>branch + files for this run\"]\n",[143,291,292],{"class":145,"line":162},[143,293,294],{},"    end\n",[143,296,297],{"class":145,"line":168},[143,298,299],{},"    subgraph gap[\"NOT isolated yet: the gap\"]\n",[143,301,302],{"class":145,"line":174},[143,303,304],{},"      P[\"agent process\"]\n",[143,306,307],{"class":145,"line":180},[143,308,309],{},"      N[\"network access\"]\n",[143,311,312],{"class":145,"line":186},[143,313,314],{},"      M[\"rest of the machine\u003Cbr\u002F>+ my credentials\"]\n",[143,316,317],{"class":145,"line":192},[143,318,294],{},[143,320,321],{"class":145,"line":198},[143,322,323],{},"    W -.->|\"but the agent still reaches past it\"| P\n",[143,325,326],{"class":145,"line":204},[143,327,328],{},"    P --- N\n",[143,330,331],{"class":145,"line":210},[143,332,333],{},"    P --- M\n",[15,335,336],{},"In the meantime I run the agents behind a fairly extensive set of hooks that intercept what they try to do and block the dangerous moves before they land. The hooks catch a lot, and I wouldn't run unattended without them. But hooks are guardrails rather than a boundary: they stop the mistakes I predicted, and a sandbox has to stop the ones I didn't.",[15,338,339],{},"To actually trust unattended runs, especially more than one at a time, the agent itself needs that boundary: a sandbox with scoped network and resource limits, so a run can't reach past its own task even if it tries. That's what I'm building now. Until it's there, \"walk away and trust it\" has an asterisk.",[28,341,343],{"id":342},"what-id-tell-someone-building-this-today","What I'd tell someone building this today",[15,345,346],{},"Three things, in order:",[348,349,350,353,356],"ol",{},[234,351,352],{},"Spend your first effort on the spec-review step, not the agent. Front-loading the review onto the input was the highest-leverage change I made, and the cheapest to build.",[234,354,355],{},"Make ground truth the real system state. Read git (or whatever the real source is) and treat your own database as a cache that can lie.",[234,357,358],{},"Don't ship unattended execution until isolation is real. Reviewing the work is the easy half; bounding what a run can touch is the half that decides whether you can actually leave. Hooks buy time, but they don't close the gap.",[15,360,361],{},"The loop is the interesting part. The isolation decides whether the loop is safe to walk away from, and I'm still building that.",[363,364,365],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":73,"searchDepth":74,"depth":74,"links":367},[368,369,370,371,372],{"id":113,"depth":77,"text":114},{"id":129,"depth":77,"text":130},{"id":225,"depth":77,"text":226},{"id":262,"depth":77,"text":263},{"id":342,"depth":77,"text":343},"2026-07-06","Review the spec before any code exists, never let the writer be the reviewer, and the isolation gap that's still open.",{},"\u002Fblog\u002F2026-07-06-operator-loop-for-unattended-agents",{"title":91,"description":374},"blog\u002F2026-07-06-operator-loop-for-unattended-agents","Agents","ulmIr8lbgz0g1uXS1grSc690yRyY7MLqW4eazdEaX4k",{"id":382,"title":383,"body":384,"date":373,"description":527,"extension":81,"meta":528,"navigation":83,"path":529,"seo":530,"stem":531,"tag":532,"__hash__":533},"blog\u002Fblog\u002F2026-07-06-the-reranker-that-looked-like-bert.md","The reranker that looked like BERT",{"type":8,"value":385,"toc":523},[386,389,392,409,413,424,453,460,463,470,474,485,488,491,505,508],[11,387,383],{"id":388},"the-reranker-that-looked-like-bert",[15,390,391],{},"I ported a reranker onto the Apple Neural Engine, and the hardest bug in the project turned out to be one line in a config file rather than anything about the hardware.",[15,393,394,395,400,401,404,405,408],{},"The model is ",[101,396,399],{"href":397,"rel":398},"https:\u002F\u002Fhuggingface.co\u002FBAAI\u002Fbge-reranker-base",[105],"BAAI\u002Fbge-reranker-base",", a 278M-parameter cross-encoder with BERT-shaped guts: 12 layers, 768 hidden, GELU. So I reached for the BERT playbook: WordPiece-style assumptions, ",[19,402,403],{},"[CLS]","\u002F",[19,406,407],{},"[SEP]",", and a single separator between the query and the document. It ran fine, but the relevance scores were quietly wrong.",[28,410,412],{"id":411},"one-line-changes-everything-downstream","One line changes everything downstream",[15,414,415,416,419,420,423],{},"The tell was in ",[19,417,418],{},"config.json",": ",[19,421,422],{},"model_type: xlm-roberta",". If you pattern-match on the architecture you will never see it, because that one line rewires everything the model expects at the input:",[231,425,426,429,447],{},[234,427,428],{},"the tokenizer is SentencePiece-Unigram, not WordPiece",[234,430,431,432,435,436,435,439,442,443,435,445],{},"the special tokens are ",[19,433,434],{},"\u003Cs>"," \u002F ",[19,437,438],{},"\u003C\u002Fs>",[19,440,441],{},"\u003Cpad>"," (ids 0\u002F2\u002F1), not ",[19,444,403],{},[19,446,407],{},[234,448,449,450],{},"the paired input uses a doubled separator: ",[19,451,452],{},"\u003Cs> query \u003C\u002Fs>\u003C\u002Fs> document \u003C\u002Fs>",[15,454,455,456,459],{},"Missing the doubled ",[19,457,458],{},"\u003C\u002Fs>\u003C\u002Fs>"," doesn't break anything. There is no crash and no warning; the model just scores against malformed input.",[15,461,462],{},"The part I didn't expect was how invisible this is to normal testing. A \"does it run\" check passes, and the scores even look plausible in isolation. The test that catches it is numeric equivalence against the reference implementation. Two models can share a shape and still disagree on tokenization, separators, and position-id math, and those disagreements never show up as errors, only as wrong numbers.",[15,464,465,466,469],{},"The lesson travels well past Apple Silicon: read the config, not the architecture diagram. ",[19,467,468],{},"model_type"," is the real contract.",[28,471,473],{"id":472},"once-the-input-was-right-the-port-held","Once the input was right, the port held",[15,475,476,477,480,481,484],{},"Getting the model resident on the Neural Engine took more than a conversion flag. A naive Core ML conversion of a BERT-family encoder lands on CPU\u002FGPU. To keep it on the ANE you rewrite the graph the way Apple's ",[19,478,479],{},"ane_transformers"," reference does: every Linear becomes a 1×1 Conv2d, tensors carry a ",[19,482,483],{},"(B, C, 1, S)"," layout, and LayerNorm is swapped for an ANE-friendly version.",[15,486,487],{},"Then you verify it stayed there. My build gate asserts an exact CPU-dispatch fingerprint (the 31 ops that physically can't run on the ANE, like the gather over a 250k-token vocab, casts, and mask math) and zero GPU fallback. If anything drifts off that fingerprint, the build fails.",[15,489,490],{},"The payoff, on my machine:",[231,492,493,496,499,502],{},[234,494,495],{},"2.62ms per pair p95 on the ANE vs 6.92ms on CPU+GPU at batch=20\u002Fseq=128, about 2.6x faster",[234,497,498],{},"at seq=256 it's 6.54ms vs 12.16ms, about 1.9x",[234,500,501],{},"FP16 Core ML, a precision conversion, not quantization",[234,503,504],{},"an MTEB SciDocs regression check showing the FP16 conversion cost +0.0005 nDCG@10 vs FP32, which is nothing",[15,506,507],{},"Two caveats, because numbers without them are marketing: this is a single-machine benchmark (50 warmup + 100 timed iterations per cell), not independently reproduced. And this is a port of an existing model, not a new reranker; the work was the graph rewrite, the residency gate, and getting the input format right.",[15,509,510,511,516,517,522],{},"The conversion code is at ",[101,512,515],{"href":513,"rel":514},"https:\u002F\u002Fgithub.com\u002Ftcashel\u002Fjuice-bge-reranker-coreml",[105],"github.com\u002Ftcashel\u002Fjuice-bge-reranker-coreml",", and the converted model is on Hugging Face as ",[101,518,521],{"href":519,"rel":520},"https:\u002F\u002Fhuggingface.co\u002Ftcashel\u002Fbge-reranker-base-coreml",[105],"tcashel\u002Fbge-reranker-base-coreml"," if you're doing on-device inference on Apple Silicon.",{"title":73,"searchDepth":74,"depth":74,"links":524},[525,526],{"id":411,"depth":77,"text":412},{"id":472,"depth":77,"text":473},"BERT-shaped guts, an XLM-RoBERTa config, and relevance scores that were silently wrong.",{},"\u002Fblog\u002F2026-07-06-the-reranker-that-looked-like-bert",{"title":383,"description":527},"blog\u002F2026-07-06-the-reranker-that-looked-like-bert","ML","1PRQQtNDA6J90X0mYILvt7B-9SbbQe7awdfi2LwekvA",1786110827603]