<?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: Andréas Bodin</title>
    <description>The latest articles on DEV Community by Andréas Bodin (@arti0).</description>
    <link>https://dev.to/arti0</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%2F4078140%2F5b1a34fe-0b4c-4ff3-9640-6165370e94b3.jpeg</url>
      <title>DEV Community: Andréas Bodin</title>
      <link>https://dev.to/arti0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arti0"/>
    <language>en</language>
    <item>
      <title>Measuring the real concurrency ceiling of an LLM agent runner</title>
      <dc:creator>Andréas Bodin</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:24:21 +0000</pubDate>
      <link>https://dev.to/arti0/measuring-the-real-concurrency-ceiling-of-an-llm-agent-runner-53cb</link>
      <guid>https://dev.to/arti0/measuring-the-real-concurrency-ceiling-of-an-llm-agent-runner-53cb</guid>
      <description>&lt;p&gt;I wanted to raise the concurrency limits on my local AI agent runner. The UI now supports multiple terminal panes running in flight, and my gut told me the runner process itself was becoming the bottleneck. &lt;/p&gt;

&lt;p&gt;Before touching a single config setting, I wrote a benchmark to test that assumption: &lt;strong&gt;When N sessions run at once, what actually breaks — the model server, the hardware, or the scheduling policy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It turns out my gut was entirely wrong.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Benchmark Setup
&lt;/h3&gt;

&lt;p&gt;I wrote a bench script that fires N concurrent chat sessions against the runner. Each turn hits a local model (&lt;code&gt;hermes3&lt;/code&gt; via Ollama) with a fixed prompt, running through the full pipeline: pre-turn intent classification, chat response, and post-reply memory extraction.&lt;/p&gt;

&lt;p&gt;I tracked four specific metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ttfa (Time to First Activity):&lt;/strong&gt; POST request -&amp;gt; first internal event emitted. Measures the runner's event loop &lt;em&gt;before&lt;/em&gt; touching Ollama.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;first-Ollama:&lt;/strong&gt; POST request -&amp;gt; first request hitting the model queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;done:&lt;/strong&gt; POST request -&amp;gt; user receives the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;wall (drained):&lt;/strong&gt; Total time until the post-reply memory extraction tail completely finishes background work in Ollama.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The Results: A Hard Wall at N=1
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;Mean ttfa&lt;/th&gt;
&lt;th&gt;Mean first-Ollama&lt;/th&gt;
&lt;th&gt;Done (Min / Mean / Max)&lt;/th&gt;
&lt;th&gt;Wall (Done)&lt;/th&gt;
&lt;th&gt;Wall (Drained)&lt;/th&gt;
&lt;th&gt;CPU Max&lt;/th&gt;
&lt;th&gt;Min Free RAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.0s&lt;/td&gt;
&lt;td&gt;0.9s&lt;/td&gt;
&lt;td&gt;2.1s / 2.1s / 2.1s&lt;/td&gt;
&lt;td&gt;2.1s&lt;/td&gt;
&lt;td&gt;9.5s&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;td&gt;6,004 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.0s&lt;/td&gt;
&lt;td&gt;1.1s&lt;/td&gt;
&lt;td&gt;3.5s / 4.2s / 5.0s&lt;/td&gt;
&lt;td&gt;5.0s&lt;/td&gt;
&lt;td&gt;12.9s&lt;/td&gt;
&lt;td&gt;37%&lt;/td&gt;
&lt;td&gt;5,957 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.0s&lt;/td&gt;
&lt;td&gt;1.5s&lt;/td&gt;
&lt;td&gt;5.9s / 7.7s / 10.3s&lt;/td&gt;
&lt;td&gt;10.3s&lt;/td&gt;
&lt;td&gt;20.7s&lt;/td&gt;
&lt;td&gt;63%&lt;/td&gt;
&lt;td&gt;5,889 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At N=4, the wall-clock time scaled to &lt;strong&gt;4.80×&lt;/strong&gt; the single-session baseline. Because 4.8× exceeds 4×, concurrent requests actually performed &lt;em&gt;worse&lt;/em&gt; than a pure, perfectly ordered serial queue.&lt;/p&gt;

&lt;p&gt;The bottleneck breakdown was immediately clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's not the runner:&lt;/strong&gt; &lt;code&gt;ttfa&lt;/code&gt; stayed at 0.0s across all runs. The runner's event loop processed and routed incoming POST requests in milliseconds without queuing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not hardware:&lt;/strong&gt; CPU peaked at 63% and free RAM never dropped below ~5.9 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's Ollama:&lt;/strong&gt; Latency stacked inside Ollama's internal request queue for the single loaded model (&lt;code&gt;hermes3 8.0B Q4_0&lt;/code&gt;). &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Furthermore, &lt;code&gt;wall (drained)&lt;/code&gt; proved that background memory extraction keeps Ollama saturated long after the user gets their answer — a hidden tax naive RPS benchmarks completely miss.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Twist: Job Queues Fail by Policy, Not Load
&lt;/h3&gt;

&lt;p&gt;Chat is only half the system. The real heavy lifting happens in coding jobs, which talk to the Claude API instead of Ollama.&lt;/p&gt;

&lt;p&gt;When I checked why coding jobs weren't running concurrently, I didn't need a load test. I just needed to look at the scheduler code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_CONCURRENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAX_CONCURRENT_JOBS&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// In the scheduler pump:&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;repoHasRunningJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// One job per repo, unconditionally&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost every ticket in my queue targets the same primary repository. Regardless of what &lt;code&gt;MAX_CONCURRENT&lt;/code&gt; was set to, &lt;strong&gt;the system was hardcoded to run one job per repo at a time.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;The rule exists for a valid reason: two AI agents branching off the same moving &lt;code&gt;HEAD&lt;/code&gt; create messy merge conflicts. But serializing the entire repository by default was a blunt policy constraint, not a system capability limit.&lt;/p&gt;




&lt;h3&gt;
  
  
  Smart Parallelism: A Disjoint Path Rule
&lt;/h3&gt;

&lt;p&gt;To fix job concurrency without causing merge chaos, I replaced the blind "one job per repo" check with a strict path-overlap predicate.&lt;/p&gt;

&lt;p&gt;Two queued jobs (A and B) in the same repo can now run concurrently &lt;strong&gt;only if all three conditions are met&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Disjoint Named Paths:&lt;/strong&gt; Both tickets explicitly declare target files/directories, and their path sets share no common files, ancestors, or subdirectories. (If a ticket names no path, it is treated as touching everything and stays serial.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Dependency Edges:&lt;/strong&gt; Neither ticket references the other as a hard or soft dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical Touched Files:&lt;/strong&gt; Reopened tickets use their previous run's actual &lt;code&gt;git diff&lt;/code&gt; file list rather than their written prose description.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If there's any ambiguity, the scheduler defaults to serial execution. A false positive (running serially when safe) costs a few minutes of queue time; a false negative (running concurrently and corrupting state) costs an hour of untangling merge conflicts by hand.&lt;/p&gt;




&lt;h3&gt;
  
  
  What I Actually Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chat concurrency&lt;/strong&gt; binds on the local model server — raising limits requires better GPU hardware or multi-model inference instances, not TypeScript optimizations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coding job concurrency&lt;/strong&gt; binds on scheduling policies — fixable with smarter dependency tracking and file-path isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The runner infrastructure&lt;/strong&gt; was completely fine all along. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measure first. The limit you think you're hitting is rarely the one actually holding you back.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Andréas — full-stack dev, CTO at a B2B SaaS, building my own agent tooling. Portfolio: &lt;a href="https://andreas-bodin.vercel.app" rel="noopener noreferrer"&gt;https://andreas-bodin.vercel.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>performance</category>
      <category>devops</category>
    </item>
    <item>
      <title>Can your verify gate actually fail?</title>
      <dc:creator>Andréas Bodin</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:20:43 +0000</pubDate>
      <link>https://dev.to/arti0/can-your-verify-gate-actually-fail-3ib4</link>
      <guid>https://dev.to/arti0/can-your-verify-gate-actually-fail-3ib4</guid>
      <description>&lt;p&gt;I let Claude Code commit directly to my repositories. I don't review the diffs. I didn't think I needed to, because I built a deterministic verify gate — a script that lints, typechecks, builds, and runs tests. If the gate goes green, the PR merges automatically.&lt;/p&gt;

&lt;p&gt;I trusted that gate implicitly. Until I actually sat down and asked the one question that matters: &lt;strong&gt;If an agent pushes completely broken code right now, will this gate actually go red?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "is the script configured?" Not "does the file exist?" &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Will it actually fail?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Turns out, for three out of my four repos, the answer was an emphatic &lt;strong&gt;no&lt;/strong&gt;. The gate was returning &lt;code&gt;GREEN&lt;/code&gt; without running a single line of code.&lt;/p&gt;




&lt;h3&gt;
  
  
  The line of code that lied to me
&lt;/h3&gt;

&lt;p&gt;My gate script is repo-agnostic. It looks up the repository name in a policy JSON file (&lt;code&gt;gates.&amp;lt;repoName&amp;gt;&lt;/code&gt;) and falls back to a default if it doesn't find one. &lt;/p&gt;

&lt;p&gt;Here is the PowerShell logic driving the whole operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$steps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$policy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$RepoName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$steps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$steps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$policy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$steps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$steps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREEN (no gate steps configured for '&lt;/span&gt;&lt;span class="nv"&gt;$RepoName&lt;/span&gt;&lt;span class="s2"&gt;')"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="c"&gt;# &amp;lt;-- Unconditional green&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is my config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"gates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"npm run lint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"npm run test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;design-gate script&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that exit code. If a repo doesn't have an explicit entry in the JSON file, it falls through to &lt;code&gt;default: []&lt;/code&gt;. The script sees zero steps, prints &lt;code&gt;GREEN&lt;/code&gt;, and exits with &lt;code&gt;0&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The runner sees exit code &lt;code&gt;0&lt;/code&gt; and instantly merges the code. I built a system where &lt;strong&gt;having no tests configured is structurally identical to passing all tests.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Audit: 4 Repos, 1 Real Gate, 0 Peace of Mind
&lt;/h3&gt;

&lt;p&gt;When I audited all four repos the runner is allowed to touch, the reality was pretty grim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app&lt;/code&gt; (The Runner itself):&lt;/strong&gt; The &lt;em&gt;only&lt;/em&gt; gated repo, but it's full of holes. &lt;code&gt;npm run lint&lt;/code&gt; runs, but &lt;code&gt;tsconfig&lt;/code&gt; explicitly excludes the &lt;code&gt;runner/&lt;/code&gt; directory — meaning &lt;strong&gt;51 files of core executor code are never typechecked&lt;/strong&gt;. Next.js &lt;code&gt;build&lt;/code&gt; isn't in the gate at all, so client/server boundary breaks ship freely. Vitest runs 28 test files, but the glob pattern completely misses &lt;code&gt;*.test.tsx&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;product&lt;/code&gt; (Next.js app):&lt;/strong&gt; The most embarrassing row. It has full ESLint, &lt;code&gt;tsc --noEmit&lt;/code&gt;, &lt;code&gt;vitest&lt;/code&gt; (10 suites), and &lt;code&gt;next build&lt;/code&gt; ready to go in &lt;code&gt;package.json&lt;/code&gt;. None of them were wired into the gate config. It had 0% coverage purely because of lazy config debt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;notes&lt;/code&gt; (Markdown vault):&lt;/strong&gt; Unconfigured. It doesn't need npm, but it does need a basic script to check for broken wikilinks or bad frontmatter. Instead, it was just auto-approving everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;portfolio&lt;/code&gt; (Vite site):&lt;/strong&gt; Unconfigured &lt;em&gt;and&lt;/em&gt; the directory wasn't even a valid Git worktree. Jobs were dying before the gate even executed, but if they had reached it, they would have passed instantly.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  How to fix a fake gate
&lt;/h3&gt;

&lt;p&gt;The real bug here isn't missing npm scripts. The structural flaw is that &lt;strong&gt;an empty gate defaults to success&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To fix this properly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make empty gates loud.&lt;/strong&gt; If &lt;code&gt;$steps.Count -eq 0&lt;/code&gt;, the script must exit non-zero or return an explicit &lt;code&gt;UNGATED&lt;/code&gt; status. A missing key should break the build, not bypass it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire up the low-hanging fruit.&lt;/strong&gt; The &lt;code&gt;product&lt;/code&gt; repo was fixed with a single line in &lt;code&gt;policy.json&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add actual build checks.&lt;/strong&gt; Typechecking doesn't catch bundler or framework errors. Adding &lt;code&gt;npm run build&lt;/code&gt; to the gate costs execution time, but it's the only way to catch real deployment breakers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix silent exclusion traps.&lt;/strong&gt; Un-exclude critical core directories from &lt;code&gt;tsconfig&lt;/code&gt; and fix the Vitest globs before someone writes a &lt;code&gt;.tsx&lt;/code&gt; test that never actually gets executed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you run AI agents against your codebase without reading the diffs, go break a file on purpose and run your gate. If it doesn't yell at you, you don't have a safety gate — you just have a script that automatically approves bad code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Andréas — full-stack dev, CTO at a B2B SaaS, building my own agent tooling. Portfolio: &lt;a href="https://andreas-bodin.vercel.app" rel="noopener noreferrer"&gt;https://andreas-bodin.vercel.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ci</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Where the time actually goes in an AI coding-agent job</title>
      <dc:creator>Andréas Bodin</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:16:18 +0000</pubDate>
      <link>https://dev.to/arti0/where-the-time-actually-goes-in-an-ai-coding-agent-job-13ei</link>
      <guid>https://dev.to/arti0/where-the-time-actually-goes-in-an-ai-coding-agent-job-13ei</guid>
      <description>&lt;p&gt;I run an agent-orchestration platform: it takes a ticket, spins up a git worktree, lets a Claude Code agent build the feature, and runs a deterministic verify gate before merging. A typical job takes 20–30 minutes. &lt;/p&gt;

&lt;p&gt;I used to blame slow model responses for the runtime, but stage-by-stage measurements proved that assumption wrong: &lt;strong&gt;infrastructure overhead costs 5–8 minutes of every job before the agent even finishes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is where the time actually goes and how to optimize it.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Baseline Overhead
&lt;/h3&gt;

&lt;p&gt;On a standard dev machine (Windows 11, Bun, Next.js), a single job pays a heavy infrastructure tax:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Measured Cost&lt;/th&gt;
&lt;th&gt;Impact / Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;git worktree add&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;td&gt;Negligible overhead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;bun install&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~91s (median)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cold worktree setup, even with a warm cache.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lint &amp;amp; Typecheck&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1–2 minutes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eslint&lt;/code&gt; + 2× &lt;code&gt;tsc&lt;/code&gt; across the full repo with no shared cache.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;next build&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Minutes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The longest gate leg due to a cold &lt;code&gt;.next&lt;/code&gt; directory every run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vitest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~26 seconds&lt;/td&gt;
&lt;td&gt;Fast and acceptable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~15–22 minutes&lt;/td&gt;
&lt;td&gt;Planning, execution, and review calls.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fixed infrastructure costs account for &lt;strong&gt;20% to 30% of total runtime&lt;/strong&gt;. Eliminating this overhead reduces small job times from ~30 minutes to ~20 minutes, leaving the remaining time strictly bounded by model inference.&lt;/p&gt;




&lt;h3&gt;
  
  
  Ranked Infrastructure Optimizations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. A Pre-Warmed Executor Slot Pool (The Highest-Value Win)
&lt;/h4&gt;

&lt;p&gt;Instead of creating a fresh worktree and running &lt;code&gt;bun install&lt;/code&gt; from scratch for every job, maintain a small pool of pre-warmed directories containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A long-lived Git worktree (re-pointed per job).&lt;/li&gt;
&lt;li&gt;A pre-installed &lt;code&gt;node_modules&lt;/code&gt; directory (invalidated only when the lockfile hash changes).&lt;/li&gt;
&lt;li&gt;Persistent build caches (&lt;code&gt;.next&lt;/code&gt;, &lt;code&gt;tsconfig.tsbuildinfo&lt;/code&gt;, &lt;code&gt;.eslintcache&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a job starts, it claims a slot, checks out the target branch, and immediately executes. After completion, &lt;code&gt;git clean&lt;/code&gt; resets the worktree while leaving cache directories intact. This eliminates the ~91-second install step on almost every job.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Persistent Next.js Build Caching
&lt;/h4&gt;

&lt;p&gt;Next.js supports persistent filesystem build caching. Combining this with a warm slot pool carries the &lt;code&gt;.next&lt;/code&gt; cache between jobs, turning the longest gate step from minutes into tens of seconds for standard diffs.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Incremental Linting and Typechecking
&lt;/h4&gt;

&lt;p&gt;Adding &lt;code&gt;--incremental&lt;/code&gt; to &lt;code&gt;tsc&lt;/code&gt; and &lt;code&gt;--cache&lt;/code&gt; to &lt;code&gt;eslint&lt;/code&gt; within the persistent slot directory saves roughly 1 minute per job without changing verification outcomes.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Windows Filesystem Optimizations
&lt;/h4&gt;

&lt;p&gt;Real-time Defender scanning severely throttles small-file I/O operations (like &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;.next&lt;/code&gt; writes). Adding directory exclusions or moving worktrees to a &lt;strong&gt;ReFS Dev Drive&lt;/strong&gt; improves install and build I/O speeds by 2× to 5× with zero code changes.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Isolated Linkers (Proceed with Caution)
&lt;/h4&gt;

&lt;p&gt;Switching to &lt;code&gt;bun install --linker isolated&lt;/code&gt; provides pnpm-style symlinked stores, yielding faster warm installs. However, caution is required: symlinks spanning worktrees can cause catastrophic unintended deletions if cleanup commands traverse outside the worktree root.&lt;/p&gt;




&lt;h3&gt;
  
  
  Implementation Strategy
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Environment Tweaks:&lt;/strong&gt; Exclude worktrees from real-time antivirus scanning (immediate win).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slot Pool Architecture:&lt;/strong&gt; Build the persistent executor slot pool to retain installed dependencies and cache directories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental Caching:&lt;/strong&gt; Enable &lt;code&gt;tsc --incremental&lt;/code&gt; and &lt;code&gt;eslint --cache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework Upgrades:&lt;/strong&gt; Enable persistent build caching in Next.js once the slot pool is stable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By optimizing the infrastructure pipeline, fixed job overhead drops from &lt;strong&gt;5–8 minutes to under 60 seconds&lt;/strong&gt;. The remaining runtime represents actual model reasoning—the exact phase worth waiting for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Andréas — full-stack dev, CTO at a B2B SaaS, building my own agent tooling. Portfolio: &lt;a href="https://andreas-bodin.vercel.app" rel="noopener noreferrer"&gt;https://andreas-bodin.vercel.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
