<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anish Shrestha</title>
    <description>The latest articles on DEV Community by Anish Shrestha (@anyesh).</description>
    <link>https://dev.to/anyesh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F202575%2F3ff44b38-0271-4c86-9438-14b0ada88277.png</url>
      <title>DEV Community: Anish Shrestha</title>
      <link>https://dev.to/anyesh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anyesh"/>
    <language>en</language>
    <item>
      <title>My Claude Code sessions work as a team, and they message each other</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Tue, 13 Oct 2026 04:03:51 +0000</pubDate>
      <link>https://dev.to/anyesh/my-claude-code-sessions-work-as-a-team-and-they-message-each-other-18b8</link>
      <guid>https://dev.to/anyesh/my-claude-code-sessions-work-as-a-team-and-they-message-each-other-18b8</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I run one Claude Code session per repository and let them message each other instead of editing each other's code. The messaging ships with Claude Code (&lt;code&gt;ListAgents&lt;/code&gt; and &lt;code&gt;SendMessage&lt;/code&gt;). Running six to nine of them at once works because of a harness that keeps cache costs visible, pushes to my phone when a session is waiting, logs every action a guard blocks, and carries context across resets. Over three and a half weeks, 81 sessions sent each other 624 messages, and cross-repository requests that would have gone through me now go straight from one session to another.&lt;/p&gt;

&lt;p&gt;On a busy day six to nine Claude Code sessions are active on my machine in the same hour, each one sitting in its own repository: Loom, the research repos that feed it (unlearn, redraft, JEV-like-research, memories-for-llms), second-brain, a memory service, wardrowbe, the harness that configures all of them. Each session owns its repository and nothing else. When one needs something from another repository, it doesn't open those files and edit them; it messages the session that owns them, and that session does the work in its own domain and replies.&lt;/p&gt;

&lt;p&gt;Between 18 September and 12 October, 81 sessions across 13 projects sent each other 624 messages. The busiest day was 1 October, with 200. The most sessions active in a single hour was nine, on three separate days.&lt;/p&gt;

&lt;h2&gt;
  
  
  One session per repository
&lt;/h2&gt;

&lt;p&gt;The setup is easiest to see in how I start a day's work. This is the opening prompt of a Loom session on 29 September, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;we were working on some advanced feature for loom where we were brigining up our various research we have like evoke(unlearn), jev, memories-for-llms, redraft etc. ... This is opus main session. You can spawn other child sonnet with appropriate effort level to do your work. main session is the orchestration and managing session. you will have all the context and you will pass in around as needed. AND additionally there are other agents on each project ready to colab with you on any related tasks. you can directly contact them and make them work on their own domain if needed. good luck!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Twenty minutes later that session called &lt;code&gt;ListAgents&lt;/code&gt; and found itself as &lt;code&gt;loom-1d&lt;/code&gt;, with four peers that had all started 23 minutes earlier: &lt;code&gt;unlearn-e6&lt;/code&gt;, &lt;code&gt;redraft-20&lt;/code&gt;, &lt;code&gt;jev-like-research-65&lt;/code&gt; and &lt;code&gt;memories-for-llms-8a&lt;/code&gt;. Loom is the hub of that team because it's where the research gets integrated; each research repository has its own session that knows its own code, its own experiments, and its own history.&lt;/p&gt;

&lt;p&gt;There are two levels of delegation here and they're different things. Inside one session, the Opus main session spawns Sonnet subagents for bounded pieces of work and reviews what comes back. Across sessions, the peers are full Claude Code sessions with their own context, their own permission prompts, and their own person approving things, which happens to be me in every case.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/defs&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="200" height="56" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="200" height="50" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="200" height="50" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="200" height="50" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="200" height="50" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1"&amp;gt;
  &amp;lt;rect width="80" height="34" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="80" height="34" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="80" height="34" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;





&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;










&amp;lt;/g&amp;gt;
&amp;lt;g stroke="" stroke-width="1.5" fill="none"&amp;gt;
  &amp;lt;line x1="240" y1="78" x2="380" y2="160"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="720" y1="78" x2="580" y2="160"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="240" y1="282" x2="380" y2="198"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="720" y1="282" x2="580" y2="198"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g stroke="currentColor" stroke-width="1" fill="none"&amp;gt;
  &amp;lt;line x1="440" y1="206" x2="390" y2="298"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="480" y1="206" x2="480" y2="298"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="520" y1="206" x2="570" y2="298"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;The team from the 29 September kickoff. Highlighted lines are cross-session messages between peers; the dashed boxes are subagents the hub spawns and discards inside its own session.
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How they talk
&lt;/h2&gt;

&lt;p&gt;The messaging itself isn't something I built. Claude Code ships two tools for it: &lt;code&gt;ListAgents&lt;/code&gt; shows the other sessions running on the machine, and &lt;code&gt;SendMessage&lt;/code&gt; delivers text to one of them over a local Unix socket. The receiving session sees the message wrapped in a &lt;code&gt;&amp;lt;cross-session-message from="..." from-name="app-c6"&amp;gt;&lt;/code&gt; block at its next tool round, so a busy session finishes the step it's on before reading it, and replies by sending back to the &lt;code&gt;from&lt;/code&gt; address. My harness doesn't wrap, extend, or configure any of this. What it does is make running this many sessions at once cheap enough and safe enough to keep doing, which comes further down.&lt;/p&gt;

&lt;p&gt;The sessions sit in panes in &lt;a href="https://herdr.dev" rel="noopener noreferrer"&gt;herdr&lt;/a&gt;, a terminal multiplexer that knows which panes hold coding agents and whether each one is working, blocked, or done. On 10 October &lt;code&gt;herdr agent list&lt;/code&gt; showed four Claude sessions in one workspace: three working, in an app repository, the harness and my home directory, and one blocked, in wardrowbe, waiting on me. Herdr's own log has up to about twenty Claude panes alive on a single day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they say to each other
&lt;/h2&gt;

&lt;p&gt;The useful part is what the messages are for, so here are five of them as they were sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An API change and a release, across two repositories.&lt;/strong&gt; A Rust memory service of mine depended on second-brain through a pinned git revision and needed a private method made public. On 18 September its session wrote to the second-brain session:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[The memory service] needs a second-brain release it can depend on from crates.io instead of its git rev pin. ... Two asks: (1) make &lt;code&gt;MaintenanceOps::backfill_embeddings&lt;/code&gt; public, or expose it through a public method that runs only the embedding backfill, not the rest of repair() ([it] must not run the cross-project merging); (2) cut a release ... Please reply with the version number once it is published, or tell me if something blocks it. No rush over your current audit work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reply came fifteen seconds later, and it pushed back on the order: second-brain was midway through an audit whose fixes also affected the memory service, so the release should wait for them and would likely be 0.9.0, "because it adds public API and changes behaviour". It also said publishing to crates.io can't be undone, so it was getting my go-ahead first. Five days later the release landed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;second-brain 0.9.0 is on crates.io with a public backfill_embeddings: all six crates are published (core, adapters, sync, mcp, api, cli), tagged v0.9.0.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The memory service moved onto it, and Loom, which runs it, checked the change and confirmed it needed no changes on its side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A turn on the GPU.&lt;/strong&gt; One GPU box is shared by three research repositories, and on 27 September memories-for-llms needed it for an experiment window. It asked Loom to "book 100 minutes on [the GPU box]; I expect it to take 45 to 55." Loom, acting as the scheduler, told the unlearn session to step off:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please step off [the GPU box] now. The user has cleared window 1 (memories-for-llms) to start immediately, booked for 100 minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stop your scheduled task and any process of yours using the GPU, and reply to me with "stopped" once VRAM is released (show the nvidia-smi line).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the same minute it corrected memories-for-llms' stop script, because "killing every llama-server by image name would take down anything of anyone's on the box." The unlearn session couldn't comply right away: it was in plan mode, which blocks anything that changes the system, so it said it was asking me. I approved it, it replied &lt;code&gt;stopped, VRAM released&lt;/code&gt;, and Loom opened the window: "unlearn-97 has stopped EVOKE, and nvidia-smi shows the GPU idle." From the booking request to the window opening took five minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A merge with ground rules.&lt;/strong&gt; On 3 October the verdict session in JEV-like-research needed work merged into Loom's main. Loom had already set the terms:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please do it on a branch in its own worktree rather than in the [main loom] checkout: I merge several branches into main from that checkout through the day, and uncommitted edits there would collide with those merges.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verdict did that, ran the gates, and asked Loom to merge the four commits. Loom asked it to hold its service restart until the merge was done, then reported back: "merged into loom main and pushed; gates passed. You can go ahead with the verdict restart."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting for everyone before a deploy.&lt;/strong&gt; Several sessions often work in one app repository at once. Before a production deploy on 4 October I asked one of them to wait for all the others, and it broadcast:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please message [me] when your work is committed and finished; I'm waiting on you before a prod deploy. ... send me one line covering three things: (1) you're finished and the tree is clean of your changes, (2) your last commit hash, and (3) whether your work adds a database migration or needs anything special in prod ...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One session answered "Not finished yet", still editing UI files; thirteen minutes later it reported "Finished: every change from my session is committed and the tree has none of my changes; I won't commit again," with its last commit hash and its one migration, and the deploy went out with its commits included.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asking a peer what it measured.&lt;/strong&gt; On 4 October Loom wanted a seven-hour training job made faster without a bigger GPU, and asked memories-for-llms, which had trained every adapter so far, for "your measured knowledge, not guesses". Two minutes later the answer started: "almost none of the knobs you named was ever measured. Details with citations follow; I read files only and did not touch [the GPU box]."&lt;/p&gt;

&lt;p&gt;Not every message is between related projects. On 12 October a session making a video built a new skill straight into &lt;code&gt;~/.claude/skills&lt;/code&gt;, then told the harness session to adopt it. The harness session moved it into the harness, ported a follow-up, and replied: "collage-film now lives in the harness at [harness]/configs/shared/skills/collage-film; please don't edit ~/.claude/skills/collage-film any more, because install.sh overwrites it from there."&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness under it
&lt;/h2&gt;

&lt;p&gt;All of these sessions run on the same &lt;a href="https://github.com/Anyesh/harness" rel="noopener noreferrer"&gt;harness&lt;/a&gt;: one repository that renders the configuration, hooks, rules, skills and MCP servers for Claude Code, Cursor, Codex and opencode. Running nine sessions at once changes what the harness has to do, and most of the pieces below exist because of a specific failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;The costliest failure the harness has recorded is a large context whose prompt cache expired. A Claude Code main session's cache lasts an hour idle and a subagent's about five minutes; the first turn after a longer gap re-sends the whole context at the cache-write price. Orchestrating sessions had been running subagents for 8 to 24 hours while the main session waited, so the rules now cap subagent work to minutes, never resume a large-context agent after a gap, and keep builds and long suites in background shells on the main session.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cache-guard&lt;/code&gt; mod, further down, puts that clock on the status line so the rule doesn't depend on anyone remembering it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/orchestrate&lt;/code&gt; command splits a plan into independent workstreams, has a bigger model review the split first, runs the streams as parallel subagents in their own worktrees where files would collide, and leaves the merge and the full test run to the main session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attention
&lt;/h3&gt;

&lt;p&gt;With nine sessions, the bottleneck is noticing which one is waiting on me. A hook sends a push to my phone when any session has been blocked on a question or a permission for fifteen seconds, or has finished a turn for sixty, and cancels the push if I answer first. In one eighteen-hour stretch on 12 and 13 October it sent 35 blocked pushes and 16 finished ones, and 34 of the 35 blocks were answered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hygiene
&lt;/h3&gt;

&lt;p&gt;Parallel sessions leave worktrees behind. The rule is that whoever creates a worktree removes it, and a sweep at session start removes any worktree idle for a day whose commits have all landed, saves uncommitted files to &lt;code&gt;.git/worktree-salvage/&lt;/code&gt;, and keeps any branch with work that hasn't landed. Each session's status files, block log entries and handoff digests are keyed by its session id, so two sessions in the same repository never write over each other's state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guards
&lt;/h3&gt;

&lt;p&gt;Hooks block specific things before they happen, and every block is logged so I can replay a guard change over real transcripts before shipping it. Between 3 and 12 October they blocked 140 actions: 45 bare &lt;code&gt;rm&lt;/code&gt;/&lt;code&gt;cp&lt;/code&gt;/&lt;code&gt;mv&lt;/code&gt; calls that would have hit interactive aliases, 33 writes breaking the code-quality rules (em dashes, smart quotes, emoji, inline imports), 26 narrating code comments, 18 unbounded reads or command outputs on a main session, 17 attempts to stop a session after code changes with nothing recorded in the wiki, and one remote shutdown of the GPU box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory
&lt;/h3&gt;

&lt;p&gt;Two stores outlive any single session. second-brain ingests every Claude Code and Cursor transcript and consolidates it into memories any session can recall over MCP; it currently holds 11,255 live memories distilled from 4,880 transcript files. The wiki is an Obsidian vault that sessions write decisions, plans and a dated devlog into, enforced by hooks, which I wrote about in &lt;a href="https://anyesh.me/writing/wiki-devlog-agent-memory" rel="noopener noreferrer"&gt;an earlier post&lt;/a&gt;. Between them, a session that starts cold on a repository it hasn't touched in a week has somewhere to look first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mods
&lt;/h3&gt;

&lt;p&gt;Hooks can block an action or add text to the conversation, but they can't show anything. Claude Code mods can: a plugin can add a segment to the status line, a band above the prompt, a toast, or a slash command. The harness ships five, all added on 11 October, and with them each of the nine panes tells me at a glance which session is about to get expensive, which one hit a guard, and which one hasn't written its scope or devlog.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cache-guard&lt;/strong&gt; learns the real cache lifetime from the hits and misses it observes, then shows the time left on the status line: &lt;code&gt;cache 42m · agents 3 · 1 cold&lt;/code&gt;. Once the cache has expired, the segment turns yellow and shows what the next prompt will cost to re-write (&lt;code&gt;cache cold ≈ $X&lt;/code&gt;). If an idle prompt would re-bill a dollar or more, it stops and asks whether to send anyway, compact first, or keep the prompt as a draft. After a cold turn that re-wrote 50,000 tokens or more, it raises a toast with the cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;guard-watch&lt;/strong&gt; counts every call a guard denied and shows &lt;code&gt;N blocks&lt;/code&gt; on the status line. It raises a toast when a guard blocks a subagent, because those blocks never reach the main transcript, so a subagent could otherwise route around a guard without the main session knowing it had been refused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nudge-band&lt;/strong&gt; puts a band above the prompt when &lt;code&gt;.scope.md&lt;/code&gt; is missing or stale, when nothing has gone into the wiki this session, or when untracked files are piling up, with buttons that pre-fill &lt;code&gt;/scope&lt;/code&gt;, &lt;code&gt;/devlog&lt;/code&gt; or a cleanup prompt. It replaced three shell hooks that did the same job by injecting reminders into the conversation, where every reminder cost context in every later turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;handoff&lt;/strong&gt; runs when a session compacts or is cleared. A shell script collects the facts (files touched, repository state, the last plan, the newest turns) without a model call, and a tool-less fork of the session writes the goal, the next step, the decisions, what was ruled out, open questions, and how to verify. The session that published this post started from a cleared context, read one of those digests, and already knew the draft had been reviewed, which four problems the review found, and that a second post was sitting uncommitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;context-audit&lt;/strong&gt; adds &lt;code&gt;/context-audit&lt;/code&gt;, which totals what the context is spending on MCP servers, skills, memory files, agents and commands, and names up to five things to trim.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/defs&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;



&amp;lt;/g&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="150" height="44" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="44" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="44" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="44" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="720" height="70" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="230" height="48" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="230" height="48" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="64" height="44" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;







&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;


&amp;lt;/g&amp;gt;
&amp;lt;g stroke="" stroke-width="1.5" fill="none"&amp;gt;
  &amp;lt;line x1="280" y1="72" x2="320" y2="72"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="470" y1="72" x2="510" y2="72"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="660" y1="72" x2="700" y2="72"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g stroke="currentColor" stroke-width="1.1" fill="none"&amp;gt;
  &amp;lt;line x1="315" y1="210" x2="315" y2="268"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="665" y1="210" x2="665" y2="268"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="850" y1="175" x2="878" y2="175"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="205" y1="94" x2="205" y2="138"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="395" y1="94" x2="395" y2="138"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="585" y1="94" x2="585" y2="138"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="775" y1="94" x2="775" y2="138"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;Every session runs on the same harness and shares the same two stores. The highlighted lines between panes are Claude Code's own cross-session messages; everything else is the harness.
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;Sessions die and their addresses go stale. A session's address is its socket, and a restarted session gets a new one. A send to the old one fails with &lt;code&gt;ENOENT ... the peer process may have restarted, so this socket path is stale&lt;/code&gt;. The sender usually recovers by listing peers again; when the peer is gone for good, the best it can do is what one did on 30 September: "The wardrowbe-b8 session is gone (its socket no longer exists and it isn't among the live peers), so I'm saving the report where it can be picked up later." When an orchestrating session was about to be replaced, it had every peer also append its reports to a shared inbox file, so a change of name wouldn't lose anything.&lt;/p&gt;

&lt;p&gt;A reset session forgets what it asked for. The memory service asked second-brain for the same release twice, five days apart, because its second session had no memory of the first request. Redraft reported two items as outstanding that had been done three days earlier, then corrected itself: it had "only looked at loom's committed copy and the redraft repo."&lt;/p&gt;

&lt;p&gt;Messages go to the wrong session. On 11 October Loom sent a landing-page brief to the wrong peer and had to follow up: "Please ignore my earlier message about the Loom landing page; I sent it to the wrong session by mistake. No reply needed." The deploy broadcast also reached a Loom session, which answered that it wasn't working in that repository.&lt;/p&gt;

&lt;p&gt;Every session commits as me. When the verdict session found a commit it hadn't made in a checkout it was working in, it asked the harness session whether it was the author. It wasn't, and nothing in git could say which session was, because all of them commit under the same name.&lt;/p&gt;

&lt;p&gt;Two sessions can start the same work. Once, a session found a peer on the same project already building the thing it had just planned to build.&lt;/p&gt;

&lt;p&gt;Each session still has its own human gate. unlearn couldn't stop its GPU process until I approved it, and second-brain wouldn't publish to crates.io without my go-ahead, which is how it's meant to work: a session can ask a peer to act, but it can't use the peer to get around an approval it would need itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it adds up to
&lt;/h2&gt;

&lt;p&gt;Commit counts don't measure this well, and I checked. My weekly commit rate across every repository more than doubled after August, but almost all of the increase is Loom, which didn't exist before September. Outside Loom the rate didn't rise, and in some repositories it fell. What changed is how much runs in parallel and who carries information between the parallel parts. On 1 October nine projects had live sessions, and they exchanged 200 messages that day: API requests, GPU bookings, merge requests, deploy barriers, measured answers with citations. I didn't relay any of them. Some started with a request from me, like the deploy wait, and some stopped for my approval partway, but what reaches me now is mostly the decisions only I can make: an approval in plan mode, a crates.io publish, a production deploy, and a phone push when one of them is waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;If you run more than one agent session, these are the parts that carry over to any setup, not only mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give every repository one owner, and make cross-repository work a request.&lt;/strong&gt; The owning session knows what a visitor doesn't: that a migration number is reserved, that the main checkout takes merges all day, that killing every &lt;code&gt;llama-server&lt;/code&gt; by name would take down someone else's job. In four of the five exchanges above, the owner caught something the asker would have broken or got wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the human gate per session.&lt;/strong&gt; A peer can ask another session to act, but it can't borrow that session's permissions. Plan mode and the crates.io approval both held while the requests came in over messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With many sessions, the bill is cache expiry.&lt;/strong&gt; The expensive moment is the first prompt after an idle hour, when the whole context is re-written. Put the cache clock where you can see it, and keep subagent work to minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your attention is what runs out first, so route it.&lt;/strong&gt; A push when a session is waiting, cancelled as soon as I answer, got 34 of 35 blocked sessions answered in one eighteen-hour stretch, without me cycling through panes to find them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume sessions die.&lt;/strong&gt; Addresses go stale and a reset session forgets what it asked for, so anything that matters has to land somewhere that outlives the session: a shared inbox file, a handoff, the wiki, second-brain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't build a framework for any of this. Claude Code supplied the messaging, and I added the harness, the mods and the rules around it until nine sessions could run side by side all day without me relaying between them.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>claudecode</category>
      <category>developertools</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Open Jev research: a month of chasing coverage at 5% error</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Mon, 12 Oct 2026 10:03:52 +0000</pubDate>
      <link>https://dev.to/anyesh/open-jev-research-a-month-of-chasing-coverage-at-5-error-4bid</link>
      <guid>https://dev.to/anyesh/open-jev-research-a-month-of-chasing-coverage-at-5-error-4bid</guid>
      <description>&lt;p&gt;TypeSafe AI put &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Jev&lt;/a&gt; into limited early access on 15 September 2026. It is a closed "System One" model: you send it a state and a list of typed questions, and it returns a probability for every option instead of generating text. The open clones already matched its accuracy, but Kev's README reports that the best of them, Kev-9B, can answer only 0.45 to 0.57 of decisions automatically at a 5% error rate, against 0.70 for Jev. Between 23 September and 12 October I worked on that gap with open models and a single 16 GB RTX 4070 Ti Super, and when the answer for my own use turned out to be "use the models that already exist", I asked whether decisions of that quality could run on a CPU.&lt;/p&gt;

&lt;p&gt;None of the models I built beat the released ones on the metric the series is about. On the same 1,046-question general set, every 4B to 9B model I scored lands between 0.550 and 0.557 coverage at 5% error: Kev-4B, decider-4b, Cloudflare's Clef-flash, and my llama.cpp port of Kev-4B. Every attempt to get a smaller model there (a raw MoE screen, Kev's training recipe on Granite-H-350M, distilling Kev-4B into Kev-0.8B) finished at or below Kev-0.8B's 0.276. Along the way I built a decision router that serves these models to my agent harness, an adversarial panel for shell-command risk, and three small public models that you can try side by side in a &lt;a href="https://huggingface.co/spaces/anish-shrestha/small-decision-models" rel="noopener noreferrer"&gt;demo Space&lt;/a&gt;. This post is the hub for the series: what Jev is, the one metric every post uses, how the month went, and a map of the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jev and its open copies
&lt;/h2&gt;

&lt;p&gt;Jev exposes one endpoint, &lt;code&gt;POST /v1/systemone&lt;/code&gt;, which takes a &lt;code&gt;state&lt;/code&gt; (text or JSON) and a list of &lt;code&gt;questions&lt;/code&gt;. There are three question types: Choice picks one of up to 255 options and returns per-option probabilities, Score rates against ordered levels, and Noul returns a yes/no probability. TypeSafe says the model is a transformer trained only on synthetic data and post-trained with "Reinforcement Learning for Calibrated Decisions", and it has published no paper, weights or parameter count. Its headline figures (193.6 times faster, 444.6 times cheaper) come from TypeSafe's own workflow evals against the average of two frontier LLMs, while independent tests found about 2.9 times the speed and 12 times lower cost. Archer Hume's &lt;a href="https://archerhume.com/posts/jevs-architecture-unmasked/" rel="noopener noreferrer"&gt;black-box probing&lt;/a&gt; suggests the likeliest design: encode the shared state once, run each question as its own branch over that prefix, and read the answer straight off the option logits.&lt;/p&gt;

&lt;p&gt;Two open projects speak the same API. &lt;a href="https://huggingface.co/jaredpalmer/kev-4b" rel="noopener noreferrer"&gt;Kev&lt;/a&gt;, by Jared Palmer, ships at 0.5B, 0.8B, 4B and 9B, and &lt;a href="https://github.com/NandhaKishorM/laya" rel="noopener noreferrer"&gt;Laya&lt;/a&gt;, mostly written by Nandakishor of Convai Innovations (Apache-2.0), ships a server that speaks Jev's wire format. Two more arrived during the month: decider-4b (Mapika, Apache-2.0) and Cloudflare's Clef-flash (9B, post-trained from Qwen3.5-9B, Apache-2.0, released 1 October). &lt;a href="https://anyesh.me/writing/open-jev-landscape" rel="noopener noreferrer"&gt;The landscape post&lt;/a&gt; covers how these differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric: coverage at 5% error
&lt;/h2&gt;

&lt;p&gt;A decision model is useful in an automated pipeline only if you can trust the answers it is sure of and send the rest to something slower. That is selective prediction: the model answers when it is confident and abstains otherwise. Coverage at 5% error measures how much it can answer under that rule.&lt;/p&gt;

&lt;p&gt;For one task, take every question, read the model's confidence as the probability of its top option, and sort the questions from most to least confident. Accept answers from the top of the list. Coverage is the largest fraction of the task you can accept while the error rate among the accepted answers stays at or under 5%. A task where the 60 most confident of 100 answers contain three mistakes covers at least 0.60. Questions with the same confidence are accepted or rejected together, and a question the model failed to answer is never accepted but still counts in the total. This is &lt;code&gt;coverage_at_error&lt;/code&gt; in Kev's &lt;code&gt;kev/metrics.py&lt;/code&gt;, which forge's scorer reuses per task. The series reports the macro figure: coverage computed per task, then averaged over tasks with equal weight.&lt;/p&gt;

&lt;p&gt;The metric rewards how well confidence is ordered, and that often disagrees with accuracy. Laya is more accurate than Kev-4B on the emotion task (0.613 against 0.550) yet covers nothing there, because its most confident answers are wrong. A raw Qwen3-4B-Instruct readout answered 72% of the general set correctly and covered 0.091, because its confidence does not separate right answers from wrong ones.&lt;/p&gt;

&lt;p&gt;Most numbers in the series come from one instrument, the general set: the development split of Kev's transfer-v9 evaluation, 1,046 questions over 11 tasks that mix public benchmarks (mmlu, sciq, qnli, emotion, paws) with probes for buried evidence, rule composition and unknowable questions. The bar for the month was about 0.55, where Kev-4B (0.555) and Clef-flash (0.550) sit.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the month went
&lt;/h2&gt;

&lt;p&gt;The first four days, 23 to 26 September, went into building my own system. After surveying the open clones I chose to build forge, a synthetic decision-data engine and coverage-first evaluation, with Kev's trainer as the first training backend. forge reproduced Kev's trainer bit for bit, a leakage audit caught the engine giving answers away through look-alike sentences, and a pilot on long buried documents was registered at about 40 GPU-hours.&lt;/p&gt;

&lt;p&gt;From 27 September the work turned toward real decisions. Loom, my agent harness, asked whether a decision model could take calls like command risk and routing off its LLM. That became verdict, a service that answers Loom over the System One API. On 1 October an experiment called K1 found that sending each task to whichever backend covers it best would gain only 1.2 points of coverage over Kev-4B alone, against a 5-point bar, and since a pipeline keeps long context in the LLM and asks the decision model short questions, the long-document pilot was solving a problem nobody had. forge was parked, and Kev and Laya became the models.&lt;/p&gt;

&lt;p&gt;On 2 and 3 October I built a command-risk panel from public security datasets, checked a public leaderboard's ranking against my own panel, and turned verdict into a router once Clef-flash came out.&lt;/p&gt;

&lt;p&gt;On 5 October the goal changed to decisions near Clef-flash quality on a CPU ladder, from a Raspberry Pi 5 with 8 GB up to a laptop x86 CPU. A single-pass decision is a prefill, which is compute-bound, so its cost tracks active parameters times tokens and offloading or streaming weights does not make it faster. I screened small bases, trained Kev's recipe on one, distilled Kev-4B into Kev-0.8B, published the three resulting models, and ported Kev to llama.cpp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results map
&lt;/h2&gt;

&lt;p&gt;The posts go up one at a time, so titles without a link are not out yet.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Headline result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://anyesh.me/writing/open-jev-landscape" rel="noopener noreferrer"&gt;The open Jev landscape&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;JevBench ranks Kev-4B 37th, but on our panel decider-4b only ties it (0.556 against 0.555 coverage)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;forge, a synthetic decision-data engine&lt;/td&gt;
&lt;td&gt;Replays Kev's trainer bit for bit (all 314 loss windows and 392 adapter tensors identical); K1's +1.2 pp against a 5 pp bar parked it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;The command_risk panel and its source leak&lt;/td&gt;
&lt;td&gt;A character n-gram logistic regression scores 0.965 AUROC, against 0.798 for Clef-flash and 0.776 for Kev&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;verdict, a decision router&lt;/td&gt;
&lt;td&gt;Clef-flash ties Kev-4B on false alarms at full risky recall (60.5% against 61.8% on test) and now heads every chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Small decision models on a CPU ladder&lt;/td&gt;
&lt;td&gt;Kev's recipe on Granite-H-350M reaches 0.132 coverage against Kev-0.8B's 0.276&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Distillation buys accuracy, not coverage&lt;/td&gt;
&lt;td&gt;Soft targets add 2.4 pp accuracy over gold labels; coverage stays at 0.250 against a 0.33 kill line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Kev on llama.cpp&lt;/td&gt;
&lt;td&gt;Kev-4B at Q8 covers 0.557 against 0.555 in torch, at about 4.5 GB instead of 18 GB, with no speed gain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6mdp805lxif7skntsxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6mdp805lxif7skntsxy.png" alt="Bar chart of macro coverage at 5% error on the 1,046-question general set: Kev-4B 0.555, its llama.cpp Q8 port 0.557, decider-4b 0.556 and Clef-flash 0.550 form a plateau, while Kev-0.8B 0.276, its gold and distilled deltas 0.256 and 0.250, raw Granite-H-Tiny 0.246, Laya 0.158 and Granite-H-350M with Kev's recipe 0.132 all sit at or below half of it" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The chart puts every general-set number from the series on one axis. The 4B to 9B models are within 0.007 of each other however they were trained or served, and below 1B nothing I tried, whether a different base, more teacher-labelled data or Kev's full recipe, moved coverage above the released Kev-0.8B. The distillation result explains part of that: the teacher's probabilities carried answer knowledge but not a better ordering of confidence, and ordering is what this metric measures.&lt;/p&gt;

&lt;p&gt;Jev's 0.70 was reported on Kev's benchmark, not on the general set, so none of these posts measures the gap to Jev directly. What the month did establish is that the open 4B to 9B models agree at about 0.55, and that getting a sub-1B model near that number is a confidence-ranking problem the methods I tried did not solve.&lt;/p&gt;

</description>
      <category>jev</category>
      <category>systemone</category>
      <category>calibration</category>
      <category>selectiveprediction</category>
    </item>
    <item>
      <title>The open Jev clones: three ways to read a decision, and a leaderboard that does not transfer</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Mon, 12 Oct 2026 10:03:50 +0000</pubDate>
      <link>https://dev.to/anyesh/the-open-jev-clones-three-ways-to-read-a-decision-and-a-leaderboard-that-does-not-transfer-4aea</link>
      <guid>https://dev.to/anyesh/the-open-jev-clones-three-ways-to-read-a-decision-and-a-leaderboard-that-does-not-transfer-4aea</guid>
      <description>&lt;p&gt;Within a week of TypeSafe AI opening early access to &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Jev&lt;/a&gt;, its closed "System One" decision model, GitHub was full of open look-alikes, and by October an independent board, JevBench, was ranking more than a hundred systems on sealed items. I needed one of these models to answer decisions for Loom, my agent harness, so the question for this post is practical: which open clone should sit behind that endpoint, and can I trust the public ranking to tell me?&lt;/p&gt;

&lt;p&gt;The clones split into three mechanism families, and the two I studied most closely, Kev and Laya, sit in different ones. JevBench ranks Kev-4B 37th with a score of 39.9, behind decider-4b at 55.8 and behind a stock Qwen3-4B-Instruct read at its option logits at 54.1. When I scored those models on my own panel, by coverage at 5% error (the share of questions a model can answer on its own while keeping its error on those under 5%; defined in &lt;a href="https://anyesh.me/writing/open-jev-research-overview" rel="noopener noreferrer"&gt;the series overview&lt;/a&gt;), decider-4b tied Kev-4B at 0.556 against 0.555, and the stock Qwen readout covered 0.091. The ranking did not transfer, and Kev-4B stayed the pinned model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the clones are copying
&lt;/h2&gt;

&lt;p&gt;Jev takes a &lt;code&gt;state&lt;/code&gt; (text or JSON) and a list of typed questions, and returns probabilities instead of text. A &lt;strong&gt;Choice&lt;/strong&gt; question picks one of up to 255 options and returns a probability per option, a &lt;strong&gt;Score&lt;/strong&gt; question rates against ordered levels, and a &lt;strong&gt;Noul&lt;/strong&gt; returns a yes/no probability. TypeSafe says it is a transformer trained on synthetic data and post-trained with "Reinforcement Learning for Calibrated Decisions" (RLCD); there is no paper, no weights and no parameter count.&lt;/p&gt;

&lt;p&gt;The most useful public guess comes from Archer Hume's &lt;a href="https://archerhume.com/posts/jevs-architecture-unmasked/" rel="noopener noreferrer"&gt;black-box probing&lt;/a&gt; of roughly 10,000 API calls. Latency tracks input tokens rather than option count, and content injected into one question does not leak into another, which fits a causal transformer that encodes the state once and runs each question as an isolated branch over that shared prefix (the pattern from Hydragen and DeFT), ending in a softmax over option logits with no text generated. A minority view (djev) holds that Jev is a diffusion model; Hume argues diffusion is not needed to explain what the API does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to read a decision
&lt;/h2&gt;

&lt;p&gt;Every open clone has to answer the same two questions: what hidden state does the decision head read, and how does that become a distribution over options? The answers fall into three families.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pointer head on a causal LLM&lt;/strong&gt; (&lt;a href="https://github.com/jaredpalmer/kev" rel="noopener noreferrer"&gt;Kev&lt;/a&gt; by Jared Palmer, and TinyJev). Kev packs one sequence per request: &lt;code&gt;&amp;lt;state&amp;gt; ... &amp;lt;q&amp;gt; instruction &amp;lt;opt&amp;gt; option1 &amp;lt;/opt&amp;gt; &amp;lt;opt&amp;gt; option2 &amp;lt;/opt&amp;gt; ... &amp;lt;decide&amp;gt;&lt;/code&gt;, with one &lt;code&gt;&amp;lt;q&amp;gt; ... &amp;lt;decide&amp;gt;&lt;/code&gt; block per question. The delimiters reuse rarely used Qwen special tokens, so no embeddings are resized. A block-causal mask lets each question's tokens attend to the state and to their own question block only, and position ids restart after the state for every question, so questions share the state but cannot read each other. The head has no vocabulary readout at all: two linear projections map the &lt;code&gt;&amp;lt;decide&amp;gt;&lt;/code&gt; hidden state and each option's &lt;code&gt;&amp;lt;/opt&amp;gt;&lt;/code&gt; hidden state into a 256-dimensional space, the scaled dot products become one logit per option, and a softmax over options gives the distribution. The base (Qwen3.5 at 0.8B, 4B or 9B) is frozen with a rank-16 LoRA adapter. Qwen3.5 interleaves Gated DeltaNet layers, which are recurrent and cannot honor a block mask, so on those bases each question runs as its own causal row continuing a cached state prefix, and a cache miss costs one forward pass instead of one per question. Calibration is a single post-hoc temperature per checkpoint (2.41 for the Kev-4B I pin).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mask-marker encoder&lt;/strong&gt; (&lt;a href="https://github.com/NandhaKishorM/laya" rel="noopener noreferrer"&gt;Laya&lt;/a&gt; by Nandakishor Mukkunnoth of Convai Innovations). Laya is an encoder, ModernBERT-large at 421M parameters. Each question becomes one sequence, &lt;code&gt;[CLS] &amp;lt;qtype&amp;gt; instructions [SEP] [MASK] opt0 [MASK] opt1 ... [SEP] state [SEP]&lt;/code&gt;, with a &lt;code&gt;[MASK]&lt;/code&gt; token inserted before every option. A two-layer transformer head runs over the encoder output, and a small shared scorer turns the hidden state at each &lt;code&gt;[MASK]&lt;/code&gt; position into one logit per option; a softmax over those, with a temperature per question type and option-count bucket, gives the probabilities. All of a request's questions are padded into one batch and answered in one call. Because every option has to fit in a fixed token budget, the option count is capped: on Banking77's 77 labels each option gets about 3 to 4 tokens. Training uses a strictly proper scoring rule (log score plus spherical score, plus a ranked probability term for ordinal questions) as a policy-gradient reward, which Laya's documentation calls RLCD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First-token logit readout&lt;/strong&gt; (&lt;a href="https://github.com/TheoLeeCJ/SemIf" rel="noopener noreferrer"&gt;SemIf&lt;/a&gt;, formerly OpenJev, and &lt;a href="https://github.com/kikoncuo/jevfire" rel="noopener noreferrer"&gt;jevfire&lt;/a&gt;). These need no training. The options are written as single tokens (letters A, B, C and so on), a stock LLM runs one forward pass, and the decision is a softmax over the logits of those candidate tokens at the first answer position, the same trick as FIRST (&lt;a href="https://arxiv.org/abs/2406.15657" rel="noopener noreferrer"&gt;arXiv 2406.15657&lt;/a&gt;) for listwise reranking. jevfire adds shared-prefix KV in vLLM so many questions over one state stay cheap. Two failure modes recur: ambiguous single-letter tokens (space-prefixed against byte-level) can silently corrupt the readout, and letter-slot variants run out of single tokens at about 26 to 52 options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kev and Laya on their own numbers
&lt;/h2&gt;

&lt;p&gt;On sources it never trained on, Kev-9B reaches 0.852 test accuracy against Jev's 0.857 on the development split, a comparison Kev's own README flags as uncontrolled. The gap that remains is coverage at 5% error: Kev covers 0.45 to 0.57 against Jev's 0.70. Kev's author tried to close it during training (label smoothing, Brier and focal losses), and that round came back negative under its pre-registered gate.&lt;/p&gt;

&lt;p&gt;Laya reports beating Jev on its own benchmark (typed-decisions accuracy 0.766 against 0.727, ECE 0.081 against 0.246), but every Jev number in those comparisons is a published third-party figure, so each one is cross-study. One independent test put Laya at 0.590 against Jev's 0.974, the lowest accuracy of the clones it compared and the fastest at about 30 ms. Laya's documentation is open about its own losses: Banking77 at 0.425 to 0.492 against Jev's 0.870, and an English checkpoint that scores 0.000 on Khmer at 0.952 mean confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What JevBench says
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://benchmarkheaven.com/api/jevbench/v1.5.4" rel="noopener noreferrer"&gt;JevBench v1.5.4&lt;/a&gt; is the one board that runs many clones on the same items: 106 ranked systems, 1,624 items of which 720 are sealed, on evaluator-owned GPUs. "Intelligence" is a chance-adjusted accuracy, and the composite rank also weights speed and cost.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;th&gt;Intelligence&lt;/th&gt;
&lt;th&gt;Choice ECE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Cygnet&lt;/td&gt;
&lt;td&gt;frozen Gemma-4-12B-it, one-token readout&lt;/td&gt;
&lt;td&gt;71.1&lt;/td&gt;
&lt;td&gt;0.041&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Winnow-12B&lt;/td&gt;
&lt;td&gt;Gemma-4-12B-it LoRA&lt;/td&gt;
&lt;td&gt;74.4&lt;/td&gt;
&lt;td&gt;0.085&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Jev 1.13.0&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;td&gt;72.0&lt;/td&gt;
&lt;td&gt;0.027&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;decider-4b v2&lt;/td&gt;
&lt;td&gt;Qwen3.5-4B-Base&lt;/td&gt;
&lt;td&gt;55.8&lt;/td&gt;
&lt;td&gt;0.049&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;raw Qwen3-4B-Instruct-2507&lt;/td&gt;
&lt;td&gt;stock, option logits&lt;/td&gt;
&lt;td&gt;54.1&lt;/td&gt;
&lt;td&gt;0.329&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;Kev-4B&lt;/td&gt;
&lt;td&gt;LoRA + pointer head&lt;/td&gt;
&lt;td&gt;39.9&lt;/td&gt;
&lt;td&gt;0.278&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;Laya&lt;/td&gt;
&lt;td&gt;ModernBERT-large&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.149&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The top open system is a first-token readout on a frozen 12B model, and a 4B model read the same way with no training at all beats Kev-4B by 14 points, which on the board's face meant I was serving the wrong model.&lt;/p&gt;

&lt;p&gt;Provenance narrowed the candidates before any measurement. Nimble's pipeline contains a bridge that calls the Jev API although its README says it was not distilled from Jev, JevK5 is reported to include GPT outputs, and several "jev-distill" models train on Jev outputs. &lt;a href="https://huggingface.co/Mapika/decider-4b" rel="noopener noreferrer"&gt;decider-4b&lt;/a&gt; (by Mapika, Apache-2.0) states a local Qwen3.5-27B teacher and nothing distilled from Jev, which made it the best-placed 4B candidate. The raw Qwen readout was the training-free control.&lt;/p&gt;

&lt;h2&gt;
  
  
  K2: scoring the board's picks on my panel
&lt;/h2&gt;

&lt;p&gt;The board's task mix is not Loom's: Loom asks short questions, and it needs a model that knows when not to answer more than one that answers most things right. The board's Kev row also describes a Qwen3-4B base, while I pin &lt;a href="https://huggingface.co/jaredpalmer/kev-4b" rel="noopener noreferrer"&gt;&lt;code&gt;jaredpalmer/kev-4b&lt;/code&gt;&lt;/a&gt; at revision &lt;code&gt;139fdd9&lt;/code&gt;, so I could not tell whether it was the same model. I pre-registered a check, K2, before running anything: score decider-4b and the raw Qwen readout on the same panel Kev-4B had already been scored on, and swap only if a candidate beats Kev-4B by at least 5 points of macro coverage at 5% error with a bootstrap lower bound above zero, on both the full panel and the panel without emotion and sciq (decider's training mixture touches those two).&lt;/p&gt;

&lt;p&gt;The panel is the development split of Kev's frozen transfer-v9 eval suite: 11 tasks and 1,046 questions. Both new arms ran on my GPU box (a 16 GB RTX 4070 Ti Super) through decider's own server, the raw Qwen arm through its chat readout layout with no temperature, so that arm measures the stock model plus decider's prompt and readout. The Kev-4B and Laya rows are reused from the earlier K1 run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fac3m7ohoye1scwx93bfb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fac3m7ohoye1scwx93bfb.png" alt="Two bar charts side by side for the same four models. On JevBench intelligence, decider-4b scores 55.8, raw Qwen3-4B 54.1, Kev-4B 39.9 and Laya 0.0. On coverage at 5% error on my 11-task panel, decider-4b scores 0.556, raw Qwen3-4B 0.091, Kev-4B 0.555 and Laya 0.158" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;11-task macro&lt;/th&gt;
&lt;th&gt;Difference from Kev-4B (lower bound)&lt;/th&gt;
&lt;th&gt;9-task macro&lt;/th&gt;
&lt;th&gt;Difference (lower bound)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kev-4B&lt;/td&gt;
&lt;td&gt;0.555&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0.549&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decider-4b&lt;/td&gt;
&lt;td&gt;0.556&lt;/td&gt;
&lt;td&gt;+0.1 pp (-9.1)&lt;/td&gt;
&lt;td&gt;0.518&lt;/td&gt;
&lt;td&gt;-3.2 pp (-13.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;raw Qwen3-4B&lt;/td&gt;
&lt;td&gt;0.091&lt;/td&gt;
&lt;td&gt;-46.4 pp (-54.7)&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;-54.9 pp (-64.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laya&lt;/td&gt;
&lt;td&gt;0.158&lt;/td&gt;
&lt;td&gt;-39.7 pp (-46.6)&lt;/td&gt;
&lt;td&gt;0.131&lt;/td&gt;
&lt;td&gt;-41.8 pp (-50.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing came close to the swap rule, so Kev-4B stayed.&lt;/p&gt;

&lt;p&gt;The raw Qwen readout answers 72% of the panel correctly and still covers only 0.091, because at temperature 1.0 its confidence does not separate right answers from wrong ones. No threshold keeps its error under 5% on any task except sciq, where every arm covers 1.000. JevBench's 54.1 rewards accuracy, while my swap rule rewards knowing when to answer.&lt;/p&gt;

&lt;p&gt;decider-4b and Kev-4B tie on the macro with opposite strengths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Kev-4B coverage&lt;/th&gt;
&lt;th&gt;decider-4b coverage&lt;/th&gt;
&lt;th&gt;Kev-4B accuracy&lt;/th&gt;
&lt;th&gt;decider-4b accuracy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unknowable_control&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.691&lt;/td&gt;
&lt;td&gt;0.955&lt;/td&gt;
&lt;td&gt;0.827&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;composition_holdout&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.750&lt;/td&gt;
&lt;td&gt;0.979&lt;/td&gt;
&lt;td&gt;0.896&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buried&lt;/td&gt;
&lt;td&gt;0.375&lt;/td&gt;
&lt;td&gt;0.075&lt;/td&gt;
&lt;td&gt;0.700&lt;/td&gt;
&lt;td&gt;0.675&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;legacy_holdout&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;td&gt;0.662&lt;/td&gt;
&lt;td&gt;0.812&lt;/td&gt;
&lt;td&gt;0.738&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;paws&lt;/td&gt;
&lt;td&gt;0.275&lt;/td&gt;
&lt;td&gt;0.188&lt;/td&gt;
&lt;td&gt;0.787&lt;/td&gt;
&lt;td&gt;0.713&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qnli&lt;/td&gt;
&lt;td&gt;0.825&lt;/td&gt;
&lt;td&gt;0.925&lt;/td&gt;
&lt;td&gt;0.912&lt;/td&gt;
&lt;td&gt;0.925&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mmlu&lt;/td&gt;
&lt;td&gt;0.350&lt;/td&gt;
&lt;td&gt;0.575&lt;/td&gt;
&lt;td&gt;0.725&lt;/td&gt;
&lt;td&gt;0.725&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_offensive&lt;/td&gt;
&lt;td&gt;0.312&lt;/td&gt;
&lt;td&gt;0.588&lt;/td&gt;
&lt;td&gt;0.738&lt;/td&gt;
&lt;td&gt;0.787&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;emotion&lt;/td&gt;
&lt;td&gt;0.163&lt;/td&gt;
&lt;td&gt;0.463&lt;/td&gt;
&lt;td&gt;0.550&lt;/td&gt;
&lt;td&gt;0.762&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mmlu_pro&lt;/td&gt;
&lt;td&gt;0.005&lt;/td&gt;
&lt;td&gt;0.205&lt;/td&gt;
&lt;td&gt;0.560&lt;/td&gt;
&lt;td&gt;0.505&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sciq&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.975&lt;/td&gt;
&lt;td&gt;0.988&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;decider-4b is better on knowledge and classification tasks (mmlu, mmlu_pro, qnli, tweet_offensive, and emotion, which its training mixture may have seen). Kev-4B is better on buried evidence, held-out rule compositions and above all unknowable_control, the questions where the deciding evidence was removed and answering is the error. That is the task that matters most to Loom, and Kev-4B covers 1.000 of it against decider-4b's 0.691. decider-4b answered at 48 ms p50 against Kev-4B's 553 ms, but the two ran on different serving stacks, so that gap mixes model and server.&lt;/p&gt;

&lt;p&gt;Running decider took three fixes on the GPU box: its server needs &lt;code&gt;flash-linear-attention&lt;/code&gt; and &lt;code&gt;causal-conv1d&lt;/code&gt;, so it ran in WSL; its CUDA-graph warm-up did not fit 16 GB, so it ran eager; and the WSL image could not live on a nearly full system drive, because filling it corrupted the distro once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the pick
&lt;/h2&gt;

&lt;p&gt;The readout family decides what the head reads (option tokens in the vocabulary, a pointer between two hidden states, or a marker in a bidirectional encoder) and with it the option cap and the cost per question. The top open system on JevBench is a first-token readout, yet on my panel a training-free first-token readout covered 0.091 because its confidence could not tell right from wrong. For a caller that has to abstain, what matters is whether the model's confidence orders right answers above wrong ones, and JevBench's intelligence axis does not measure that. A per-task mix of Kev-4B and decider-4b looks tempting given the opposite strengths, but the earlier K1 run found only +1.2 points of oracle headroom for per-task selection with Laya as the second model, and this panel was not built to choose between models, so any follow-up needs fresh items. K2 kept Kev-4B as the pinned model in verdict, the decision service Loom calls.&lt;/p&gt;

</description>
      <category>jev</category>
      <category>kev</category>
      <category>laya</category>
      <category>calibration</category>
    </item>
    <item>
      <title>My coding agents keep a record of what they decided and why, not just what they built</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Sat, 29 Aug 2026 21:55:14 +0000</pubDate>
      <link>https://dev.to/anyesh/my-coding-agents-keep-a-record-of-what-they-decided-and-why-not-just-what-they-built-igk</link>
      <guid>https://dev.to/anyesh/my-coding-agents-keep-a-record-of-what-they-decided-and-why-not-just-what-they-built-igk</guid>
      <description>&lt;p&gt;A session with a coding agent ends and whatever it figured out along the way goes with it. The code survives, since it's on disk. The reasoning behind the code doesn't: why this table got a composite index instead of two separate ones, why the retry logic gave up after three attempts instead of five, which approach got tried first and thrown out. Open a new session next week on the same project and the agent starts from the code alone, with no memory of any of that.&lt;/p&gt;

&lt;p&gt;Claude Code has a memory system for this, and it's genuinely good at what it does. Cursor has nothing like it. I run the two on different machines for different kinds of work, Claude Code where I vibecode, Cursor on a separate box for the work I want to sit with, so it was never a question of getting one tool's session context into the other's. What I wanted was a record of the reasoning that outlives any single machine: an Obsidian vault, synced with the self-hosted &lt;code&gt;obsidian-livesync&lt;/code&gt; plugin, that both machines write into and that also lands on my phone for a quick read when I'm nowhere near either one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three hooks doing what a prompt can't
&lt;/h2&gt;

&lt;p&gt;Telling an agent in its system prompt to "keep a devlog" doesn't work reliably. It's one more instruction competing with the task itself, and the task wins almost every time, because the task is enforced by the point of the session and the devlog instruction is enforced by nothing. What actually holds is three separate hooks in the harness, each firing at a different point in the session, backing each other up.&lt;/p&gt;

&lt;p&gt;The first hook, &lt;code&gt;session-start-wiki.sh&lt;/code&gt;, runs when a session opens and hands the agent the last three devlog entries plus an index of every project in the wiki, so it starts already knowing what happened last time instead of re-deriving it from the code. The context it injects is one fixed block: &lt;code&gt;WIKI: project={slug} vault={wiki_vault}&lt;/code&gt;, followed by an instruction to write plans, decisions, spikes, and devlog entries as it works.&lt;/p&gt;

&lt;p&gt;The second, &lt;code&gt;wiki-nudge.sh&lt;/code&gt;, checks in every fifth turn. If no wiki file has been touched yet this session, it injects a reminder pointing at the exact devlog path to write to and the index file that has to be updated alongside it, and it repeats on the same schedule until something gets written.&lt;/p&gt;

&lt;p&gt;The third is the one that actually forces the issue. &lt;code&gt;stop-wiki-enforce.sh&lt;/code&gt; runs when the session is about to end, counts how many &lt;code&gt;Write&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt;, or &lt;code&gt;NotebookEdit&lt;/code&gt; calls happened in the transcript, and if three or more code changes went by with no devlog or index update to match, it blocks the stop outright. In Claude Code that's a &lt;code&gt;{"decision": "block"}&lt;/code&gt; response the hook returns; in Cursor, which doesn't expose a transcript path the same way, it falls back to checking whether anything in the wiki directory changed in the last two hours. Either way the session doesn't get to close clean. The reason for it gets re-fed as the next prompt, and the agent has to either write the entry or explain why this particular session didn't need one.&lt;/p&gt;

&lt;p&gt;A fourth hook applies the same idea to a different failure mode. Long sessions get compacted to fit the context window, and anything unwritten before that happens disappears the same way it would at session end. &lt;code&gt;pre-compact.sh&lt;/code&gt; fires right before that compression runs and gives the agent one explicit last chance to write down any plans, decisions, spikes, or concepts from the session, since compaction is the point where the context actually goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually gets written
&lt;/h2&gt;

&lt;p&gt;The wiki has a fixed shape: &lt;code&gt;wiki/projects/&amp;lt;slug&amp;gt;/&lt;/code&gt; holds an &lt;code&gt;overview.md&lt;/code&gt;, a &lt;code&gt;devlog.md&lt;/code&gt;, and subfolders for &lt;code&gt;decisions/&lt;/code&gt;, &lt;code&gt;plans/&lt;/code&gt;, and &lt;code&gt;spikes/&lt;/code&gt;, plus a top-level &lt;code&gt;index.md&lt;/code&gt; that lists every project. Each page type has its own template. A decision page has to name the alternatives that were on the table alongside the one that won. A spike page states the question it was answering before it gets to the finding. The devlog format is the strictest: a dated heading, two to four bullets, written as plain fact rather than a "here's what we did this session" narration, with no name attached and no hedged phrasing softening what happened. That strictness is what keeps a year of entries readable in one sitting rather than turning into the same throat-clearing paragraph with a new date stamped on it.&lt;/p&gt;

&lt;p&gt;The hooks themselves are identical on both sides. &lt;code&gt;session-start-wiki.sh&lt;/code&gt;, &lt;code&gt;wiki-nudge.sh&lt;/code&gt;, and &lt;code&gt;stop-wiki-enforce.sh&lt;/code&gt; are deployed to &lt;code&gt;~/.claude/&lt;/code&gt; and &lt;code&gt;~/.cursor/&lt;/code&gt; unchanged, same triggers, same thresholds, and only the JSON shape of the block response differs because the two tools expect different formats. The harness's own docs never claim Cursor needs this more because it lacks a native memory system; that's my own read of the setup, not a documented rationale. I think it holds up anyway. Claude Code's memory already softens the cost of a missed devlog entry, where Cursor has no such fallback, so the same devlog is doing more of the actual work for Cursor sessions than for Claude Code ones.&lt;/p&gt;

&lt;p&gt;This doesn't replace what Claude Code's memory does. That system decides on its own what's worth keeping and recalls it without me asking, where the wiki only surfaces the specific file a hook decided to inject. What the wiki adds is a shared, greppable record that exists independent of either tool, readable on a phone as easily as on either machine, in a text editor, or in a completely different tool six months from now, because it's a stack of plain markdown files with a template, sitting on disk, that I could read at 2am with nothing running at all.&lt;/p&gt;

&lt;p&gt;The harness's own README states the philosophy bluntly: "The human never writes the wiki. The LLM writes and maintains all of it." That line only holds because the enforcement backs it up. Without the stop hook it would just be an aspiration; with it, a session that closes on three or more real changes has to have written the record of them first.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>claudecode</category>
      <category>developertools</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Scheduling every job on a GPU that can only hold one model</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Sat, 29 Aug 2026 21:54:33 +0000</pubDate>
      <link>https://dev.to/anyesh/scheduling-every-job-on-a-gpu-that-can-only-hold-one-model-1n</link>
      <guid>https://dev.to/anyesh/scheduling-every-job-on-a-gpu-that-can-only-hold-one-model-1n</guid>
      <description>&lt;p&gt;A Raspberry Pi 5 runs DNS for the network, plus dozens of other small services that don't need much CPU or memory but can't go down. A Jetson Orin Nano handles anything that needs a GPU, and its GPU and CPU share one memory pool, small enough to hold one loaded model or one active render job, never both.&lt;/p&gt;

&lt;h2&gt;
  
  
  One flat network
&lt;/h2&gt;

&lt;p&gt;There's one LAN. Clients and services aren't split into VLANs or separated behind an internal firewall; the upstream router is the gateway, and everything else sits behind it as a peer. If one device gets compromised, there's nothing stopping it from reaching every other device on the LAN. I've thought about segmenting it and decided against it: at this size, a mesh of routing rules is more to maintain than the threat justifies. It stays flat.&lt;/p&gt;

&lt;p&gt;The Pi carries DNS and everything that has to be up all the time. It's picked for uptime and low draw, not speed, and I don't let it run anything that could pin its CPU long enough to take DNS down with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One model at a time
&lt;/h2&gt;

&lt;p&gt;Load two things onto the Jetson that it can't both hold, and it thrashes. So nothing talks to it directly: every job (a chat request, a background research task, a scheduled scrape) lands in a durable queue first, and a single gateway between that queue and the Jetson admits exactly one job at a time, by priority. That gateway is most of the engineering here. Skip it and two jobs landing at once both try to load into memory built for one, and now the box is fighting itself instead of doing either job.&lt;/p&gt;

&lt;p&gt;Priority doesn't preempt. If a background scrape is already loaded and running when a chat request comes in, the chat request doesn't kill it, it queues behind it and jumps ahead of everything else waiting. The alternative, killing a running job because something more urgent showed up, throws away whatever work was already done and risks leaving the model half-loaded. Waiting for the current job to finish costs a few seconds at most; a job that has to restart from zero costs more than that most of the time.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/defs&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="280" height="56" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="76" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="56" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="60" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="70" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="76" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="56" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;line x1="180" y1="86" x2="180" y2="126"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="180" y1="206" x2="180" y2="246"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="180" y1="306" x2="180" y2="346"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="180" y1="410" x2="180" y2="446"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="540" y1="206" x2="540" y2="446"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;path d="M 400,180 C 300,230 260,300 300,352"&amp;gt;&amp;lt;/path&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;



















&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;Everything queues through one gateway before it reaches the Jetson, except the RTX box's own render work, which never goes near it.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;A second machine, with an RTX 4070 Ti Super, does rendering, and it doesn't go through this gateway for that work. The gateway exists to arbitrate the Jetson's single model slot, and the RTX box was never competing for that slot to begin with, so routing it through anyway would just add latency it doesn't need. It does share one thing with the Jetson: a single LLM call both need. For that one call, the two machines are fighting over the same resource, so it queues like everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The price of running it this way
&lt;/h2&gt;

&lt;p&gt;A Pi 5, a Jetson Orin Nano, and a desktop GPU: together roughly the price of a decent laptop.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosting</category>
      <category>networkarchitecture</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>hearth is what I built to schedule every job on my homelab's GPU</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Sat, 29 Aug 2026 21:54:31 +0000</pubDate>
      <link>https://dev.to/anyesh/hearth-is-what-i-built-to-schedule-every-job-on-my-homelabs-gpu-534c</link>
      <guid>https://dev.to/anyesh/hearth-is-what-i-built-to-schedule-every-job-on-my-homelabs-gpu-534c</guid>
      <description>&lt;p&gt;Every scheduled job in my homelab runs through a platform I built called hearth, and one piece of it is a gateway that admits one job at a time to whichever machine happens to be running a model. It runs on the Pi, and it exists because nothing else that could do the job fit the hardware. Kubernetes-style orchestration (k3s, Nomad) carries its own memory tax, and the Pi was already sitting at 5 GiB used plus 4.4 GiB of swap before adding a scheduler to the pile. Plain systemd timers per service were the other option, and they don't do priority ordering, backpressure, or a shared budget across jobs, which is most of what a GPU with one model slot needs.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/defs&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="150" height="70" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="70" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="70" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="70" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="70" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="110" height="50" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="110" height="50" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;line x1="170" y1="185" x2="196" y2="185"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="350" y1="185" x2="376" y2="185"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="530" y1="185" x2="556" y2="185"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="710" y1="185" x2="736" y2="185"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="245" y1="220" x2="200" y2="286"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="280" y1="220" x2="325" y2="286"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;



















&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;The two numbers that guard the gateway, the duration estimate and the output cap, both live at the one point that can measure them.
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A queue that survives a reboot for free
&lt;/h2&gt;

&lt;p&gt;hearth's queue is SQLite in WAL mode, not Redis or Celery. Memory is the scarce resource on this box, not throughput, and a SQLite file on disk means the queue's state survives a reboot without anything extra. Jobs carry a priority (interactive, user, batch, best-effort), a manifest-driven schedule, retry with exponential backoff, and a dead-letter path for anything that needs a human to look at it. If a job depends on something that's down, it parks instead of burning retries against a dependency that isn't coming back soon.&lt;/p&gt;

&lt;p&gt;Each job runs as a subprocess inside its own &lt;code&gt;systemd-run&lt;/code&gt; scope, and every resource class maps to a systemd slice with its own memory and CPU limits. An OOM kills the one job in that slice, not the host.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/g&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="340" height="210" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="150" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="340" height="210" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="40" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="40" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="40" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g stroke="" stroke-width="2"&amp;gt;
  &amp;lt;line x1="20" y1="50" x2="360" y2="260"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="360" y1="50" x2="20" y2="260"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="740" y1="140" x2="760" y2="170"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="760" y1="140" x2="740" y2="170"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;






&amp;lt;/g&amp;gt;
&amp;lt;g fill=""&amp;gt;

&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;

&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;Without a slice boundary, one job's OOM takes the whole host down. With one, it's contained to the slice that job runs in.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Adding a resource class costs two files, not one, a manifest entry and a matching slice unit, and there's a test that fails loudly if the second file is missing. A job in a class with no slice unit doesn't error at validation, it dies five seconds into every run with no output and nothing in the journal, so the two-file rule exists to keep that failure mode rare.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gateway learns durations from the worst recent run, not the typical one
&lt;/h2&gt;

&lt;p&gt;hearth's scheduler will only start a queued job ahead of a fixed time window if it can promise the job finishes before that window closes, which means it needs an honest estimate of how long the job takes. The first version learned that estimate as the median of the last five runs, on the reasoning that one bad night shouldn't poison the number.&lt;/p&gt;

&lt;p&gt;One of the jobs that runs through hearth is bimodal: about twenty minutes when it finds real work, about five seconds when it doesn't. Two of its three recorded runs were quiet ones, so the learned median came out to six seconds against a real run that took twenty-one minutes. Under that estimate, the scheduler would have started the job thirty seconds before a workday window it had no chance of clearing. Deleting the two quiet runs fixed the number for twenty minutes, until a fresh quiet run dragged it right back down, which is what proved the fix was against the symptom, not the cause: a bimodal job produces quiet runs indefinitely, and a median can't tell a normal quiet run from an anomaly.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/defs&amp;gt;
&amp;lt;g stroke="currentColor" stroke-width="1"&amp;gt;
  &amp;lt;line x1="30" y1="200" x2="790" y2="200"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="219" y1="200" x2="219" y2="206"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="452" y1="200" x2="452" y2="206"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="40" y1="40" x2="234" y2="40"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="234" y1="40" x2="234" y2="200"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="755" y1="30" x2="755" y2="200" stroke=""&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="40" y1="70" x2="219" y2="70"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="40" y1="110" x2="219" y2="110"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="40" y1="150" x2="752" y2="150"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;







&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;The median estimate sits next to the two quiet runs and ignores the one real run entirely; the max estimate sits next to the real run.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The estimate is now the maximum of the last five runs, not the median. Verified against the same history: max reads 1277 seconds where median read 641. Under-promising is the dangerous direction here, since a job that starts and gets killed partway wastes the work it did before dying, while over-promising only makes something wait a little longer, which costs nothing. A job with occasional long runs is always scheduled as if it will take the long path, so fewer jobs fit into small gaps in an otherwise empty day. For a scheduler whose entire job is filling a mostly idle day, that's a fine trade. It wouldn't be on hardware that's busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output cap moved out of every caller and into the gateway
&lt;/h2&gt;

&lt;p&gt;Every service calling into the GPU box's model used to hardcode its own token limit, and every single one of those numbers was wrong. Twenty-nine calls in one day landed on exactly 1024 tokens against prompts of 600 to 900 tokens, some of them for a job scoring zero results out of eighty-one it should have judged, because the model's answer got cut mid-word before it finished.&lt;/p&gt;

&lt;p&gt;No caller can size this correctly, because the two facts that determine a correct cap don't live where the caller lives. The context window is a property of the model config, and the generation rate is a property of the hardware, and a caller has access to neither, so any number it writes down is a guess. Guesses land low, because a low guess only fails sometimes, and it takes a while to notice.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/g&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="51" height="24"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="48" height="24"&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;rect width="620" height="24" fill=""&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;



&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;The guessed cap barely covered the prompt itself, let alone an answer. The derived cap comes from the timeout budget and the model's measured speed, the two facts only the gateway has.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The fix moved the cap to the one place that has both facts: the gateway derives it from the request's timeout budget times the model's measured tokens per second, and fills that number in whenever a caller sends none. One of the models running through hearth is both the slowest measured, at 13.9 tokens a second against a faster model's 17.5, and the one with the largest context window, so an unbounded generation on that model alone could hold the GPU for close to an hour under a single-slot gateway. Sizing the cap to the same time budget the timeout already enforces costs the same wall clock in the worst case, but comes back with a usable partial answer instead of nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second GPU doesn't get routed through a chokepoint built for a different GPU
&lt;/h2&gt;

&lt;p&gt;There's a second machine on the network with its own GPU, a dedicated render box that runs its own copy of the render pipeline directly against its own REST API, not through hearth's gateway. The gateway's entire job is arbitrating time on the GPU box's single model slot, and the second box was never competing for that slot in the first place, so sending its traffic through anyway would add nothing but latency.&lt;/p&gt;

&lt;p&gt;It does share one thing with the GPU box: a short script-writing call that only the GPU box can serve, since the second box has no model of its own. That one call goes through the gateway, under its own named budget, so heavy experimentation on the render side can't starve the budget an established job already depends on. The rest of that box's work gets its own resource class rather than sharing the class built to stop the GPU box double-booking itself, because serializing a render job behind the GPU box's queue would waste GPU time on hardware that was never in contention to begin with. Reachability is checked the same way hearth checks any dependency, a live probe declared once in the job's manifest, not a guard written into the job itself.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosting</category>
      <category>systemdesign</category>
      <category>scheduling</category>
    </item>
    <item>
      <title>I run browser automation inside a disposable VM</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:00:08 +0000</pubDate>
      <link>https://dev.to/anyesh/i-run-browser-automation-inside-a-disposable-vm-2le5</link>
      <guid>https://dev.to/anyesh/i-run-browser-automation-inside-a-disposable-vm-2le5</guid>
      <description>&lt;p&gt;Automating a browser that holds real logins means running code that fetches arbitrary pages on the open internet. That code needs to be treated as something that can be compromised. That's the assumption behind every decision here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a VM instead of a sandboxed process
&lt;/h2&gt;

&lt;p&gt;The browser automation on this network runs inside a disposable virtual machine, launched on demand and torn down after each job. The reason isn't abstract: Chromium's rendering engine assumes 4 KB memory pages, and the Pi it needed to run on uses a kernel built with 16 KB pages, so navigation hung outright on the host. The direct fix, rebuilding the Pi's kernel with 4 KB pages, broke the Pi's Ethernet and took the home network's DNS down with it. That ruled out patching the host to match the browser.&lt;/p&gt;

&lt;p&gt;Running the browser inside its own small guest, with its own kernel built the way Chromium expects, sidesteps the page-size problem entirely and never touches the host kernel, its network stack, or the DNS resolver running on that same box. The guest costs about 2 GB of memory while it's running and a few tens of seconds to boot cold, so it stays on-demand rather than always-on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Placement: the always-on box, not the one running a model
&lt;/h2&gt;

&lt;p&gt;This automation runs on the Raspberry Pi, not the GPU box. The GPU box exists to hold a model in memory, and a browser process, headless or not, competes for the same pool that model needs. The Pi already carries DNS and everything else that has to be up all the time, and a browser job launched on demand under its own memory cap costs that box far less than fighting a model for the same 8 GB would cost the other one. Same reasoning that put DNS on the low-power box in the first place: a job goes where it costs the least to be wrong about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the VM guarantees, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;A VM boundary is real protection against one specific thing: if the browser inside it gets exploited at the kernel level, the compromise is stuck inside that guest. It is not, by itself, protection against the compromised guest reaching other things on the network. The guest's outbound traffic gets network-address-translated through the host, and network address translation is a routing detail, not a security boundary, so a compromised browser inside the VM can still open a connection to anything else on the LAN unless something stops it.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/defs&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="220" height="120" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="220" height="120" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="140" height="120" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g stroke="" stroke-width="2"&amp;gt;
  &amp;lt;line x1="260" y1="90" x2="336" y2="90"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="490" y1="130" x2="490" y2="180"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="460" y1="180" x2="520" y2="180"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;line x1="560" y1="90" x2="636" y2="90"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;








&amp;lt;/g&amp;gt;
&amp;lt;g fill=""&amp;gt;


&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;The VM stops the exploit from leaving the guest's kernel. A separate firewall rule, enforced on the host and keyed to the guest's own uid, is what stops the resulting network traffic from leaving the guest at all.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The fix is a firewall rule on the host itself, not inside the guest, keyed to the dedicated user account the guest's process runs as. It denies that uid's traffic to the local network ranges and allows everything else, loaded at boot and reasserted every time a guest starts, so it holds regardless of what happens inside the VM. Enforcing it on the host rather than trusting the guest to police itself is the whole point: a compromised guest has no way to touch a rule it was never allowed to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two identities, not one guest with two exits
&lt;/h2&gt;

&lt;p&gt;Some jobs need to browse anonymously; some need a real logged-in session. The obvious design puts both behind one guest with a switchable exit, direct or through a VPN tunnel. That falls apart at the browser profile: a single cookie jar that sometimes exits from the home connection and sometimes from a rotating VPN address looks exactly like the pattern session-security heuristics are built to catch, and a logged-in profile behind a hopping exit collects CAPTCHAs and forced re-logins until it locks out entirely.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/g&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="340" height="180" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="260" height="50" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="340" height="180" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="280" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;






&amp;lt;/g&amp;gt;
&amp;lt;g&amp;gt;


&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;One profile alternating between a home IP and a rotating VPN exit reads as account-takeover behavior to the sites it visits. Two profiles, each with a fixed identity, never trigger that pattern.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The fix is two separate guests, one per exit mode, each with its own user account, its own disk profile, and its own machine identity, chosen at the start of a job rather than switched mid-run. The VPN guest's profile starts empty and stays empty on purpose: anything that needs a real login has to run in the direct guest, which is the design working as intended, not a gap in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kill switch blocked its own tunnel
&lt;/h2&gt;

&lt;p&gt;Building the VPN path surfaced a subtler lesson worth keeping. The firewall rule meant to guarantee that guest's traffic never leaves except through the tunnel was written as: drop everything for that uid except traffic on the tunnel's own interface. It looked correct, and it passed every test against a tunnel that had never successfully connected.&lt;/p&gt;

&lt;p&gt;The moment a real tunnel came up, all of that guest's traffic stopped dead, tunnel included. WireGuard's encrypted outer packets inherit the user ID of the socket that generated the data they're carrying, so they left the host still tagged as the browser guest's own uid, on the physical network interface rather than the tunnel interface, and the same rule that was supposed to protect the tunnel killed it outright.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/defs&amp;gt;
&amp;lt;g fill="none" stroke="currentColor" stroke-width="1.4"&amp;gt;
  &amp;lt;rect width="170" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="180" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="180" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="170" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="180" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="180" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
  &amp;lt;rect width="150" height="45" rx="2"&amp;gt;&amp;lt;/rect&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g stroke="currentColor" stroke-width="1.2"&amp;gt;
  &amp;lt;line x1="190" y1="52" x2="226" y2="52"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="410" y1="52" x2="446" y2="52"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="630" y1="52" x2="666" y2="52"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="190" y1="142" x2="226" y2="142"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="410" y1="142" x2="446" y2="142"&amp;gt;&amp;lt;/line&amp;gt;
  &amp;lt;line x1="630" y1="142" x2="666" y2="142"&amp;gt;&amp;lt;/line&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;g fill="currentColor"&amp;gt;














&amp;lt;/g&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;&lt;br&gt;The fix didn't change what the browser guest can do. It gave the tunnel's own transport socket a mark the guest itself has no privilege to set, so the fix can't be forged from inside the guest it protects.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The fix tags the tunnel's own outbound packets with a firewall mark that only a process with elevated network privileges can set, something the confined guest doesn't have, and lets that mark bypass the block.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosting</category>
      <category>security</category>
      <category>virtualization</category>
    </item>
    <item>
      <title>Running a 26B MoE on an 8 GB Jetson by streaming experts from SSD</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:00:35 +0000</pubDate>
      <link>https://dev.to/anyesh/running-a-26b-moe-on-an-8-gb-jetson-by-streaming-experts-from-ssd-2jpf</link>
      <guid>https://dev.to/anyesh/running-a-26b-moe-on-an-8-gb-jetson-by-streaming-experts-from-ssd-2jpf</guid>
      <description>&lt;p&gt;My Jetson Orin Nano has 8 GB of unified memory. Gemma 4 26B-A4B is a 13.3 GiB download at Q4. This post is about the llama.cpp patch that lets the first thing run the second thing anyway, producing logits that are bit-for-bit identical to the standard code path, and why mixture-of-experts models make that possible when dense models never could.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fci4pqpye489dqh8qgfr1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fci4pqpye489dqh8qgfr1.gif" alt="Terminal recording of gemma4-26b explaining MoE routing on the Jetson Orin Nano: raw tegrastats GPU/RAM/power at top, a derived elapsed/token/tok-s/gpu/power line under it, and the decode scrolling below, ending on llama.cpp's timings block at 2.09 tokens per second" width="760" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea is borrowed, and worth borrowing
&lt;/h2&gt;

&lt;p&gt;Credit where it belongs: the architecture comes from &lt;a href="https://github.com/drumih/turbo-fieldfare" rel="noopener noreferrer"&gt;TurboFieldfare&lt;/a&gt;, a Swift and Metal runtime that runs Gemma 4 26B-A4B in about 2 GB of RAM on Apple Silicon. Its trick is to notice that an MoE model only touches a few experts per token. The dense weights (attention, router, shared expert) stay resident, and the routed experts, which are most of the model, live on the SSD. A small per-layer cache of expert slots is filled by parallel reads the moment the router announces which experts the next token needs, and the reads hide behind compute the GPU was doing anyway. The related academic thread is Eliseev and Mazur's expert-offloading work (&lt;a href="https://arxiv.org/abs/2312.17238" rel="noopener noreferrer"&gt;arXiv:2312.17238&lt;/a&gt;), which established that MoE routing is stable enough between tokens for caching to pay off.&lt;/p&gt;

&lt;p&gt;TurboFieldfare proves the idea beautifully, but it is a bespoke runtime: two supported models, Apple platforms only, custom kernels for everything. I wanted the same idea for the other cheap 8 GB machine on my desk, a Jetson Orin Nano, and I wanted it for any MoE model I could quantize. So instead of porting the runtime, I grafted the idea into &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;, which already runs on the Jetson and already has Ampere-tuned CUDA kernels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming without touching a single GPU kernel
&lt;/h2&gt;

&lt;p&gt;The part I like most about the design is that no CUDA kernel changes were needed. llama.cpp evaluates the routed-expert matmul with &lt;code&gt;ggml_mul_mat_id&lt;/code&gt;, which takes the expert weight tensor plus a small tensor of selected expert ids and addresses each expert as &lt;code&gt;base + id * stride&lt;/code&gt; on the device. The kernel does not care whether &lt;code&gt;base&lt;/code&gt; points at 128 experts or at 32; it just multiplies ids by strides.&lt;/p&gt;

&lt;p&gt;So the patch allocates, per MoE layer, a persistent pool tensor holding N expert-sized slots plus a tiny slot-id tensor, and at load time it records where every expert's bytes sit in the GGUF file. During decode, a scheduler callback fires on the router's top-k node, reads the chosen expert ids, and consults a per-layer LFU cache: experts already in slots are hits, misses are &lt;code&gt;pread&lt;/code&gt; directly from the file into their assigned slots by a small thread pool. The callback writes the slot indices into the slot-id tensor and lets the graph continue. The matmul reads the pool through slot ids and gets exactly the bytes it would have read from the full tensor.&lt;/p&gt;

&lt;p&gt;There is one place to be genuinely careful, and it is the reason this patch has a paranoid test. The same expert ids feed several consumers in the graph: the main matmul, the per-expert scale vectors that quantization-aware checkpoints ship, LoRA adapters, and expert biases. Only the main matmul may see remapped slot ids. Everything else must keep the real expert ids, because those tensors stay fully resident and are indexed by actual expert number. Get this wrong and nothing crashes; the model just gets quietly stupider. The test that guards against it greedy-decodes the same prompt with streaming off and on and compares every logit of every step with &lt;code&gt;memcmp&lt;/code&gt;. Same kernels, same bytes, different addressing, therefore bitwise identity is the correct bar, not "close enough".&lt;/p&gt;

&lt;p&gt;On the Jetson there is a bonus that TurboFieldfare's mmap trick foreshadowed: the Orin's GPU shares memory with the CPU, so the slot pools live in pinned host memory that the disk reads land in directly and the GPU addresses without any copy.&lt;/p&gt;

&lt;p&gt;For the IO-hiding part, Gemma 4's graph has a shared dense FFN that runs for every token alongside the routed experts. The patch reorders the layer so the router's top-k comes first, dispatches the disk reads, lets the GPU compute the shared FFN while the NVMe works, and joins right before the expert matmul needs the data. One synchronization per layer, exactly TurboFieldfare's overlap, expressed as a graph reordering plus a two-phase callback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers from the actual machine
&lt;/h2&gt;

&lt;p&gt;Measured on a Jetson Orin Nano Super (8 GB, MAXN, NVMe) with Gemma 4 26B-A4B QAT Q4, using &lt;code&gt;llama-bench&lt;/code&gt; tg128. The baseline is the best you could previously do in llama.cpp: dense weights on the GPU, experts mmap'd on the CPU with &lt;code&gt;--cpu-moe&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Decode tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--cpu-moe&lt;/code&gt; mmap baseline&lt;/td&gt;
&lt;td&gt;1.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming, 32 slots x 8 IO threads, overlap on&lt;/td&gt;
&lt;td&gt;2.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same, overlap off&lt;/td&gt;
&lt;td&gt;2.23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few observations that the table compresses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The overlap alone is worth 32 percent, which is the TurboFieldfare thesis confirmed on entirely different hardware: expert IO really does hide behind compute you were already doing.&lt;/li&gt;
&lt;li&gt;The NVMe's random-read ceiling (about 1.27 GB/s at this block size, per fio) puts the IO-bound limit near 5 tok/s, so there is headroom left in the IO path, not a wall.&lt;/li&gt;
&lt;li&gt;Slot count is a real tuning knob with a cliff. 32 slots per layer (a 3 GB pinned pool) is the sweet spot on 8 GB; 64 slots allocates 6 GB and starves the rest of the system.&lt;/li&gt;
&lt;li&gt;Peak RSS stays at 6.7 GB, and the correctness bar held on device: 32 greedy steps on the real 26B, every logit bitwise identical, with 10.5 GiB streamed through the pools during the comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest tradeoff: cold prefill drops to about a quarter of the baseline's speed, because streaming mode deliberately stops pre-faulting the expert file (pre-faulting 13 GiB into 8 GB of RAM is its own disaster). Prefill-through-the-pool is the obvious next piece of work, along with O_DIRECT reads.&lt;/p&gt;

&lt;p&gt;The GIF at the top is one request against the production &lt;code&gt;gemma4-26b&lt;/code&gt; entry in llama-swap. I sent a warm-up call first to get past cold-load and cold-prefill, then recorded a streamed completion with &lt;code&gt;tegrastats&lt;/code&gt; logging GPU load and power draw as each token arrived, laid out as three regions in the same terminal so the numbers and the text are visible at once. It settled at 2.09 tok/s where the bench measured 2.95, which is about the gap I'd expect between a clean &lt;code&gt;llama-bench&lt;/code&gt; run and one live request with a different prompt. The GPU trace is the interesting part: utilization swings between 0 and 95 percent because for stretches of every layer the GPU waits while the NVMe pulls in the next expert. That is the overlap claim from earlier, on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generalizing bought
&lt;/h2&gt;

&lt;p&gt;Because the hook lives in llama.cpp's graph layer rather than in any model's code, it works for every GGUF MoE architecture. I verified bitwise identity on three families: Qwen3-MoE style graphs with per-expert scale tensors, Gemma 4's merged gate-up projection, and Qwen 3.5/3.6's hybrid DeltaNet layers. The same flag also gets a 22 GiB Qwen 3.6 35B-A3B onto the same 8 GB board.&lt;/p&gt;

&lt;p&gt;Is 3 tokens per second fast? No. It is roughly reading speed, fine for anything asynchronous and painful for chat. But the alternative on this hardware is not a slower 26B, it is no 26B at all. The compromise the patch offers is exactly the one I wanted: model size becomes a choice you pay for in speed instead of a hard out-of-memory wall.&lt;/p&gt;

&lt;p&gt;The work lives on the &lt;a href="https://github.com/Anyesh/llama.cpp/tree/upstream-pr/moe-stream" rel="noopener noreferrer"&gt;&lt;code&gt;moe-stream&lt;/code&gt; branch of my llama.cpp fork&lt;/a&gt;: a standalone expert streamer, a &lt;code&gt;--moe-stream&lt;/code&gt; flag, pool allocation at load, the dual-id graph integration, the Gemma 4 overlap, and the identity test harness, in seven commits against upstream master.&lt;/p&gt;

&lt;p&gt;Thanks again to TurboFieldfare for the architecture. Ideas this good deserve to escape their original hardware.&lt;/p&gt;

&lt;p&gt;The run behind that GIF is scrubbable. I put a &lt;a href="https://learn.anyesh.me/a/moe-stream-jetson-replay" rel="noopener noreferrer"&gt;replay of it on the Learning Lab&lt;/a&gt;, with GPU load and power draw lined up against each token.&lt;/p&gt;

</description>
      <category>moestream</category>
      <category>llamacpp</category>
      <category>mixtureofexperts</category>
      <category>jetsonorin</category>
    </item>
    <item>
      <title>My context selector beat grep. An agent with grep beat it.</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:00:23 +0000</pubDate>
      <link>https://dev.to/anyesh/my-context-selector-beat-grep-an-agent-with-grep-beat-it-5</link>
      <guid>https://dev.to/anyesh/my-context-selector-beat-grep-an-agent-with-grep-beat-it-5</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/Anyesh/cognitive-cache" rel="noopener noreferrer"&gt;cognitive-cache&lt;/a&gt; started from an observation I still think is correct: every coding tool decides what goes into the context window, and almost none of them treat that decision as an explicit selection problem. Cursor greps, RAG systems embed and cosine-search, and plenty of harnesses just stuff files until the budget runs out. The OS analogy is hard to unsee once you have it. The context window is RAM, eviction and retrieval are virtual memory, and token budget allocation is an allocator that nobody has written.&lt;/p&gt;

&lt;p&gt;So I wrote one. Six weighted signals score every file in a repo against a task description, and a two-phase greedy submodular selector picks the highest-value set that fits a token budget. Nine languages, no LLM calls, no API keys. It shipped to PyPI and I used it through MCP for a while.&lt;/p&gt;

&lt;p&gt;Then I built a benchmark harness, and the harness spent the next two months dismantling most of what I believed about the tool. This is that sequence, in order, because the order is the interesting part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygvja5qkdelq2ow1nws5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygvja5qkdelq2ow1nws5.gif" alt="Animated forest plot building row by row: cognitive-cache's recall@5 advantage over grep, with paired bootstrap 95% confidence intervals. At n=23 the interval straddles zero. At n=78 and n=91 it clears zero" width="799" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first honest measurement said it does not beat grep
&lt;/h2&gt;

&lt;p&gt;The original benchmark was bad in the way most self-benchmarks are bad. It coupled retrieval to LLM patch generation, reported rank-blind full-set recall, had no significance test, and I almost never ran it.&lt;/p&gt;

&lt;p&gt;The replacement is LLM-free and deterministic. For each issue it clones the repo at the pre-fix commit, produces a full file ranking per strategy, and scores that ranking against the files the gold patch actually modified, using recall@k and MRR. Every head-to-head comparison carries a paired bootstrap 95% confidence interval, so a difference that excludes zero is real and a difference that straddles zero is not a result.&lt;/p&gt;

&lt;p&gt;On the first dataset, 23 hand-curated issues across 8 repos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;strategy&lt;/th&gt;
&lt;th&gt;R@1&lt;/th&gt;
&lt;th&gt;R@5&lt;/th&gt;
&lt;th&gt;R@10&lt;/th&gt;
&lt;th&gt;MRR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cognitive-cache&lt;/td&gt;
&lt;td&gt;0.170&lt;/td&gt;
&lt;td&gt;0.428&lt;/td&gt;
&lt;td&gt;0.511&lt;/td&gt;
&lt;td&gt;0.540&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grep (keyword count)&lt;/td&gt;
&lt;td&gt;0.170&lt;/td&gt;
&lt;td&gt;0.409&lt;/td&gt;
&lt;td&gt;0.559&lt;/td&gt;
&lt;td&gt;0.533&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lexical (TF-IDF)&lt;/td&gt;
&lt;td&gt;0.159&lt;/td&gt;
&lt;td&gt;0.362&lt;/td&gt;
&lt;td&gt;0.496&lt;/td&gt;
&lt;td&gt;0.487&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The recall@5 difference over grep was +0.020 with a 95% CI of [-0.067, +0.109]. Grep won at &lt;a href="mailto:recall@10"&gt;recall@10&lt;/a&gt;. Whatever the tool was doing, at that sample size it was indistinguishable from counting keywords.&lt;/p&gt;

&lt;p&gt;Two other things fell out of the same run. The per-signal ablation suggested &lt;code&gt;graph_distance&lt;/code&gt;, carrying 0.20 of the weight budget, was net negative, and &lt;code&gt;file_role_prior&lt;/code&gt; was doing nothing at 0.07. And the budget selector was leaking badly: ranking recall@5 was 0.428 while end-to-end recall under a 12k budget was 0.282, so the selector was discarding relevant files its own ranking had already surfaced.&lt;/p&gt;

&lt;p&gt;I nearly acted on the ablation immediately. A weight sweep stopped me, because every candidate reweighting had a bootstrap lower bound of +0.000. The "graph_distance is net negative" reading was itself noise at n=23. The real blocker was not the weights, it was that 23 narrow issues cannot detect or drive an improvement in either direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Widening the dataset flipped the result
&lt;/h2&gt;

&lt;p&gt;I moved to &lt;a href="https://www.swebench.com/" rel="noopener noreferrer"&gt;SWE-bench&lt;/a&gt;, which solves the curation problem by construction: real GitHub issues paired with the gold patch that fixed them, pulled over the HuggingFace datasets-server with no auth. Ground truth is the set of source files the patch modified, and because patches only touch files present at the base commit, every ground-truth file is retrievable.&lt;/p&gt;

&lt;p&gt;On a balanced 78-instance subset of SWE-bench Lite across 12 repos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;strategy&lt;/th&gt;
&lt;th&gt;R@1&lt;/th&gt;
&lt;th&gt;R@5&lt;/th&gt;
&lt;th&gt;R@10&lt;/th&gt;
&lt;th&gt;MRR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cognitive-cache&lt;/td&gt;
&lt;td&gt;0.231&lt;/td&gt;
&lt;td&gt;0.538&lt;/td&gt;
&lt;td&gt;0.654&lt;/td&gt;
&lt;td&gt;0.375&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grep&lt;/td&gt;
&lt;td&gt;0.103&lt;/td&gt;
&lt;td&gt;0.359&lt;/td&gt;
&lt;td&gt;0.487&lt;/td&gt;
&lt;td&gt;0.236&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lexical (TF-IDF)&lt;/td&gt;
&lt;td&gt;0.141&lt;/td&gt;
&lt;td&gt;0.410&lt;/td&gt;
&lt;td&gt;0.462&lt;/td&gt;
&lt;td&gt;0.253&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Recall@5 over grep: +0.179, CI [+0.064, +0.295]. Over TF-IDF: +0.128, CI [+0.013, +0.244]. Both exclude zero. On a harder 91-issue subset restricted to fixes touching two or more files, the edge held at 0.365 vs 0.252, CI [+0.056, +0.174].&lt;/p&gt;

&lt;p&gt;The earlier negative was a small-sample artifact. That cuts both ways, and it is worth sitting with: if I had published the n=23 result I would have been wrong, and if I had published the n=78 result as the final word I would also have been wrong, for the reason in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baseline I had not run was an agent
&lt;/h2&gt;

&lt;p&gt;"Beats grep" means beats a naive keyword count. It says nothing about the thing my tool would actually be competing with, which is a coding agent running its own search loop.&lt;/p&gt;

&lt;p&gt;So I ran that. Five single-file SWE-bench issues, one each from flask, requests, xarray, pylint, and pytest. Fresh Claude subagents, cold in the repo at the base commit, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;glob&lt;/code&gt;, and &lt;code&gt;read&lt;/code&gt; only, cognitive-cache explicitly forbidden.&lt;/p&gt;

&lt;p&gt;The agent got 5 of 5, averaging about 3.4 tool calls and 19k tokens, and in each case it pinpointed the mechanism rather than just the file. cognitive-cache got 4 of 5, missing pylint #5859, which the agent found in two calls. Naive keyword grep got 3 of 5.&lt;/p&gt;

&lt;p&gt;The token argument inverted at the same time. The agent read a handful of targeted files. A cognitive-cache call returns file contents up to the full token budget, so on these tasks it front-loads more context than a surgical agent consumes. It was costing context, not saving it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flijfipkigkpkhi25v5r5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flijfipkigkpkhi25v5r5.png" alt="Three stat tiles: an agent with only grep and read localized the fix file in 5 of 5 issues at about 3.4 tool calls each, cognitive-cache in 4 of 5, naive keyword grep in 3 of 5" width="799" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is n=5 on hand-picked single-file issues, so it is directional rather than definitive. But it moved the honest positioning from "more accurate than the agent" to "fewer roundtrips on a cold start in unfamiliar code", which is a much smaller claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal that was worth zero, and why
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;graph_distance&lt;/code&gt; measures import-graph distance from the files the task names. It carried 0.20 weight from the day I wrote it, on the reasoning that a fix often lives one hop away from the file the issue mentions.&lt;/p&gt;

&lt;p&gt;Across both datasets it earned nothing. On the 91-issue multi-file set, zeroing it changed recall@5 by +0.000 and MRR by -0.006. That was the pre-registered kill criterion, so I was ready to retire it.&lt;/p&gt;

&lt;p&gt;Then I pushed back on my own experiment, which turned out to be the useful move. Multi-file is not the same as multi-hop, a regex-based import graph is not a reference graph, and testing a signal as one term in a drowned weighted sum is not a fair test of the underlying idea. All three critiques were fair, so instead of assuming, I wrote a diagnostic over the 91-issue set to check whether the ground truth was even reachable.&lt;/p&gt;

&lt;p&gt;The answer killed the revival rather than supporting it. 93% of issues have all their ground-truth files reachable from the entry points. But &lt;strong&gt;87% of fix files are themselves entry points, at graph distance 0&lt;/strong&gt;, with only about 10% sitting at one to three hops and none further out. The mechanism was never starvation, it was redundancy: real issue text names the files it touches, so &lt;code&gt;symbol_overlap&lt;/code&gt; has already ranked those files highly and import distance adds nothing on top.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0cn1xnbm8n4tlrl9y7es.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0cn1xnbm8n4tlrl9y7es.png" alt="A single bar split into three segments: 87% of ground-truth fix files sit at import-graph distance 0 from the files the issue names, 10% at one to three hops, 3% unreachable" width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is not a bug in the experiment. It is a fact about the task distribution, and it explains the agent result too. On issue-shaped work the target is usually named in the prompt, which is exactly the regime where an agentic grep loop is excellent and a static ranker has little room to add value. Claude Code shipping no code index looks less like an omission and more like the same finding, reached earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The delivery problem, measured
&lt;/h2&gt;

&lt;p&gt;One thread survived long enough to be worth reporting separately. I built a jedi-backed neighborhood tool (definition, callers, in-repo callees, with real scope-aware resolution rather than name matching) and tested it two ways.&lt;/p&gt;

&lt;p&gt;Against agents, on two hard cross-file issues where naive grep recall was 0, I ran a 2x2: agents with grep and read, versus agents offered the neighborhood tool as well. Every arm scored 0.5, finding the file tied to the named concept and missing the cross-file one. &lt;strong&gt;The agents that had the tool invoked it zero times out of two&lt;/strong&gt;, despite being told to. Checking the tool's output directly against the symbols those same agents had reasoned about, it surfaced both missed files. The data would have lifted recall from 0.5 to 1.0 if it had been used.&lt;/p&gt;

&lt;p&gt;At scale the value is real but modest: across all 91 multi-file issues, neighborhood-of-seeds alone scores 0.164 (worse than grep's 0.252 standalone), while grep-or-neighborhood combined reaches 0.330, recovering 20 of grep's 257 missed fix files. That is +7.8 absolute points, in line with RepoGraph's own reported 9-12%.&lt;/p&gt;

&lt;p&gt;Put the two halves together and the conclusion is about delivery, not capability. A passive tool that competes with the grep reflex for invocation loses the invocation. Value like that only lands if it is injected into results the agent is already reading, not offered as one more thing it could call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it ended
&lt;/h2&gt;

&lt;p&gt;I set &lt;code&gt;graph_distance&lt;/code&gt; to weight 0, rewrote the README around the scoreboard instead of the pitch, and stopped building. The remaining pitch would have required a forced-injection layer to capture a modest gain, in a niche that already has direct clones, running against the current of agents that search well on their own.&lt;/p&gt;

&lt;p&gt;What survives is the measurement rig: the SWE-bench importer that needs no token, the retrieval evaluator, the weight tuner, the multi-hop diagnostic, the neighbor-recovery harness, and the bootstrap CIs that make each of those trustworthy. In a category that ships on anecdotes, that is the part I would build again first.&lt;/p&gt;

&lt;p&gt;The generalizable lesson is not "context selection is a dead end". It is that the baseline you skip is the one that decides whether your work matters. I measured against grep for two months because grep was the baseline that let the tool win. The agent was two hours of work and it reframed the entire project.&lt;/p&gt;

</description>
      <category>contextengineering</category>
      <category>retrieval</category>
      <category>swebench</category>
      <category>benchmarking</category>
    </item>
    <item>
      <title>J-space in practice: using Anthropic's Jacobian lens to decide what an LLM can forget</title>
      <dc:creator>Anish Shrestha</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:07:06 +0000</pubDate>
      <link>https://dev.to/anyesh/j-space-in-practice-using-anthropics-jacobian-lens-to-decide-what-an-llm-can-forget-14h1</link>
      <guid>https://dev.to/anyesh/j-space-in-practice-using-anthropics-jacobian-lens-to-decide-what-an-llm-can-forget-14h1</guid>
      <description>&lt;p&gt;Anthropic published &lt;a href="https://transformer-circuits.pub/2026/workspace/index.html" rel="noopener noreferrer"&gt;Verbalizable Representations Form a Global Workspace in Language Models&lt;/a&gt; on July 6, and the vocabulary it introduced is suddenly everywhere: J-space, the Jacobian lens, a global workspace inside Claude. Most of the discussion so far is about interpretability and alignment auditing, which is fair, since that is what the paper is about. I had a narrower and more mercenary question: can the workspace tell an inference runtime which parts of the KV cache it is safe to throw away?&lt;/p&gt;

&lt;p&gt;Three days after the paper landed, the first pre-registered gate on that question passed. As of this week the signal has replicated on three models and ships inside &lt;a href="https://github.com/Anyesh/EVOKE" rel="noopener noreferrer"&gt;EVOKE&lt;/a&gt;, my KV cache memory manager built on a forked llama.cpp. This post covers what J-space is, why it makes a good KV cache eviction signal, the numbers across Qwen2.5-7B, Qwen3-8B, and Qwen3-4B, and the caveat that comes with them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frh1hfff9leh8ua2rp18x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frh1hfff9leh8ua2rp18x.gif" alt="Animation of the two-stage pipeline: the Jacobian lens sweeps a session's KV blocks assigning workspace scores at prefill, then under a 25% cache budget the workspace policy keeps the planted fact and answers the turn-14 probe while SnapKV evicts it and fails" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What J-space is, in one paragraph
&lt;/h2&gt;

&lt;p&gt;The Jacobian lens is the instrument and J-space is the phenomenon. The lens isolates directions in a model's residual stream that encode a token the model could verbalize next, and those directions form a low-dimensional workspace: roughly 10% of activation variance, concentrated in the middle layers, carrying whatever the model is "holding in mind" at each position. Anthropic's headline application is alignment auditing, reading reasoning the model never voices. What makes independent work possible is that they released &lt;a href="https://github.com/anthropics/jacobian-lens" rel="noopener noreferrer"&gt;companion code&lt;/a&gt; under Apache-2.0 along with fitted lens matrices for open Qwen models on &lt;a href="https://huggingface.co/neuronpedia/jacobian-lens" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt;, so anyone can apply the lens to an open-weights model on a single GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  The systems problem: KV cache eviction
&lt;/h2&gt;

&lt;p&gt;Every long-running LLM session eventually outgrows its KV cache budget. An agent session in a coding harness crosses tens of thousands of cached tokens within a few turns, and something has to decide which entries stay in GPU memory. The standard answers, H2O and SnapKV, rank cache blocks by accumulated attention history: keep what the model has been attending to, evict the rest. StreamingLLM adds protected sink tokens and a recency window.&lt;/p&gt;

&lt;p&gt;These policies share a structural weakness. They are backward-looking, so they can only rank blocks the model has already used. A fact planted at turn 1 that will matter at turn 14 looks cold the whole time, and it gets evicted exactly when the budget tightens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis: workspace content predicts KV reuse
&lt;/h2&gt;

&lt;p&gt;The lens readout suggested a forward-looking alternative. If a position carries workspace content, meaning the model is holding something in mind there, then its KV entries should be disproportionately likely to be read by later reasoning. That is a content signal, computable at prefill time from the residual stream alone, before any attention history exists.&lt;/p&gt;

&lt;p&gt;The offline validation lives in &lt;a href="https://github.com/Anyesh/j-space" rel="noopener noreferrer"&gt;j-space&lt;/a&gt;: lens application, a family of workspace statistics over the readout (kurtosis of block means, transport ratios, energy concentration, computed across layers), block aggregation, eviction simulation, and probe distillation. Because a signal this convenient deserved suspicion, I pre-registered three kill gates before running anything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fact ranking.&lt;/strong&gt; The best workspace statistic has to beat SnapKV at ranking the task-critical block, by a pre-set AUC margin, or the project dies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Masked eviction.&lt;/strong&gt; With only 25% of the cache retained under the workspace policy, the planted fact has to stay answerable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probe distillation.&lt;/strong&gt; Applying the full lens at inference time is too expensive, so a ridge probe distilled from the workspace readout has to reproduce the ranking at Spearman 0.8 or better. The deployable artifact is one dot product per position.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Results across three Qwen models
&lt;/h2&gt;

&lt;p&gt;All three gates passed on all three models, with no hand-tuning on the replications.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Fact-AUC (workspace)&lt;/th&gt;
&lt;th&gt;Fact-AUC (SnapKV)&lt;/th&gt;
&lt;th&gt;Eviction @ 25% budget&lt;/th&gt;
&lt;th&gt;Probe Spearman&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen2.5-7B-Instruct&lt;/td&gt;
&lt;td&gt;0.891&lt;/td&gt;
&lt;td&gt;0.622&lt;/td&gt;
&lt;td&gt;35/36 vs 25/36&lt;/td&gt;
&lt;td&gt;0.959&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-8B&lt;/td&gt;
&lt;td&gt;0.865&lt;/td&gt;
&lt;td&gt;0.563&lt;/td&gt;
&lt;td&gt;35/36 vs 14/36&lt;/td&gt;
&lt;td&gt;0.944&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-4B&lt;/td&gt;
&lt;td&gt;0.939&lt;/td&gt;
&lt;td&gt;0.541&lt;/td&gt;
&lt;td&gt;36/36 vs 13/36&lt;/td&gt;
&lt;td&gt;0.946&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdoeyae6nxq4pkk2aapdl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdoeyae6nxq4pkk2aapdl.png" alt="Grouped bar chart of fact-AUC on three Qwen models: the workspace signal scores 0.891, 0.865, and 0.939 while SnapKV scores 0.622, 0.563, and 0.541, barely above the 0.5 chance line" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The eviction column reads: episodes where the planted fact survived eviction and was answered correctly, workspace policy vs SnapKV at the same budget. On Qwen3-4B the workspace policy saturates at 36/36 at every budget tested.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvd8l6tg4fswy6q877sea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvd8l6tg4fswy6q877sea.png" alt="Grouped bar chart of eviction survival at a 25% cache budget: the workspace policy keeps the fact answerable in 35, 35, and 36 of 36 episodes while SnapKV manages 25, 14, and 13" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The distilled probe also holds up live. Inside EVOKE's agent bench, the workspace signal passes the fact probe at every cache budget (512, 1024, and 2048 tokens) with recovery disabled, while H2O fails all three budgets and SnapKV passes only at 2048. Measured decode overhead was -0.6%, which is noise; the probe is a dot product per position at prefill, and each decode step adds only a 28 KB host copy and one dot product per block.&lt;/p&gt;

&lt;h2&gt;
  
  
  What transfers and what does not
&lt;/h2&gt;

&lt;p&gt;Here is the caveat, and I think it is the most interesting finding in the project. The winning statistic is model-dependent. On Qwen2.5-7B and Qwen3-8B it is kurtosis of block-mean workspace scores at deep layers (layer 23 and layer 30 respectively). On Qwen3-4B it is a transport-ratio statistic at layer 12 of 36. If I had frozen the 7B statistic and shipped it as "the" signal, the 4B replication would have failed. What transfers is the statistic family plus the automated procedure that selects the winner per model on held-out episodes, not any single magic number.&lt;/p&gt;

&lt;p&gt;The other scope limits are stated plainly in the paper: three models so far, all Qwen; the probes are fit on the benchmark's own episode family; and the task family is long-horizon fact recall. LongBench-style suites and non-Qwen architectures are open work. As far as I can tell this is the first interpretability-derived KV eviction signal, and I would much rather over-state the limits than the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it ships: EVOKE v2.0
&lt;/h2&gt;

&lt;p&gt;EVOKE treats the KV cache the way an operating system treats memory. Blocks are evicted under budget pressure and recovered recompute-free through save/restore primitives added to a forked llama.cpp, 20 to 32 times faster than re-prefilling the same tokens. &lt;a href="https://github.com/Anyesh/EVOKE/releases/tag/v2.0" rel="noopener noreferrer"&gt;Version 2.0&lt;/a&gt; adds the workspace probe as a first-class eviction signal alongside live attention capture, harness priority tags, task-focus coherence, and recency, and includes the paper PDF with the full three-model evaluation.&lt;/p&gt;

&lt;p&gt;You can watch the signal work in the &lt;a href="https://huggingface.co/spaces/anish-shrestha/evoke-demo" rel="noopener noreferrer"&gt;live demo on Hugging Face Spaces&lt;/a&gt;, which runs three arms side by side on Qwen3-4B: a plain baseline, EVOKE with its attention scorer, and EVOKE with workspace eviction. There is also a benchmark walkthrough on my &lt;a href="https://anyesh.github.io" rel="noopener noreferrer"&gt;learning lab&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://transformer-circuits.pub/2026/workspace/index.html" rel="noopener noreferrer"&gt;The Anthropic paper&lt;/a&gt;, the primary source on J-space and the Jacobian lens&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/jacobian-lens" rel="noopener noreferrer"&gt;anthropics/jacobian-lens&lt;/a&gt;, the official lens code, with fitted matrices at &lt;a href="https://huggingface.co/neuronpedia/jacobian-lens" rel="noopener noreferrer"&gt;neuronpedia/jacobian-lens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Anyesh/j-space" rel="noopener noreferrer"&gt;j-space&lt;/a&gt;, the offline validation and probe distillation pipeline behind this post&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Anyesh/EVOKE" rel="noopener noreferrer"&gt;EVOKE&lt;/a&gt;, the runtime that consumes the signal, plus the &lt;a href="https://huggingface.co/spaces/anish-shrestha/evoke-demo" rel="noopener noreferrer"&gt;demo Space&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you work on KV cache compression, long-context inference, or interpretability and want to poke holes in any of this, the repos are public and the gates were registered before the runs. I would genuinely like to know where it breaks.&lt;/p&gt;

</description>
      <category>jspace</category>
      <category>jacobianlens</category>
      <category>interpretability</category>
      <category>kvcache</category>
    </item>
  </channel>
</rss>
