<?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 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%2Cformat=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%2Cformat=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 nothing runs during decode.&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>
