<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.nikosbaxevanis.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.nikosbaxevanis.com/" rel="alternate" type="text/html" /><updated>2026-10-24T20:04:19+00:00</updated><id>https://blog.nikosbaxevanis.com/feed.xml</id><title type="html">blog</title><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><entry><title type="html">Restoring the Models on a Clean Mac</title><link href="https://blog.nikosbaxevanis.com/2026/10/23/the-restore-is-the-test/" rel="alternate" type="text/html" title="Restoring the Models on a Clean Mac" /><published>2026-10-23T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/23/the-restore-is-the-test</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/23/the-restore-is-the-test/"><![CDATA[<p><em>Part 8 of <a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a>.</em></p>

<p>By this point I had separate model configurations for the two Macs,
measured context limits, and several runtime builds to keep track of.
The <a href="/2026/10/22/local-models-and-tool-permissions/">previous post</a> also covered the access settings I wanted
to manage separately. I needed a way to preserve that setup without
reconstructing it from terminal history.</p>

<p>This matters for the agent experiments that started the series. If I
change a model or its settings and get different results, I want to be
able to return to the earlier configuration. I also want to be able to
reset a Mac without losing the work needed to make those models run.</p>

<p>I put together an offline recovery library on an external SSD, including
the model files, pinned source, Python wheels, native runtimes, and Apple
toolchain material. Then I reset the M5 and restored from it. I handled
the administrator and license prompts myself. The installer finished with:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RESTORED ALL EIGHT PROFILES. No persistent servers started.
</code></pre></div></div>

<p>I then started each model, checked its status, requested a generation, and
stopped it before moving to the next one. All eight passed. That was the
first full test of this M5 library on a factory-reset machine.</p>

<h2 id="what-the-clean-restore-tested">What the clean restore tested</h2>

<p>Before the reset, the qualification records said that offline component
restores had passed over independently provisioned files. They explicitly
did not claim a factory-reset restore.</p>

<p>The reset removed that uncertainty about the existing installation. It
was still a narrower inference test than the earlier qualification: I
ran generation smoke tests for all eight models, without repeating the
long-input or separate image tests. The <a href="/assets/local-inference/qualification.json">evidence summary</a> records
which checks ran before and after the reset.</p>

<p>The restore also exercised a dependency that would have been easy to
miss on a working Mac. Xcode setup initially couldn’t find the Metal
compiler. The installer then imported the archived Metal Toolchain
17F109, checked the compiler, and continued. Keeping the model files and
source alone would have left that part of the setup unfinished.</p>

<h2 id="one-repository-machine-specific-kits">One repository, machine-specific kits</h2>

<p>The storage layout settled into this shape:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LLM/
  .git/
  restore-all.sh
  machines/
    m3-ultra-256gb/
      DwarfStar/
      Orca/
    m5-max-128gb/
      DwarfStar/
      Orca/
      optional-models/
    m5-ultra-256gb/       # Reserved; not qualified.
</code></pre></div></div>

<p>Git lives at <code class="language-plaintext highlighter-rouge">LLM/</code>, above the machine directories. It tracks the small
material that explains the installation: scripts, configuration, manifests,
and reviewed records. The large payloads remain outside Git history.</p>

<p>The model files need their own backup and checksums: recovering the Git
repository alone wouldn’t recover those ignored files. Keeping them
outside Git lets me review changes to the scripts and configuration
without putting hundreds of gigabytes of weights into each kit’s history.</p>

<p>The installed locations under <code class="language-plaintext highlighter-rouge">~/Downloads</code> stayed separate from the SSD
layout. The managers run from internal storage, including the streaming
model’s installed files. After a successful restore, the recovery SSD can
be ejected without taking the service away.</p>

<p>Each kit retained its own required assets. I avoided cross-kit links that
would make one machine’s recovery depend on another machine’s directory.
The M5 work also preserved the M3 engine pins and existing manager
interface. A cosmetic cleanup was not a sufficient reason to disturb a
working installation.</p>

<h2 id="eight-accepted-models">Eight accepted models</h2>

<p>The M3 had nine profiles. The M5 finished with eight.</p>

<p>An Orca DeepSeek conversion was still incomplete. Cancelling it meant
stopping its supervisor as well as the conversion process, after checking
their identities. Otherwise the supervisor could start a later step after
the visible process disappeared.</p>

<p>I retained the verified original MXFP4 shards as optional future-work
assets. The incomplete output and redundant internal download copies were
removed only after the SSD originals had been verified.</p>

<p>I also removed that profile from the installer, manifests, manager help,
and model tables, and renamed the M5 Orca kit to reflect its four working
models. The retained shards are explicitly optional. Restoring the eight
accepted models doesn’t depend on finishing that conversion.</p>

<h2 id="copying-the-library">Copying the library</h2>

<p>After the M5 restore test, I copied its library to the other SSD and used
an M2 Mac to inspect both drives. The M2 was a verification host, not a
newly qualified inference target.</p>

<p>Both copies had the same Git revision and contained 4,082 filesystem
entries and 3,593 regular files. Names, sizes, permissions, and symlink
targets matched. The copied Git database passed its integrity check.</p>

<p>I initially started a full content hash. That reads every byte on both
drives: about 4.9 TB for these two logical copies. The first implementation
was CPU-bound. Switching the large-file hashing to the built-in OpenSSL
implementation raised observed disk reads to about 1.5 GB/s per drive.</p>

<p>I then chose the shorter verification scope. It hashed 3,400 small files
on each drive, checked all 617 tracked files against Git, and checked the
available small-file manifest entries. Those checks passed. The 193 large
files on each drive were not all fully rehashed in that comparison.</p>

<p>The quick comparison therefore verified the copied structure, Git state,
and small-file contents. It left the large model contents unverified in
that copy. Their earlier checksums don’t prove that every byte survived
the later copy unchanged.</p>

<p>The <a href="/assets/local-inference/copy-verification.json">copy-check summary</a> records the exact scope. Model weights
may be downloadable again from their upstream repositories, but that
depends on continued availability and access. It is still worth keeping
the archives that already restored successfully.</p>

<h2 id="what-i-kept-separate-from-restore">What I kept separate from restore</h2>

<p>The restore does not add firewall exceptions, change the router, configure
Tailscale, or leave persistent servers running. Those operations change
how the machine is exposed and when it consumes resources. They belong to
explicit operating steps.</p>

<p>Likewise, synchronization after independent work needs more care than
copying a folder over another folder. The two Git histories must be
preserved and reconciled, while payload differences are inspected and
verified separately. Replacing <code class="language-plaintext highlighter-rouge">.git</code> is not a merge strategy.</p>

<p>The <a href="/local-inference-operations/">command reference</a> contains the restore and lifecycle
commands for each accepted machine. It also calls out the older M3
single-model manager’s different argument syntax.</p>

<p>That finishes the installation work I needed before returning to the
agent experiments. I can restore the accepted configurations and check
that they generate, and I have records of the longer inputs they handled.
I still need to test them on the work described back in February: reading
specifications, proposing useful tests, and helping triage the failures.
Those results will determine which of these models I keep using.</p>

<hr />

<p><em>Retrospective entry for October 23. Assembled October 24; the SSD-copy
verification above took place on October 24.</em></p>

<p><strong>Reference:</strong> <a href="/local-inference-operations/">Local inference operations</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 8 of Oracles, Traces, Triage. Resetting a Mac, restoring eight local model profiles, and separating a copied directory from a verified backup.]]></summary></entry><entry><title type="html">Local Models and Tool Permissions</title><link href="https://blog.nikosbaxevanis.com/2026/10/22/local-models-and-tool-permissions/" rel="alternate" type="text/html" title="Local Models and Tool Permissions" /><published>2026-10-22T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/22/local-models-and-tool-permissions</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/22/local-models-and-tool-permissions/"><![CDATA[<p><em>Part 7 of <a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a>.</em></p>

<p>After getting the local models running, I <a href="/2026/10/21/a-context-window-is-a-testable-claim/">tested their input limits</a>.
Those checks were requests to an inference server:
text went in and an answer came back. The testing agents from the earlier
parts of this series would do more with that answer. They would use it to
decide which files to read, which commands to run, and what to investigate
next.</p>

<p>While choosing the models, I kept coming back to a concern about the
community-modified weights. Could a model emit a tool call asking my agent
to scan the drive and upload what it found? Would using an original
upstream model, or a different runtime, make that safe?</p>

<p>Running inference locally keeps the prompt processing on my machine. It
doesn’t stop a model from proposing an upload, and it doesn’t stop an agent
with broad permissions from carrying one out. That applies to an upstream
model too. I need to consider both the software I’m loading and the
permissions of the program acting on its output.</p>

<p>For the repository work I’ve described in this series, access to a
compiler, a checkout, and test results is useful. Access to unrelated
files in my home directory usually isn’t. The local installation is a
good point to make those choices explicit, before attaching the tools.</p>

<h2 id="start-with-what-executes">Start with what executes</h2>

<p>A model installation contains more than weights. It can include model
implementation code, tokenizer and processor code, a chat template, native
libraries, a Python environment, and an HTTP server. Some of these execute
directly. Others supply data to code that executes.</p>

<p>Custom model code can have an ordinary purpose: implementing an
architecture that the installed library does not support yet. Its presence
does not establish malicious behavior. It does add code to the review and
pinning problem. Hugging Face’s <a href="https://huggingface.co/docs/transformers/custom_models">custom-model documentation</a>
describes this extension mechanism and the explicit trust involved in
loading custom code.</p>

<p>File formats matter too. Pickle-based loading can execute code during
deserialization; Hugging Face documents that risk in its
<a href="https://huggingface.co/docs/hub/security-pickle">pickle-scanning guidance</a>. A tensor-oriented format avoids that
particular mechanism. It does not prove that a parser is free of bugs, that
the surrounding package is benign, or that the generated text is safe to
act on.</p>

<p>I therefore recorded the components together:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>weights + model code + runtime + template + server configuration
</code></pre></div></div>

<p>That includes the separate native runtime needed by Orca GLM, as well as
the model files. Revisions and hashes let me identify what I used and
notice changes later. They don’t establish that the software is benign;
the generation tests weren’t a security audit.</p>

<h2 id="then-follow-the-tool-call">Then follow the tool call</h2>

<p>Consider the path from a response to a side effect:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>model output
    |
    v
tool-call parser
    |
    v
permission checks
    |
    v
tool process ----&gt; filesystem / subprocesses / network
</code></pre></div></div>

<p>The model can propose a filesystem search. The parser can recognize it as
a valid tool request. Neither fact should authorize the search.</p>

<p>The same applies to uploads. A syntactically valid HTTP tool call can
still name an unapproved destination or contain data the process should
never have read. Giving a model a narrower system prompt does not remove
those capabilities from the process that executes its instructions.</p>

<p>For a repository investigation, I would make the checked-out repository
and an output directory available, keep unrelated home-directory data out
of the sandbox, and restrict network access at the execution boundary.
The specific permissions depend on the task. The important property is
that they are enforced outside the generated response.</p>

<p>This is a design requirement for the agent harness. The inference tests
described in these articles did not validate a complete sandbox or an
exfiltration-resistant worker.</p>

<h2 id="upstream-provenance-helps-within-its-scope">Upstream provenance helps, within its scope</h2>

<p>Using an upstream checkpoint can remove uncertainty about changes made by
another distributor. It does not remove the need to review the runtime or
constrain the agent. An upstream model can also produce an unsafe tool
request, misunderstand the task, or follow instructions found in a file.</p>

<p>Similarly, switching from MLX-VLM to MLX-LM would change part of the
software stack. It would not by itself make the model’s tool requests
trustworthy. I would still need to establish compatibility, inspect the
loaded implementation, and test the resulting service.</p>

<p>The word “uncensored” in an artifact name describes a publisher’s
positioning. It is not evidence about the absence or presence of a
backdoor. I had no finding that the Orca artifacts were malicious.</p>

<h2 id="accessing-the-server-from-another-device">Accessing the server from another device</h2>

<p>I also wanted to send requests from other devices, including my phone
through a router running Tailscale. That raised a different access
question: who could reach the model server?</p>

<p>With the service bound to loopback, an SSH tunnel can carry requests from
another machine. With a wildcard bind, the service can accept connections
through the machine’s network interfaces, subject to routing and firewall
rules. The tested servers did not provide API authentication.</p>

<p>For an already configured SSH host alias named <code class="language-plaintext highlighter-rouge">inference-mac</code>, a tunnel
can look like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-N</span> <span class="nt">-o</span> <span class="nv">ExitOnForwardFailure</span><span class="o">=</span><span class="nb">yes</span> <span class="se">\</span>
  <span class="nt">-L</span> 127.0.0.1:18000:127.0.0.1:8000 inference-mac
</code></pre></div></div>

<p>The client then talks to <code class="language-plaintext highlighter-rouge">http://127.0.0.1:18000/v1</code>. The explicit local
bind keeps this forwarded port on the client’s loopback interface. These
are the ordinary <a href="https://man.openbsd.org/ssh#L">OpenSSH local-forwarding semantics</a>. SSH access
must already be configured and its host key verified.</p>

<p>For direct LAN access I added macOS application-firewall exceptions
manually. The installer deliberately left that decision to the operator.
Apple’s <a href="https://support.apple.com/guide/mac-help/mh34041/mac">firewall controls</a> allow incoming connections by
application; those exceptions should not be confused with a rule allowing
only one source address to one model port.</p>

<p>After the M5 restore, a request from another machine returned the model
listing on port 8000. That established remote HTTP reachability. The local
probe had already established generation. I haven’t yet tested the phone’s route through the Tailscale router.</p>

<p>For a future investigation, I would start by restricting the agent to its
checkout, working directory, and approved tools and destinations. Choosing
a model or changing the server doesn’t do that work for me.</p>

<p>The restore scripts preserve the model settings but leave firewall
changes to the operator. In the <a href="/2026/10/23/the-restore-is-the-test/">next post</a>, I describe what they do
restore, and what happened when I tried them after resetting the M5.</p>

<hr />

<p><em>Retrospective entry for October 22. Assembled October 24, including the
subsequent M5 LAN check.</em></p>

<p><strong>Next:</strong> <a href="/2026/10/23/the-restore-is-the-test/">Restoring the Models on a Clean Mac</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 7 of Oracles, Traces, Triage. The concern that came up while choosing community models, and the tool permissions I need to settle before using them for repository investigations.]]></summary></entry><entry><title type="html">Testing Longer Contexts</title><link href="https://blog.nikosbaxevanis.com/2026/10/21/a-context-window-is-a-testable-claim/" rel="alternate" type="text/html" title="Testing Longer Contexts" /><published>2026-10-21T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/21/a-context-window-is-a-testable-claim</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/21/a-context-window-is-a-testable-claim/"><![CDATA[<p><em>Part 6 of <a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a>.</em></p>

<p>In the <a href="/2026/10/20/self-hosted-inference-on-apple-silicon/">previous article</a>, I described getting local models
running on the two Macs. A short prompt checked that an installation could
generate an answer. The agent experiments from earlier in this series
need more than that: the model may have to read source files, a piece of a
specification, and the output of several failed test runs before deciding
what to try next.</p>

<p>All of that takes space in the context window, the input and output the
model can work with in one request. That space is measured in tokens,
which are pieces of text rather than necessarily whole words. The code
and conversation history use the same budget as the answer.</p>

<p>When I looked at the initial settings, two <a href="https://github.com/antirez/ds4">DwarfStar</a> profiles were limited
to 8,192 tokens. Several <a href="https://huggingface.co/orcarouter">Orca</a> profiles had no explicit cap in the manager.
I wanted to see how much further I could take them on these machines,
and to have a predictable failure when a request was too large.</p>

<p>The 8K settings were defaults, not measurements of what the models could
handle. Nor could I read an upstream context limit and assume the local
installation would manage it. The runtime’s memory use and handling of
long requests needed testing with the exact configurations I intended to
keep.</p>

<h2 id="a-cap-needs-a-test-on-both-sides">A cap needs a test on both sides</h2>

<p>The procedure started with a small generation and increased the input in
stages. The model had to retrieve specific records from synthetic text,
then handle a follow-up asking for those records in a different order.
An oversized request had to be rejected. A final small probe checked that
the server remained usable afterwards.</p>

<p>For each configuration I checked that:</p>

<ol>
  <li>The server reports the expected model and context configuration.</li>
  <li>The request contains the measured number of tokens after formatting.</li>
  <li>The answer contains the expected records.</li>
  <li>A follow-up preserves the required information.</li>
  <li>An over-limit request fails instead of silently discarding input.</li>
  <li>The server can still generate after that rejection.</li>
</ol>

<p>The request and the generated answer share a context budget. A 200,000
token cap is not an invitation to send 200,000 input tokens and then ask
for a long answer. The template and any other inserted tokens count too.
The server’s default output budget is a separate setting; clients should
send <code class="language-plaintext highlighter-rouge">max_tokens</code> explicitly.</p>

<p>I precomputed the test inputs before loading the large engine. Earlier,
running an auxiliary tokenizer diagnostic alongside a resident model had
caused memory pressure severe enough to interfere with SSH access. Moving
that preparation out of the inference run avoided making the diagnostic
compete with the model for memory.</p>

<h2 id="what-passed-on-the-smaller-mac">What passed on the smaller Mac</h2>

<p>The following is the accepted M5 Max, 128 GB result. The figures come from
the <a href="/assets/local-inference/qualification.json">recorded qualification summaries</a>, not from model-card maxima.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Collection</th>
      <th style="text-align: left">Model</th>
      <th style="text-align: right">Context cap</th>
      <th style="text-align: right">Largest tested input</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731">DeepSeek V4 Flash 0731</a></td>
      <td style="text-align: right">200,000</td>
      <td style="text-align: right">197,925</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017">DeepSeek V4.1 Flash 1017</a></td>
      <td style="text-align: right">200,000</td>
      <td style="text-align: right">197,906</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/zai-org/GLM-5.3-Flash">GLM 5.3 Flash</a></td>
      <td style="text-align: right">200,000</td>
      <td style="text-align: right">197,918</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/Qwen/Qwen3.8-Flash-Next">Qwen3.8 Flash Next</a></td>
      <td style="text-align: right">200,000</td>
      <td style="text-align: right">197,847</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Qwen3.8-27B-Uncensored-MLX">Qwen3.8 27B</a></td>
      <td style="text-align: right">131,072</td>
      <td style="text-align: right">129,174</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Nex-N2.5-mini-Uncensored-MLX">Nex-N2.5-mini</a></td>
      <td style="text-align: right">131,072</td>
      <td style="text-align: right">129,141</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/OrcaSAQ-2-Cyber-27B-Uncensored-GGUF">OrcaSAQ-2 Cyber 27B</a></td>
      <td style="text-align: right">131,072</td>
      <td style="text-align: right">129,283</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/GLM-5.3-Flash-Uncensored-GGUF">GLM 5.3 Flash</a></td>
      <td style="text-align: right">131,072</td>
      <td style="text-align: right">129,019</td>
    </tr>
  </tbody>
</table>

<p>Each accepted M5 run used one engine and one sequence or session. The
sampled memory-pressure level remained normal. Some runs began with swap
already allocated, so I recorded the starting and peak values rather than
describing the machine as swap-free.</p>

<p>All eight passed the synthetic retrieval, follow-up, rejection, and
recovery checks at these sizes. I would still need real repository tasks
to judge whether the extra context helps with an investigation. Finding
records in generated text is a much narrower task than understanding the
interactions in a large codebase. The separate image smoke tests also
leave long-context multimodal use untested.</p>

<h2 id="the-second-request-can-be-a-different-workload">The second request can be a different workload</h2>

<p>The largest Orca requests produced the following elapsed times, including
their short answers:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Model</th>
      <th style="text-align: right">Initial request (seconds)</th>
      <th style="text-align: right">Follow-up (seconds)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Qwen3.8-27B-Uncensored-MLX">Qwen3.8 27B</a></td>
      <td style="text-align: right">447.7</td>
      <td style="text-align: right">454.0</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Nex-N2.5-mini-Uncensored-MLX">Nex-N2.5-mini</a></td>
      <td style="text-align: right">138.0</td>
      <td style="text-align: right">139.6</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/OrcaSAQ-2-Cyber-27B-Uncensored-GGUF">OrcaSAQ-2 Cyber 27B</a></td>
      <td style="text-align: right">869.7</td>
      <td style="text-align: right">4.6</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/GLM-5.3-Flash-Uncensored-GGUF">GLM 5.3 Flash</a></td>
      <td style="text-align: right">3,208.8</td>
      <td style="text-align: right">4.6</td>
    </tr>
  </tbody>
</table>

<p>The follow-up sent the conversation again and asked for the records in
reverse order. The selected MLX serving path recomputed the conversation;
the native serving paths reused much more state. A four-second follow-up
therefore tells a very different story from a four-second cold request.</p>

<p>Background copies and downloads were active during qualification. These
are observations from the recorded workloads, not isolated throughput
benchmarks. They should not be used to rank the underlying model families.</p>

<p>The DwarfStar DeepSeek V4.1 streaming configuration made the same point:
its largest input took about thirty minutes, followed by a cached request
of about five seconds. Whether that is acceptable depends on the job. A
long unattended investigation and an interactive editing session have
different tolerances.</p>

<h2 id="what-i-can-use-these-results-for">What I can use these results for</h2>

<p>The caps now give the agent client a budget to work within, and the
rejection tests give me a way to catch regressions in how the server
enforces it. The timings will also affect how I construct requests. A
follow-up that reuses earlier computation and one that processes the
whole conversation again can have very different costs.</p>

<p>There is still a quality comparison to do. On the M5, smaller
quantizations made some of these configurations practical, but the
retrieval checks don’t tell me how much reasoning or coding ability I
lost. For that I need the same tasks, prompts, tool budgets, and scoring
for each candidate. I will keep those results separate from the context
qualification table.</p>

<p>For now, I have measured input limits and a repeatable procedure for
checking them. Before connecting an agent to these services, I also need
to decide what it will be allowed to do with their responses. That became
a concrete concern while choosing community-modified models, and is the
subject of the <a href="/2026/10/22/local-models-and-tool-permissions/">next article</a>.</p>

<hr />

<p><em>Retrospective entry for October 21. Assembled October 24; the M5
measurements above are from October 23.</em></p>

<p><strong>Next:</strong> <a href="/2026/10/22/local-models-and-tool-permissions/">Local Models and Tool Permissions</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 6 of Oracles, Traces, Triage. Moving beyond an 8K default with measured inputs, explicit caps, follow-up requests, and rejection tests.]]></summary></entry><entry><title type="html">Self-Hosted Inference on Apple Silicon</title><link href="https://blog.nikosbaxevanis.com/2026/10/20/self-hosted-inference-on-apple-silicon/" rel="alternate" type="text/html" title="Self-Hosted Inference on Apple Silicon" /><published>2026-10-20T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/20/self-hosted-inference-on-apple-silicon</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/20/self-hosted-inference-on-apple-silicon/"><![CDATA[<p><em>Part 5 of <a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a>.</em></p>

<p>Back in February, I left this series with <a href="/2026/02/16/agents-as-fuzzers/">Agents as Fuzzers</a>.
The idea was to combine agents that read specifications and propose tests
with fuzzers that execute those tests, then check and triage the results.
The earlier posts covered the instructions, coordination, and testing
practices around that loop. I was using Claude Code, and hadn’t spent much
of the series on the model running underneath it.</p>

<p>Eight months later, I’m picking up that part. I’ve been setting up local
models on two Macs: an M3 Ultra with 256 GB of unified memory and an M5 Max
with 128 GB. I want to find out how much of this agent-assisted work I can
run on my own hardware. Before comparing the models on testing tasks, I
need an installation I can run reliably and rebuild when something goes
wrong.</p>

<p>The series outline originally put self-hosted inference on Runpod next.
This experiment ended up on Apple Silicon instead. It is a detour into the
machinery behind the agent, over four posts: getting the models running,
testing how much input they can handle, deciding what access to give them,
and restoring the whole setup after a reset. I haven’t yet measured
whether these local models can do the bug-finding work as well as the
hosted models I’ve been using.</p>

<h2 id="what-im-moving-onto-the-mac">What I’m moving onto the Mac</h2>

<p>In the earlier articles, an agent could read a repository, suggest a test,
run it, and inspect the result. The model supplies the responses, which
may include requests to use a
tool. The agent program handles the conversation and runs the permitted
tools. In this setup, the model runs in a separate server process.</p>

<p>Here I’m setting up the model side. An inference server loads the model
files and accepts requests from a client, such as an agent. The files
contain the model’s weights; the runtime is the software that uses those
weights to generate an answer. Hosting that server locally lets me keep
this part of the work on the Mac and pin the exact files and software I
use. The agent’s tools and permissions still need their own configuration.</p>

<p>This also changes the practical constraints on the experiments from
February. A model that takes several minutes to read a prompt will make
an agent’s repeated attempts expensive in time. A model that fits alone
may not fit alongside another worker. I need to know those limits before
trying the <a href="/2026/02/08/agent-teams-and-claude-swarm/">parallel-agent setup from Part 2</a> with these models.</p>

<h2 id="moving-from-256-gb-to-128-gb">Moving from 256 GB to 128 GB</h2>

<p>The M3 installation had nine model profiles. Each profile selected a model
and the settings needed to serve it. I kept separate kits for the M5,
because copying the M3 configuration to a machine with half the memory
wouldn’t tell me which models were actually usable there.</p>

<p>For several models, that meant choosing a smaller quantization: weights
stored with reduced numerical precision to reduce their size. Names such
as Q2, Q4, and 8-bit identify different choices of representation. Some
artifacts use a mixture, keeping selected parts at higher precision. The
memory saving needs to be weighed against any loss in answer quality;
getting a file to load only answers the first half of that question.</p>

<p>I organized the kits into two collections, called <a href="https://github.com/antirez/ds4">DwarfStar</a> and
<a href="https://huggingface.co/orcarouter">Orca</a>.
These names recur in the tables below. Each kit includes the model files,
runtime, settings, restore scripts, and test records. The collection name
doesn’t imply that all its models use the same engine.</p>

<p>The M5 ended up with these eight configurations:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Collection</th>
      <th style="text-align: left">Model</th>
      <th style="text-align: left">Selected format or loading arrangement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731">DeepSeek V4 Flash 0731</a></td>
      <td style="text-align: left">Mixed Q2/Q4 experts</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017">DeepSeek V4.1 Flash 1017</a></td>
      <td style="text-align: left">IQ2/Q2, SSD expert streaming</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/zai-org/GLM-5.3-Flash">GLM 5.3 Flash</a></td>
      <td style="text-align: left">Q2 with a vision encoder</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/Qwen/Qwen3.8-Flash-Next">Qwen3.8 Flash Next</a></td>
      <td style="text-align: left">Q4, disk-backed n-grams</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Qwen3.8-27B-Uncensored-MLX">Qwen3.8 27B</a></td>
      <td style="text-align: left">MLX 8-bit</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Nex-N2.5-mini-Uncensored-MLX">Nex-N2.5-mini</a></td>
      <td style="text-align: left">MLX 6-bit with selected 8-bit modules</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/OrcaSAQ-2-Cyber-27B-Uncensored-GGUF">OrcaSAQ-2 Cyber 27B</a></td>
      <td style="text-align: left">Mixed GGUF, mostly IQ4_XS</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/GLM-5.3-Flash-Uncensored-GGUF">GLM 5.3 Flash</a></td>
      <td style="text-align: left">Q2_K GGUF with an F16 vision encoder</td>
    </tr>
  </tbody>
</table>

<p>The <a href="/assets/local-inference/qualification.json">qualification record</a> identifies the selected artifacts and
the tests they passed. This isn’t a recommendation of one model over
another. I haven’t done the paired coding and reasoning tests needed to
compare, for example, the smaller GLM quantization with the one on the M3.
DeepSeek V4.1 Flash 1017 is a recent addition: its public repository records
its <a href="https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017/commits/main">release on October 17</a>, before these experiments.</p>

<h2 id="what-stays-in-memory">What stays in memory</h2>

<p>The size on disk wasn’t enough to work out what would fit. I also had to
account for what the runtime kept in memory, and what it needed while
processing a request.</p>

<p>The M5 DeepSeek V4.1 configuration streams expert weights from the
internal SSD, keeping a <code class="language-plaintext highlighter-rouge">64GB</code> cache in memory. Qwen has large n-gram
tables, described in its <a href="https://huggingface.co/Qwen/Qwen3.8-Flash-Next">model card</a>, which the selected runtime
keeps disk-backed. Both arrangements let me use artifacts larger than I
could keep entirely resident. They also make the time spent reading and
processing a new prompt worth measuring.</p>

<p>The external SSD serves a different purpose. It holds the recovery kit;
the installed models run from internal storage. I can disconnect the
external drive after restoring them.</p>

<p>I tested one engine and one sequence at a time on the M5. During the
earlier work, an auxiliary diagnostic running alongside a loaded model
caused severe memory pressure. For now, the agent-swarm experiments will
have to respect that restriction. These results don’t establish that I
can run several local workers concurrently.</p>

<h2 id="more-than-one-runtime">More than one runtime</h2>

<p>The model choices also left me with several runtimes to maintain.
<a href="https://github.com/ml-explore/mlx-lm">MLX-LM</a> and <a href="https://github.com/Blaizzy/mlx-vlm">MLX-VLM</a> are separate projects using Apple’s
MLX framework, with MLX-VLM supporting vision-language models. The Orca
Qwen and Nex installations use a pinned MLX-VLM environment. OrcaSAQ and
Orca GLM use GGUF files, a format consumed here by native llama.cpp
servers. The DwarfStar models use two archived revisions of the DS4 engine.</p>

<p>Even the two llama.cpp installations aren’t interchangeable. Orca GLM
needed a separate pinned build. One loading attempt failed a Metal
allocation; the configuration that passed used automatic fitting, a
single sequence, and <code class="language-plaintext highlighter-rouge">--load-mode none</code>. In that particular build,
<a href="https://github.com/unslothai/llama.cpp/blob/86ebfef2c6a0f3359a2a07d2c215d61b0fa885c9/common/arg.cpp#L2580"><code class="language-plaintext highlighter-rouge">none</code> disables special loading modes</a>, including mmap. It
doesn’t disable GPU inference.</p>

<p>I kept those settings with their runtime revisions so that a future
restore wouldn’t depend on remembering which build accepted which flags.
The <a href="/local-inference-operations/">operations reference</a> has the machine-specific commands.</p>

<h2 id="checking-that-a-model-can-run">Checking that a model can run</h2>

<p>Once a server starts, it can answer an HTTP request for its model list
without having generated anything. I needed a short generation test as
well as that readiness check. The managers therefore expose four actions:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>start -&gt; status -&gt; probe -&gt; stop
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">status</code> checks the owned process and HTTP readiness. <code class="language-plaintext highlighter-rouge">probe</code> asks the model
to generate an answer and checks the response. The Orca start operation
also runs a generation before reporting success. Stopping checks the
process identity, including its start time and command, before sending a
signal; a saved PID alone could refer to a different process by then.</p>

<p>By October 23, eight M5 profiles had passed the qualification procedure.
The remaining candidate, Orca DeepSeek, still had an unfinished conversion.
I stopped the conversion and its supervisor, kept the verified original
shards for possible later work, and left it out of the installer.</p>

<p>A short answer was enough to check that each restored installation could
perform inference. For the work in this series, though, the input will
include code, specifications, and the results of earlier tool calls. The
<a href="/2026/10/21/a-context-window-is-a-testable-claim/">next article</a> covers what happened when I tested much longer
requests, including how long I had to wait for the answers.</p>

<hr />

<p><em>Retrospective entry for October 20. Assembled October 24; the M5 results
above were obtained on October 23.</em></p>

<p><strong>Next:</strong> <a href="/2026/10/21/a-context-window-is-a-testable-claim/">Testing Longer Contexts</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 5 of Oracles, Traces, Triage. Returning to the agent experiments with a local model server: what runs on my Macs, and what needs testing before I can use it for that work.]]></summary></entry><entry><title type="html">Agents as Fuzzers</title><link href="https://blog.nikosbaxevanis.com/2026/02/16/agents-as-fuzzers/" rel="alternate" type="text/html" title="Agents as Fuzzers" /><published>2026-02-16T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/02/16/agents-as-fuzzers</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/02/16/agents-as-fuzzers/"><![CDATA[<p><em>This article is part of the Oracles, Traces, Triage <a href="/2026/01/25/oracles-traces-triage/">series</a>.</em></p>

<h2 id="the-short-version">The short version</h2>

<p><strong>A fuzzer is a search tool whose results must be triaged. An AI agent is a search tool whose results must be triaged.</strong> The parallel is not metaphorical. I think it’s structural.</p>

<h2 id="two-search-tools">Two search tools</h2>

<p><strong>A fuzzer</strong> explores the input space of a program, looking for inputs that violate some oracle—a crash, a hang, a property violation. When it finds something, you triage: real bug? Duplicate? Exploitable?</p>

<p><strong>An AI agent</strong> explores the solution space of a problem, looking for code, fixes, or tests that satisfy some goal. When it produces something, you triage: correct? Complete? Does it address the problem?</p>

<p>Both search. Both produce results that need judgment. Both waste enormous time if pointed in the wrong direction.</p>

<h2 id="the-anatomy-side-by-side">The anatomy, side by side</h2>

<p><strong>Every fuzzer</strong> does four things:</p>

<ol>
  <li><strong>Generates inputs</strong> (random, mutational, grammar-based, coverage-guided)</li>
  <li><strong>Executes the target</strong> with those inputs</li>
  <li><strong>Checks an oracle</strong> (crash? new coverage? property violation?)</li>
  <li><strong>Saves interesting results</strong> for triage</li>
</ol>

<p><strong>Every AI agent</strong> does the same four things:</p>

<ol>
  <li><strong>Generates candidates</strong> (from prompt, codebase, agent skills)</li>
  <li><strong>Executes or applies</strong> them (writes code, runs tests, modifies files)</li>
  <li><strong>Checks an oracle</strong> (tests pass? linter clean? invariants hold?)</li>
  <li><strong>Saves results</strong> for triage (commits, PRs, logs)</li>
</ol>

<p>Replace “inputs” with “candidates” and “crash” with “test failure.” The structure is identical.</p>

<h2 id="what-changes-when-the-searcher-understands-context">What changes when the searcher understands context</h2>

<p>Traditional fuzzers are <strong>context-blind</strong>. AFL doesn’t know what a function does. libFuzzer doesn’t understand the specification. They compensate with <strong>volume</strong>—millions of executions per second.</p>

<p>Context-blindness has costs:</p>

<ul>
  <li><strong>Shallow oracles.</strong> “Did it crash?” works. “Does this violate the protocol invariant?” requires a custom harness—often harder to write than the code being tested.</li>
  <li><strong>Redundant exploration.</strong> Without understanding structure, the fuzzer wastes cycles in uninteresting regions of input space.</li>
  <li><strong>Triage burden.</strong> Many findings are duplicates, benign panics, or expected edge cases. You sort the signal from the noise.</li>
</ul>

<p>An AI agent, by contrast:</p>

<ul>
  <li>Can <strong>read the specification</strong></li>
  <li>Can <strong>reason about</strong> which inputs trigger interesting behavior</li>
  <li>Can <strong>write its own oracle</strong> and generate inputs designed to challenge it</li>
</ul>

<p>The search becomes <strong>intentional</strong> without becoming rigid.</p>

<h2 id="the-convergence">The convergence</h2>

<p>Combine the pieces from this series:</p>

<table>
  <thead>
    <tr>
      <th>Piece</th>
      <th>Fuzzer equivalent</th>
      <th>What it adds</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="/2026/02/01/agent-skills-and-claude-lint/">Agent skills</a></td>
      <td>Oracle</td>
      <td>Richer than “did it crash?”—norms that agents translate into testable properties</td>
    </tr>
    <tr>
      <td><a href="/2026/02/08/agent-teams-and-claude-swarm/">Agent swarms</a></td>
      <td>Multiple seeds</td>
      <td>Parallel search where each instance can <strong>specialize</strong>, sharing findings via git</td>
    </tr>
    <tr>
      <td><a href="/2026/02/15/testing-between-calls-with-agents/">Stateful testing</a></td>
      <td>Execution loop</td>
      <td>For <strong>traces</strong> instead of single inputs</td>
    </tr>
  </tbody>
</table>

<p>Together: <strong>context-aware search, parallel exploration, rich oracles</strong>.</p>

<h2 id="fuzzers-still-win-at">Fuzzers still win at</h2>

<ul>
  <li><strong>Speed.</strong> Millions of executions/sec with a simple oracle (“did it crash?”). AFL and libFuzzer are unbeatable here.</li>
  <li><strong>Binary targets.</strong> No source code, no spec? Blind fuzzing is often the only option.</li>
  <li><strong>Deterministic reproduction.</strong> Fuzzers produce exact inputs. Agent traces may need work to become deterministic.</li>
  <li><strong>Corpus management.</strong> Mature fuzzers have corpus minimization, coverage tracking, seed scheduling. Agent ecosystems don’t—yet.</li>
</ul>

<h2 id="agents-win-at">Agents win at</h2>

<ul>
  <li><strong>Rich invariants.</strong> “Does this sequence of state transitions preserve safety properties?” An agent can both <em>formulate</em> and <em>check</em> the invariant.</li>
  <li><strong>Spec-guided search.</strong> When the spec exists and is readable, agents generate targeted campaigns rather than relying on coverage alone.</li>
  <li><strong>Triage.</strong> An agent can produce a root-cause hypothesis before you ever see the failure. It can check for duplicates.</li>
  <li><strong>Harness generation.</strong> Writing fuzz harnesses is expert work. Agents can draft them from specs and iterate.</li>
</ul>

<h2 id="the-spectrum">The spectrum</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Traditional Fuzzer</th>
      <th>AI Agent</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Input generation</td>
      <td>Random / mutational / grammar</td>
      <td>Context-aware / intentional</td>
    </tr>
    <tr>
      <td>Oracle</td>
      <td>Crash / coverage / property</td>
      <td>Natural-language norm → property</td>
    </tr>
    <tr>
      <td>Speed</td>
      <td>Millions of executions/sec</td>
      <td>Seconds to minutes per session</td>
    </tr>
    <tr>
      <td>Context understanding</td>
      <td>None</td>
      <td>Deep</td>
    </tr>
    <tr>
      <td>Triage</td>
      <td>Manual</td>
      <td>Agent-assisted</td>
    </tr>
    <tr>
      <td>Parallelism</td>
      <td>Independent seeds</td>
      <td>Coordinated via git</td>
    </tr>
  </tbody>
</table>

<p>The gap is narrowing. What matters is understanding which tool fits which problem—and being willing to combine them.</p>

<h2 id="in-practice">In practice</h2>

<ul>
  <li><strong>Traditional fuzzers</strong> for the fast, low-level search—serialization, encoding edge cases, roundtrip invariants. Simple oracles, enormous input spaces. <strong>Volume wins.</strong></li>
  <li><strong>AI agents</strong> for the slow, high-level search—stateful invariants, cross-component interactions, spec compliance. Complex oracles, understanding required. <strong>Context wins.</strong></li>
  <li><strong>Both together</strong>—agents generating hypotheses and fuzz harnesses, fuzzers executing at speed, agents triaging the results.</li>
</ul>

<p>Fuzzing was barely known outside security research fifteen years ago. Standard practice after AFL and OSS-Fuzz. Table stakes today.</p>

<p>AI-assisted testing is on the same trajectory.</p>

<h2 id="related-posts">Related posts</h2>

<ul>
  <li><a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a> (series index)</li>
  <li><a href="/2026/02/01/agent-skills-and-claude-lint/">Agent Skills and claude-lint</a></li>
  <li><a href="/2026/02/08/agent-teams-and-claude-swarm/">Agent Teams and claude-swarm</a></li>
  <li><a href="/2026/02/15/testing-between-calls-with-agents/">Testing the Bugs Between Calls</a></li>
  <li><a href="/2026/01/31/the-bugs-between-calls/">The Bugs Between Calls</a></li>
  <li><a href="/2023/03/03/property-tests-vs-fuzzing/">Property Tests Are Not A Fuzzer</a></li>
  <li><a href="/2023/12/15/fuzzing-meets-property-testing/">Fuzzing meets property testing</a></li>
</ul>

<hr />

<p><strong>Next:</strong> <a href="/2026/10/20/self-hosted-inference-on-apple-silicon/">Self-Hosted Inference on Apple Silicon</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 4 of the Oracles, Traces, Triage series. A fuzzer is a search tool whose results must be triaged. An AI agent is a search tool whose results must be triaged. Perhaps that's not a coincidence.]]></summary></entry><entry><title type="html">Testing the Bugs Between Calls</title><link href="https://blog.nikosbaxevanis.com/2026/02/15/testing-between-calls-with-agents/" rel="alternate" type="text/html" title="Testing the Bugs Between Calls" /><published>2026-02-15T00:00:00+00:00</published><updated>2026-02-15T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/02/15/testing-between-calls-with-agents</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/02/15/testing-between-calls-with-agents/"><![CDATA[<p><em>This article is part of the Oracles, Traces, Triage <a href="/2026/01/25/oracles-traces-triage/">series</a>.</em></p>

<h2 id="the-short-version">The short version</h2>

<p><strong>Agent skills + agent swarms + stateful testing could compound into something stronger than any piece alone.</strong> While this combination hasn’t been tested at scale with agents yet, the individual components have proven effective in practice. The following explores how these pieces might integrate.</p>

<h2 id="the-problem">The problem</h2>

<p>In <a href="/2026/01/31/the-bugs-between-calls/">The Bugs Between Calls</a>, I argued:</p>

<ul>
  <li>The most expensive failures don’t live in <strong>single function calls</strong></li>
  <li>They live in <strong>sequences</strong>—valid operations that, composed under load, trigger liveness incidents</li>
  <li><strong>Stateless</strong> property-based testing catches bugs in the bricks</li>
  <li><strong>Stateful</strong> property-based testing catches bugs in how the bricks stack</li>
</ul>

<p>The December 2025 Prysm incident, the May 2023 finality delays—stacking failures. Every individual operation was valid. The trace was the problem.</p>

<p>If you want another non-Ethereum example of “trace-shaped” failures, the Stacks PoX-2 <code class="language-plaintext highlighter-rouge">stack-increase</code> bug is a good one to skim (<a href="https://forum.stacks.org/t/a-bug-in-stacks-increase-call-is-impacting-stacking-rewards-this-cycle/14867">thread</a>). I wasn’t at Stacks at the time, and I’m not claiming I would have caught it; the point is simply that these failures often emerge from sequences and accounting state, not single calls.</p>

<h2 id="what-agents-could-add">What agents could add</h2>

<p>Stateful property-based testing (described in <a href="/2026/01/31/the-bugs-between-calls/">The Bugs Between Calls</a>) generates <strong>random command sequences</strong>, runs them, checks invariants after each step.</p>

<p>The framework doesn’t know <em>why</em> a particular sequence might be interesting. It just tries many and hopes to stumble on something broken.</p>

<p>An AI agent is not blind:</p>

<ul>
  <li>It can <strong>read the specification</strong></li>
  <li>It can <strong>study past incidents</strong></li>
  <li>It can <strong>reason about which sequences</strong> are likely to trigger interesting states</li>
</ul>

<p>It wouldn’t replace the random search. It would augment it with <strong>intentional exploration</strong>.</p>

<h2 id="three-pieces">Three pieces</h2>

<h3 id="1-agent-skills-define-testing-norms">1. Agent skills define testing norms</h3>

<p>Following the <a href="/2026/02/01/agent-skills-and-claude-lint/">agent skills philosophy</a>, you wouldn’t give agents step-by-step procedures. You’d give them <strong>norms</strong>:</p>

<ul>
  <li><strong>Idempotent imports:</strong> importing the same block twice must not double-apply side effects</li>
  <li><strong>Epoch boundaries:</strong> boundary logic must not run twice across reorgs</li>
  <li><strong>Invariant preservation:</strong> state transitions must preserve declared invariants</li>
</ul>

<p>These go into <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> and agent skills. The agent figures out <em>how</em> to test them. At least, that’s the idea.</p>

<h3 id="2-agent-swarms-parallelize-exploration">2. Agent swarms parallelize exploration</h3>

<p>With <a href="/2026/02/08/agent-teams-and-claude-swarm/">claude-swarm</a>, you could run <strong>multiple agents against the same codebase</strong>, each exploring a different class of invariant:</p>

<ul>
  <li>One explores <strong>idempotence</strong></li>
  <li>Another targets <strong>epoch boundaries</strong></li>
  <li>Another maintains the <strong>test infrastructure</strong></li>
</ul>

<p>Each agent pushes to the same repo. When one discovers a failing trace, the others see it on the next fetch.</p>

<p>No message passing needed—the test failures <em>are</em> the messages.</p>

<h3 id="3-the-feedback-loop-tightens">3. The feedback loop tightens</h3>

<p>Today, you get a shrunk counterexample and figure out what it means yourself. With agents, the cycle could become:</p>

<ol>
  <li>Agent generates command sequences <strong>based on norms</strong></li>
  <li>proptest executes them, <strong>finds a failure</strong></li>
  <li>proptest <strong>shrinks</strong> the failure to a minimal trace</li>
  <li>Agent reads the trace, <strong>generates a root-cause hypothesis</strong></li>
  <li>Agent <strong>writes a regression test</strong></li>
</ol>

<p>Steps 4 and 5 are currently hours of manual work. They wouldn’t be free with agents—output still needs triage. But the iteration speed could be fundamentally different.</p>

<p>While this complete loop hasn’t been tested end-to-end yet, each component exists and has proven valuable in isolation.</p>

<h2 id="where-this-probably-wont-work">Where this probably won’t work</h2>

<ul>
  <li><strong>Enormous state spaces, simple invariants.</strong> If your oracle is “did it crash,” a traditional fuzzer wins. Agents are slow by comparison.</li>
  <li><strong>Precise mathematical constraints.</strong> When the goal is “find the exact input satisfying this formal constraint,” SMT solvers are more reliable. Agents reason about code, but they don’t exhaustively search a constraint space.</li>
</ul>

<p>The agent’s advantage: <strong>structured exploration</strong>—when invariants are rich, the state machine is complex, and interesting traces require understanding.</p>

<h2 id="the-combination">The combination</h2>

<table>
  <thead>
    <tr>
      <th>Piece</th>
      <th>Role</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="/2026/02/01/agent-skills-and-claude-lint/">Agent skills</a></td>
      <td>Encode <strong>what matters</strong>—norms, invariants, properties</td>
    </tr>
    <tr>
      <td><a href="/2026/02/08/agent-teams-and-claude-swarm/">Agent swarms</a></td>
      <td>Provide <strong>parallel exploration</strong>—multiple agents, different state-space regions</td>
    </tr>
    <tr>
      <td><a href="/2026/01/31/the-bugs-between-calls/">Stateful testing</a></td>
      <td>Provide the <strong>execution engine</strong>—command sequences, invariant checks, shrinking</td>
    </tr>
  </tbody>
</table>

<p>Each piece works alone. Together, they compound.</p>

<p>This hypothesis remains unproven at scale. However, the individual components have demonstrated value in isolation, and their integration appears promising.</p>

<p>The best testing infrastructure usually emerges that way—you notice the pieces reinforcing each other before you design the integration.</p>

<h2 id="related-posts">Related posts</h2>

<ul>
  <li><a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a> (series index)</li>
  <li><a href="/2026/02/01/agent-skills-and-claude-lint/">Agent Skills and claude-lint</a></li>
  <li><a href="/2026/02/08/agent-teams-and-claude-swarm/">Agent Teams and claude-swarm</a></li>
  <li><a href="/2026/01/31/the-bugs-between-calls/">The Bugs Between Calls</a></li>
  <li><a href="/2025/03/10/chaos-testing-stacks-node/">Chaos Testing stacks-node with Model-Based Stateful Testing</a></li>
</ul>

<hr />

<p><strong>Next:</strong> <a href="/2026/02/16/agents-as-fuzzers/">Agents as Fuzzers</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 3 of the Oracles, Traces, Triage series. On combining agent swarms and agent skills with stateful property-based testing to find the consensus bugs that live in sequences, not single calls.]]></summary></entry><entry><title type="html">Agent Teams and claude-swarm</title><link href="https://blog.nikosbaxevanis.com/2026/02/08/agent-teams-and-claude-swarm/" rel="alternate" type="text/html" title="Agent Teams and claude-swarm" /><published>2026-02-08T00:00:00+00:00</published><updated>2026-02-08T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/02/08/agent-teams-and-claude-swarm</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/02/08/agent-teams-and-claude-swarm/"><![CDATA[<p><em>This article is part of the Oracles, Traces, Triage <a href="/2026/01/25/oracles-traces-triage/">series</a>.</em></p>

<h2 id="one-agent-hits-a-ceiling">One agent hits a ceiling</h2>

<p>A single Claude Code session can do one thing at a time. For small tasks—fix this function, write that test—that’s fine. But the work I care about is not small. Exploring multiple hypotheses in parallel, maintaining documentation while debugging, running specialized analysis while generating test harnesses.</p>

<p>One agent, one task, one context window. It doesn’t scale.</p>

<h2 id="the-agent-team-pattern">The agent-team pattern</h2>

<p>In early February 2026, Anthropic published <a href="https://www.anthropic.com/engineering/building-c-compiler">Building a C Compiler with Large Language Models</a>—a detailed account of 16 Claude instances working in parallel to produce a 100,000-line Rust-based C compiler capable of building the Linux kernel. The total: nearly 2,000 Claude Code sessions, 2 billion input tokens, 140 million output tokens.</p>

<p>The architecture was surprisingly simple. No orchestrator. No message bus. No shared memory. Just git.</p>

<p>Each agent ran in a Docker container. Each cloned a shared bare repo, worked on a task, and pushed. When two agents tried to claim the same task, git’s built-in conflict resolution forced the second one to pick something else. Merge conflicts happened often; Claude was smart enough to resolve them.</p>

<p>The key insight: <strong>coordination through the codebase itself</strong>. The repo <em>is</em> the shared state. Commits <em>are</em> the messages. Locks <em>are</em> text files.</p>

<h2 id="claude-swarm">claude-swarm</h2>

<p>This pattern can be implemented through <a href="https://github.com/moodmosaic/claude-swarm">claude-swarm</a>—a reusable harness currently wired for running multiple Claude Code sessions in Docker containers, coordinating through git. The coordination pattern itself is tool-agnostic; <code class="language-plaintext highlighter-rouge">claude-swarm</code> is just one concrete implementation.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export ANTHROPIC_API_KEY="sk-ant-..."
export AGENT_PROMPT="path/to/prompt.md"
./tools/claude-swarm/launch.sh start
./tools/claude-swarm/launch.sh status
./tools/claude-swarm/launch.sh stop
</code></pre></div></div>

<p>The design is minimal by conviction, not by laziness:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host                        /tmp (bare repos)
~/project/ ── git clone ──&gt; project-upstream.git (rw)
               --bare       project-mirror-*.git (ro)
                                     |
                                     | docker volumes
                                     |
               .---------------------+---------------------.
               |                     |                     |
           Container 1          Container 2          Container 3
           /upstream  (rw)      /upstream  (rw)      /upstream  (rw)
           /mirrors/* (ro)      /mirrors/* (ro)      /mirrors/* (ro)
               |                     |                     |
               v                     v                     v
           /workspace/          /workspace/          /workspace/
           (agent-work)         (agent-work)         (agent-work)
</code></pre></div></div>

<p>All containers mount the same bare repo. When one agent pushes, others see the changes on the next fetch. Each container runs <code class="language-plaintext highlighter-rouge">harness.sh</code>, which clones, resets to <code class="language-plaintext highlighter-rouge">origin/agent-work</code>, runs one Claude session, and loops. Agents stop after a configurable number of idle sessions with no commits.</p>

<h2 id="why-no-orchestrator">Why no orchestrator</h2>

<p>The temptation is always to add a coordinator—something that assigns tasks, monitors progress, resolves conflicts. This approach avoids orchestration for the same reason it avoids workflow verbs in <a href="/2026/02/01/agent-skills-and-claude-lint/">CLAUDE.md</a>: <strong>centralized control tends to reduce agent autonomy and reasoning capabilities</strong>.</p>

<p>With no orchestrator, each agent must orient itself. It reads the README, checks the current state of the code, looks at what other agents have done, and decides what to work on next. This mirrors how good engineering teams actually function: shared context, local autonomy, coordination through artifacts.</p>

<p>Anthropic’s experience confirmed the pattern. Their agents maintained running docs of failed approaches. They took locks on tasks by writing text files. They specialized naturally—one agent coalescing duplicate code, another improving performance, another working on documentation.</p>

<h2 id="specialization-is-possible-not-required">Specialization is possible, not required</h2>

<p>Right now, all agents in claude-swarm share the same prompt. They self-organize by looking at the repo and picking different things to work on.</p>

<p>Anthropic’s experience suggests that per-agent prompts—one focused on code quality, another on test coverage, another on documentation—can help at scale. claude-swarm supports that (just point <code class="language-plaintext highlighter-rouge">AGENT_PROMPT</code> at different files per container), but In practice, shared prompts often suffice for initial implementations. Agents typically self-organize effectively without specialized prompts.</p>

<p>This connects to the <a href="/2026/02/01/agent-skills-and-claude-lint/">agent skills philosophy</a>: the prompt shapes behavior. The harness just runs the loop.</p>

<h2 id="when-it-works-when-it-doesnt">When it works, when it doesn’t</h2>

<p>Agent swarms work best when the problem decomposes into independent sub-tasks—many distinct failing tests, different modules, separate components. Each agent picks a different piece, and parallelism is trivial.</p>

<p>They struggle when the problem is monolithic. Anthropic hit this when compiling the Linux kernel: every agent would find the same bug, fix it independently, and overwrite each other’s changes. Their solution was to use GCC as an oracle and randomly split compilation between GCC and their compiler, letting each agent work on different failing file subsets.</p>

<p>For testing work, the decomposition is usually natural. Different invariants to test. Different modules to fuzz. Different state-machine paths to explore. The swarm pattern fits.</p>

<h2 id="what-this-is-really-about">What this is really about</h2>

<p>claude-swarm is about 200 lines of shell. It’s not the point.</p>

<p>The point is that the agent-team pattern—N autonomous agents, shared codebase, no central control—is a genuine paradigm for how AI-assisted work can scale. It’s not about making one agent smarter. It’s about making many agents productive together, the same way you’d make a team of engineers productive: clear context, local ownership, shared truth in the repo.</p>

<p>The C compiler was the proof of concept. Fuzz testing is where I’m applying it.</p>

<h2 id="related-posts">Related posts</h2>

<ul>
  <li><a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a> (series index)</li>
  <li><a href="/2026/02/01/agent-skills-and-claude-lint/">Agent Skills and claude-lint</a></li>
  <li><a href="/2026/01/31/the-bugs-between-calls/">The Bugs Between Calls</a></li>
</ul>

<hr />

<p><strong>Next:</strong> <a href="/2026/02/15/testing-between-calls-with-agents/">Testing the Bugs Between Calls</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 2 of the Oracles, Traces, Triage series. On running multiple agents in parallel through git—no orchestrator, no message passing—and why the pattern matters more than the tool.]]></summary></entry><entry><title type="html">Agent Skills and claude-lint</title><link href="https://blog.nikosbaxevanis.com/2026/02/01/agent-skills-and-claude-lint/" rel="alternate" type="text/html" title="Agent Skills and claude-lint" /><published>2026-02-01T00:00:00+00:00</published><updated>2026-02-01T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/02/01/agent-skills-and-claude-lint</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/02/01/agent-skills-and-claude-lint/"><![CDATA[<p><em>This article is part of the Oracles, Traces, Triage <a href="/2026/01/25/oracles-traces-triage/">series</a>.</em></p>

<h2 id="the-temptation">The temptation</h2>

<p>The first thing most people do with a <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> file is write a recipe. Step 1, do this. Step 2, do that. If you see an error, run this command. Here’s a code block you can paste.</p>

<p>It works. For about a week. Then the codebase shifts, the recipe goes stale, and the model follows outdated instructions with the confidence of someone who doesn’t know they’re wrong.</p>

<p>I’ve seen this pattern before. It’s the same failure mode as over-specified test fixtures: the more you hard-code the steps, the more brittle the system becomes. The test passes for the wrong reasons. The agent succeeds for the wrong reasons.</p>

<h2 id="context-should-shape-reasoning-not-script-behavior">Context should shape reasoning, not script behavior</h2>

<p>This distinction proves crucial in practice. When <code class="language-plaintext highlighter-rouge">.claude/</code> directories emphasize workflows over norms, models tend to follow outdated instructions rigidly. When structured around principles and facts, models demonstrate greater adaptability to changing contexts.</p>

<p>Whether this constitutes “reasoning from principles” in a deep sense remains an open question. However, the resulting outputs consistently demonstrate improved quality and relevance.</p>

<p>Think about it from a testing perspective. A unit test that asserts <code class="language-plaintext highlighter-rouge">f(3) == 7</code> checks one input. A property that asserts <code class="language-plaintext highlighter-rouge">for all x: f(f_inverse(x)) == x</code> checks the <em>relationship</em>.</p>

<p>Change how <code class="language-plaintext highlighter-rouge">f</code> computes internally and the property still holds—it only cares that the roundtrip works. The hard-coded assertion breaks the moment the mapping shifts.</p>

<p>Same idea. A <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> that says “run <code class="language-plaintext highlighter-rouge">cargo test</code> after every change” is a hard-coded assertion. A <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> that says “all changes must pass the existing test suite” is a property. The model can figure out <em>how</em> to run the tests. What it needs from you is <em>what matters</em>.</p>

<h2 id="the-layers">The layers</h2>

<p>Over time, I’ve settled on a layered structure for <code class="language-plaintext highlighter-rouge">.claude/</code> directories:</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>What belongs</th>
      <th>What doesn’t</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CLAUDE.md</code></td>
      <td>Norms, facts, project conventions</td>
      <td>Workflow verbs (“step 1”, “then do”), code blocks</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">agents/*.md</code></td>
      <td>Perspective, values (≤120 lines)</td>
      <td>Procedures, code blocks</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">skills/*/SKILL.md</code></td>
      <td>Capabilities (≤500 lines)</td>
      <td>Success criteria, code blocks</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">references/*.md</code></td>
      <td>Playbooks, optional reference material</td>
      <td>Missing “optional” declaration</td>
    </tr>
  </tbody>
</table>

<p><strong>CLAUDE.md</strong> is the constitution. Short. Declarative. “This project uses Rust.” “Tests must pass before commits.” “Prefer explicit error handling over unwrap.” No instructions on <em>how</em> to do things—just <em>what matters</em>.</p>

<p><strong>Agents</strong> get a perspective. If you have a code-quality agent, it gets values like “favor readability over cleverness” and “flag any function longer than 40 lines.” It doesn’t get a checklist.</p>

<p><strong>Skills</strong> describe capabilities the model can use—not step-by-step procedures. A skill for “running fuzzers” says what the fuzzer does, what inputs it expects, what success looks like at a high level. It does <em>not</em> contain a bash script.</p>

<p><strong>References</strong> are the escape hatch. Sometimes you genuinely need a playbook—a deployment procedure, a migration guide. References hold those, but they must declare themselves as optional. The model should know these are reference material, not marching orders.</p>

<h2 id="claude-lint">claude-lint</h2>

<p>A Rust CLI tool called <a href="https://github.com/moodmosaic/claude-lint">claude-lint</a> helps enforce these patterns by checking <code class="language-plaintext highlighter-rouge">.claude/</code> directories for violations.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ claude-lint .claude
ok: .claude passes all checks

$ claude-lint /path/to/.claude
error: /path/to/.claude/CLAUDE.md: contains workflow verb 'step 1'
error: /path/to/.claude/skills/foo/SKILL.md: contains fenced code block
2 error(s)
</code></pre></div></div>

<p>It checks for:</p>

<ul>
  <li><strong>Workflow verbs</strong> in <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> (e.g., “step 1”, “then run”, “next, do”)</li>
  <li><strong>Code blocks</strong> where they don’t belong (everywhere except references)</li>
  <li><strong>Line limits</strong> on agents (≤120) and skills (≤500)</li>
  <li><strong>Missing “optional” declarations</strong> in reference files</li>
</ul>

<p>It’s deliberately strict. The point is not to make <code class="language-plaintext highlighter-rouge">.claude/</code> directories pleasant to read. The point is to keep them in the shape where I’ve seen the model produce the best results.</p>

<h2 id="why-this-matters-in-practice">Why this matters in practice</h2>

<p>Claude Code demonstrates this approach in practice, using structured context to explore edge cases, generate test harnesses, and reason about state-machine invariants. The quality of outputs correlates directly with the quality of provided context.</p>

<p>When I embed workflows, the model sticks to them—even when they’re wrong for the current situation. When I embed norms (“never skip precondition checks”, “all state transitions must be tested for idempotence”), I get output that adapts to whatever the model finds in the codebase.</p>

<p>Whether that’s “reasoning from norms” or just the model having more room to draw on its training, I can’t say for certain. What I can say is that the parallel to property-based testing feels right. Properties tell the system <em>what must hold</em>. The system figures out <em>how to check it</em>. Norms tell the model <em>what matters</em>. The model figures out <em>how to act on it</em>.</p>

<p>Same shape. I’ll take it.</p>

<h2 id="related-posts">Related posts</h2>

<ul>
  <li><a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a> (series index)</li>
  <li><a href="/2026/01/31/the-bugs-between-calls/">The Bugs Between Calls</a></li>
</ul>

<hr />

<p><strong>Next:</strong> <a href="/2026/02/08/agent-teams-and-claude-swarm/">Agent Teams and claude-swarm</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 1 of the Oracles, Traces, Triage series. On structuring .claude/ directories so the model adapts from norms instead of following stale scripts—and a Rust tool to enforce it.]]></summary></entry><entry><title type="html">The Bugs Between Calls</title><link href="https://blog.nikosbaxevanis.com/2026/01/31/the-bugs-between-calls/" rel="alternate" type="text/html" title="The Bugs Between Calls" /><published>2026-01-31T00:00:00+00:00</published><updated>2026-01-31T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/01/31/the-bugs-between-calls</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/01/31/the-bugs-between-calls/"><![CDATA[<p>Anthropic just showed how far property-based testing can go when you can express a property at a function boundary. Their agent <a href="https://red.anthropic.com/2026/property-based-testing/">generated Hypothesis tests for real-world libraries and validated/reported several bugs in NumPy, Pandas, and SciPy</a>.</p>

<p>One important gap is that, while we still see critical bugs in single calls (SSZ decoding, BLS edge cases), many of the most expensive recent failures live <em>between calls</em>.</p>

<p>In many cases, the protocol rules are fine; the failure is a valid-but-expensive trace that turns into a liveness incident under load.</p>

<p><strong>December 2025:</strong> Shortly after Fusaka activated (Dec 3, 2025), Prysm hit a resource-exhaustion path processing certain attestations, dropping network participation to ~75% and pushing voting participation as low as ~74.7% in some epochs—uncomfortably close to the 2/3 stake threshold required for finality. In this incident, attestations referencing a previous-epoch block root could trigger repeated state recreation, replay, and epoch-transition recomputation, exhausting node resources under load. (See the <a href="https://prysm.offchainlabs.com/docs/misc/mainnet-postmortems/">Prysm mainnet postmortems</a> for the primary write-up.)</p>

<p><strong>May 2023:</strong> Mainnet finality was delayed twice within ~24 hours (first ~4 epochs, then ~9). The trigger was valid old-target attestations that forced expensive beacon-state regeneration in some clients; diversity helped the chain recover without intervention. (Postmortem: <a href="https://medium.com/offchainlabs/post-mortem-report-ethereum-mainnet-finality-05-11-2023-95e271dfd8b2">Ethereum Mainnet Finality Incident (May 2023)</a>.)</p>

<p><strong>April 2023:</strong> Stacks hit a PoX-2 bug in <code class="language-plaintext highlighter-rouge">stack-increase</code> that impacted Stacking rewards for a cycle. The details are different, but the shape is familiar: stateful logic where correctness is about how an accounting state evolves over a sequence of actions, not a single call in isolation. I wasn’t at Stacks at the time, and I’m not claiming “I would have caught it” — I mention it because it’s a clean example of why tests that exercise <em>traces</em> (not just inputs) matter. (Thread: <a href="https://forum.stacks.org/t/a-bug-in-stacks-increase-call-is-impacting-stacking-rewards-this-cycle/14867">A bug in stacks-increase call is impacting Stacking rewards this cycle</a>.)</p>

<p>Each operation was valid. The sequence proved problematic only under load.</p>

<p>Many expensive bugs live in <em>sequences</em> that look fine individually.</p>

<h2 id="stateless-properties-and-where-they-stop">Stateless properties (and where they stop)</h2>

<p>Stateless properties shine when:</p>

<ul>
  <li>the function boundary is the correctness boundary</li>
  <li>behavior is local to a single invocation</li>
  <li>invariants don’t depend on history</li>
</ul>

<p>This covers a lot of “pure-ish” code: parsing, formatting, serialization, numerical edge cases.</p>

<p>But consensus software is not primarily pure functions.</p>

<h2 id="consensus-clients-are-state-machines">Consensus clients are state machines</h2>

<p>Ethereum’s consensus clients (e.g., <a href="https://github.com/sigp/lighthouse">Lighthouse</a>, Prysm, Teku, Grandine, Nimbus, Lodestar) implement a long-lived state machine:</p>

<ul>
  <li><strong>per-slot processing</strong> (slots advance, duties change, messages arrive out of order)</li>
  <li><strong>data availability</strong> (verifying that required data is available; evolving toward Data Availability Sampling via <a href="https://eips.ethereum.org/EIPS/eip-7594">PeerDAS</a>)</li>
  <li><strong>fork choice</strong> (multiple competing branches, attestation-weighted via LMD-GHOST)</li>
  <li><strong>finality</strong> (justified/finalized checkpoints that must only move forward)</li>
  <li><strong>storage and replay</strong> (idempotence, witness caching, pruning, reorgs)</li>
</ul>

<p>Correctness is rarely “the output of one function call”.
It’s “the system’s behavior over a trace”.</p>

<h2 id="examples-of-stateful-invariants-in-consensus-clients">Examples of stateful invariants in consensus clients</h2>

<p>Here are a few invariants that are naturally <em>history-dependent</em>:</p>

<ul>
  <li><strong>Finality is monotonic</strong>: the finalized checkpoint’s epoch must never decrease. (Finality can stall; it must not regress.)</li>
  <li><strong>Fork choice respects finality</strong>: once a checkpoint is finalized, the selected head must be a descendant of it. (Heads can reorg; finalized history cannot.)</li>
  <li><strong>Data Availability gates what validators can accept/vote for</strong>: A block header is not enough in 2026. Availability is enforced via fork-choice/voting rules: validators should only accept and vote for blocks once sufficient data availability has been verified (today: all blobs; with <a href="https://eips.ethereum.org/EIPS/eip-7594">PeerDAS</a>: sampling cells/columns). Fork choice can only safely give full weight to blocks that validators can legally vote for. Testing the transition from “pending availability” to “available and valid” is a classic stateful trace.</li>
  <li><strong>Stake-weighted participation (MaxEB)</strong>: Participation was always stake-weighted, but <a href="https://eips.ethereum.org/EIPS/eip-7251">MaxEB</a> makes the variance visible by raising the cap from 32 ETH to 2048 ETH. Not every validator will immediately sit at the cap, but stake weight per validator can now vary widely, so a bug that affects a handful of high-effective-balance validators can represent outsized stake impact.</li>
  <li><strong>Idempotent imports</strong>: importing the <em>same block</em> twice (same root) must not double-apply side effects (DB indexes, caches, votes, metrics, etc.).</li>
  <li><strong>Equivocations must be handled, not assumed away</strong>: you can see multiple distinct blocks for the same slot. A client shouldn’t panic or corrupt state just because reality is adversarial.</li>
  <li><strong>Epoch-boundary logic must not run twice</strong>: “do X once per epoch” bugs are classic state-machine failures when reorgs, retries, and partial persistence meet.</li>
</ul>

<p>If you try to phrase these as “(f(x)) preserves (P)”, you end up smuggling “history” into (x) until it stops being a useful boundary.</p>

<p>Take “finality is monotonic.” You might try:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>for all (old_finalized, new_finalized):
    process_block(...) implies new_finalized &gt;= old_finalized
</code></pre></div></div>

<p>But now <code class="language-plaintext highlighter-rouge">old_finalized</code> is part of the input. Where does it come from? You have to generate it. And to generate a <em>valid</em> old state, you need to know what sequence of blocks led there. You’ve just reinvented traces—badly.</p>

<p>The honest framing is: “after any valid sequence of operations, the finalized epoch never decreases”. That’s a property over traces, not over inputs.</p>

<h2 id="model-based-stateful-property-based-testing">Model-based, stateful property-based testing</h2>

<p>Stateful testing makes the history explicit:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>State --(Command)--&gt; State'
</code></pre></div></div>

<p>Instead of generating inputs for a single call, you generate <em>commands</em> and run them as a scenario.
The bug is often not in any single step, but in a <em>particular ordering</em> of steps.</p>

<p>This idea is old and battle-tested (<a href="https://hackage.haskell.org/package/quickcheck-state-machine">QuickCheck state machine testing</a>, <a href="https://github.com/hedgehogqa/haskell-hedgehog">Hedgehog</a>, <a href="https://crates.io/crates/proptest-state-machine">proptest-state-machine</a>), but to my knowledge still underused in many production systems.</p>

<p><a href="/2025/03/10/chaos-testing-stacks-node/">The same approach, built into madhouse-rs, caught a production bug in the Stacks blockchain</a> that traditional testing missed. A 533-line integration test failed to reproduce it. A chaotic command sequence succeeded.</p>

<p>Model-based, stateful testing has been applied successfully to production systems like the Stacks PoX contracts. The approach proved practical for ongoing use, helping catch issues that traditional testing methods missed and demonstrating the value of stateful property testing in complex consensus systems.</p>

<h2 id="a-minimal-rust-harness-the-boring-runner">A minimal Rust harness (the “boring runner”)</h2>

<p>The core trick is to keep the runner boring and put all the logic in commands.
This is the same shape that scales in practice.</p>

<h3 id="state-and-context">State and context</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">trait</span> <span class="n">State</span><span class="p">:</span> <span class="nn">std</span><span class="p">::</span><span class="nn">fmt</span><span class="p">::</span><span class="n">Debug</span> <span class="p">{}</span>

<span class="k">pub</span> <span class="k">trait</span> <span class="n">TestContext</span><span class="p">:</span> <span class="nn">std</span><span class="p">::</span><span class="nn">fmt</span><span class="p">::</span><span class="n">Debug</span> <span class="o">+</span> <span class="nb">Clone</span> <span class="p">{}</span>
</code></pre></div></div>

<p>For the examples below, assume an empty context:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Debug,</span> <span class="nd">Clone,</span> <span class="nd">Default)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">BeaconContext</span><span class="p">;</span>
<span class="k">impl</span> <span class="n">TestContext</span> <span class="k">for</span> <span class="n">BeaconContext</span> <span class="p">{}</span>
</code></pre></div></div>

<h3 id="commands">Commands</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">proptest</span><span class="p">::</span><span class="nn">prelude</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="nb">Arc</span><span class="p">;</span>

<span class="k">pub</span> <span class="k">trait</span> <span class="n">Command</span><span class="o">&lt;</span><span class="n">S</span><span class="p">:</span> <span class="n">State</span><span class="p">,</span> <span class="n">C</span><span class="p">:</span> <span class="n">TestContext</span><span class="o">&gt;</span><span class="p">:</span>
    <span class="nn">std</span><span class="p">::</span><span class="nn">fmt</span><span class="p">::</span><span class="n">Debug</span> <span class="o">+</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nb">Sync</span>
<span class="p">{</span>
    <span class="c1">// Precondition: is this command meaningful *now*.</span>
    <span class="k">fn</span> <span class="nf">check</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">S</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">bool</span><span class="p">;</span>

    <span class="c1">// Apply the transition and assert postconditions.</span>
    <span class="k">fn</span> <span class="nf">apply</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">S</span><span class="p">);</span>

    <span class="c1">// For debugging and shrunk traces.</span>
    <span class="k">fn</span> <span class="nf">label</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">String</span><span class="p">;</span>

    <span class="c1">// Generate commands.</span>
    <span class="k">fn</span> <span class="nf">build</span><span class="p">(</span><span class="n">ctx</span><span class="p">:</span> <span class="nb">Arc</span><span class="o">&lt;</span><span class="n">C</span><span class="o">&gt;</span><span class="p">)</span>
        <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Strategy</span><span class="o">&lt;</span><span class="n">Value</span> <span class="o">=</span> <span class="n">CommandWrapper</span><span class="o">&lt;</span><span class="n">S</span><span class="p">,</span> <span class="n">C</span><span class="o">&gt;&gt;</span>
    <span class="k">where</span>
        <span class="k">Self</span><span class="p">:</span> <span class="nb">Sized</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="wrapper-for-heterogeneous-sequences">Wrapper for heterogeneous sequences</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Clone)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">CommandWrapper</span><span class="o">&lt;</span><span class="n">S</span><span class="p">:</span> <span class="n">State</span><span class="p">,</span> <span class="n">C</span><span class="p">:</span> <span class="n">TestContext</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">command</span><span class="p">:</span> <span class="nb">Arc</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">Command</span><span class="o">&lt;</span><span class="n">S</span><span class="p">,</span> <span class="n">C</span><span class="o">&gt;&gt;</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span><span class="o">&lt;</span><span class="n">S</span><span class="p">:</span> <span class="n">State</span><span class="p">,</span> <span class="n">C</span><span class="p">:</span> <span class="n">TestContext</span><span class="o">&gt;</span> <span class="n">CommandWrapper</span><span class="o">&lt;</span><span class="n">S</span><span class="p">,</span> <span class="n">C</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="n">new</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">t</span><span class="p">:</span> <span class="n">T</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="k">Self</span>
    <span class="k">where</span>
        <span class="n">T</span><span class="p">:</span> <span class="n">Command</span><span class="o">&lt;</span><span class="n">S</span><span class="p">,</span> <span class="n">C</span><span class="o">&gt;</span> <span class="o">+</span> <span class="k">'static</span><span class="p">,</span>
    <span class="p">{</span>
        <span class="k">Self</span> <span class="p">{</span> <span class="n">command</span><span class="p">:</span> <span class="nn">Arc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">t</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="execution-loop">Execution loop</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="n">execute_commands</span><span class="o">&lt;</span><span class="n">S</span><span class="p">:</span> <span class="n">State</span><span class="p">,</span> <span class="n">C</span><span class="p">:</span> <span class="n">TestContext</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="n">commands</span><span class="p">:</span> <span class="o">&amp;</span><span class="p">[</span><span class="n">CommandWrapper</span><span class="o">&lt;</span><span class="n">S</span><span class="p">,</span> <span class="n">C</span><span class="o">&gt;</span><span class="p">],</span>
    <span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">S</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="n">cmd</span> <span class="k">in</span> <span class="n">commands</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">cmd</span><span class="py">.command</span><span class="nf">.check</span><span class="p">(</span><span class="n">state</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">cmd</span><span class="py">.command</span><span class="nf">.apply</span><span class="p">(</span><span class="n">state</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The point is <em>locality</em>: generation, preconditions, transition logic, and invariants live together.
That design choice is exactly the “data-open” side of <a href="/2025/03/25/expression-problem-in-practice/">the expression problem</a>, and it’s why these harnesses survive contact with real systems.</p>

<h2 id="a-consensus-client-flavored-example-with-correct-slot-semantics">A consensus-client-flavored example (with correct slot semantics)</h2>

<p>One easy trap is to assume “there is only one block per slot”.
In the spec there is one <em>proposer</em> per slot, but on the network you can see:</p>

<ul>
  <li>equivocations (two blocks for the same slot from the proposer)</li>
  <li>different views due to propagation delays</li>
  <li>reorgs that temporarily make a “worse” chain the head</li>
</ul>

<p>So a stateful invariant should not be “reject a second block at slot (s)”.
That’s not how fork choice works.</p>

<p>Instead, here’s a deliberately small example that matches real failure modes: <strong>idempotence by block root</strong>.
If a client re-imports the same block (same root), it must not double-apply side effects.</p>

<h3 id="model-state">Model state</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">collections</span><span class="p">::{</span><span class="n">HashMap</span><span class="p">,</span> <span class="n">HashSet</span><span class="p">};</span>

<span class="nd">#[derive(Debug,</span> <span class="nd">Default)]</span>
<span class="k">struct</span> <span class="n">BeaconModel</span> <span class="p">{</span>
    <span class="n">current_slot</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>

    <span class="c1">// Slot -&gt; set of known block roots at that slot (forks allowed).</span>
    <span class="n">known_by_slot</span><span class="p">:</span> <span class="n">HashMap</span><span class="o">&lt;</span><span class="nb">u64</span><span class="p">,</span> <span class="n">HashSet</span><span class="o">&lt;</span><span class="p">[</span><span class="nb">u8</span><span class="p">;</span> <span class="mi">32</span><span class="p">]</span><span class="o">&gt;&gt;</span><span class="p">,</span>

    <span class="c1">// In 2026, participation is stake-weighted (MaxEB / EIP-7251).</span>
    <span class="c1">// Total weight of unique blocks we've imported.</span>
    <span class="n">total_imported_weight</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>

    <span class="c1">// Track which block states are available to prevent the 2025 Prysm regression</span>
    <span class="c1">// (expensive state regeneration when validating attestations for uncached blocks).</span>
    <span class="n">state_cache</span><span class="p">:</span> <span class="n">HashSet</span><span class="o">&lt;</span><span class="p">[</span><span class="nb">u8</span><span class="p">;</span> <span class="mi">32</span><span class="p">]</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">impl</span> <span class="n">State</span> <span class="k">for</span> <span class="n">BeaconModel</span> <span class="p">{}</span>
</code></pre></div></div>

<h3 id="command-tick-time">Command: tick time</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Debug)]</span>
<span class="k">struct</span> <span class="n">TickSlot</span><span class="p">;</span>

<span class="k">impl</span> <span class="n">Command</span><span class="o">&lt;</span><span class="n">BeaconModel</span><span class="p">,</span> <span class="n">BeaconContext</span><span class="o">&gt;</span> <span class="k">for</span> <span class="n">TickSlot</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">check</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">BeaconModel</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">bool</span> <span class="p">{</span> <span class="k">true</span> <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">apply</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">BeaconModel</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">state</span><span class="py">.current_slot</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">label</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">String</span> <span class="p">{</span> <span class="s">"TICK_SLOT"</span><span class="nf">.to_string</span><span class="p">()</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="command-import-a-block-stake-weighted-duplicates-forbidden">Command: import a block (stake-weighted, duplicates forbidden)</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Debug)]</span>
<span class="k">struct</span> <span class="n">ImportBlock</span> <span class="p">{</span>
    <span class="n">slot</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">root</span><span class="p">:</span> <span class="p">[</span><span class="nb">u8</span><span class="p">;</span> <span class="mi">32</span><span class="p">],</span>
    <span class="n">weight</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Stake-weighted via MaxEB.</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Command</span><span class="o">&lt;</span><span class="n">BeaconModel</span><span class="p">,</span> <span class="n">BeaconContext</span><span class="o">&gt;</span> <span class="k">for</span> <span class="n">ImportBlock</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">check</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">BeaconModel</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">bool</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.slot</span> <span class="o">&lt;=</span> <span class="n">state</span><span class="py">.current_slot</span>
    <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">apply</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">BeaconModel</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">entry</span> <span class="o">=</span> <span class="n">state</span>
            <span class="py">.known_by_slot</span>
            <span class="nf">.entry</span><span class="p">(</span><span class="k">self</span><span class="py">.slot</span><span class="p">)</span>
            <span class="nf">.or_default</span><span class="p">();</span>
        <span class="k">let</span> <span class="n">is_new</span> <span class="o">=</span> <span class="n">entry</span><span class="nf">.insert</span><span class="p">(</span><span class="k">self</span><span class="py">.root</span><span class="p">);</span>

        <span class="c1">// This is the invariant: same root must not be "new" twice.</span>
        <span class="c1">// Stake-weighting means a duplicate root shouldn't double-count weight.</span>
        <span class="k">if</span> <span class="n">is_new</span> <span class="p">{</span>
            <span class="n">state</span><span class="py">.total_imported_weight</span> <span class="o">+=</span> <span class="k">self</span><span class="py">.weight</span><span class="p">;</span>
            <span class="n">state</span><span class="py">.state_cache</span><span class="nf">.insert</span><span class="p">(</span><span class="k">self</span><span class="py">.root</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">label</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">String</span> <span class="p">{</span>
        <span class="nd">format!</span><span class="p">(</span><span class="s">"IMPORT_BLOCK(slot={}, weight={})"</span><span class="p">,</span> <span class="k">self</span><span class="py">.slot</span><span class="p">,</span> <span class="k">self</span><span class="py">.weight</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// The bug from December 2025: attestations for stale blocks </span>
<span class="c1">// triggering expensive state regeneration.</span>
<span class="nd">#[derive(Debug)]</span>
<span class="k">struct</span> <span class="n">ProcessAttestation</span> <span class="p">{</span>
    <span class="n">slot</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">block_root</span><span class="p">:</span> <span class="p">[</span><span class="nb">u8</span><span class="p">;</span> <span class="mi">32</span><span class="p">],</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Command</span><span class="o">&lt;</span><span class="n">BeaconModel</span><span class="p">,</span> <span class="n">BeaconContext</span><span class="o">&gt;</span> <span class="k">for</span> <span class="n">ProcessAttestation</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">check</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">BeaconModel</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">bool</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.slot</span> <span class="o">&lt;=</span> <span class="n">state</span><span class="py">.current_slot</span>
    <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">apply</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">BeaconModel</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Invariant: looking up state for an attestation must not </span>
        <span class="c1">// cause a "miss" that triggers an expensive re-play.</span>
        <span class="nd">assert!</span><span class="p">(</span>
            <span class="n">state</span><span class="py">.state_cache</span><span class="nf">.contains</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.block_root</span><span class="p">),</span>
            <span class="s">"State cache miss for block {:?} at slot {}"</span><span class="p">,</span> 
            <span class="k">self</span><span class="py">.block_root</span><span class="p">,</span> <span class="k">self</span><span class="py">.slot</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">label</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">String</span> <span class="p">{</span>
        <span class="nd">format!</span><span class="p">(</span><span class="s">"PROCESS_ATTESTATION(slot={})"</span><span class="p">,</span> <span class="k">self</span><span class="py">.slot</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If the implementation accidentally increments counters, updates indexes, or applies cached transitions twice on duplicate import, a failing trace usually shrinks to something like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
  TICK_SLOT,
  IMPORT_BLOCK(slot=1, root=R),
  IMPORT_BLOCK(slot=1, root=R),
]
</code></pre></div></div>

<p>If the system is not truly idempotent, stateful testing will reduce a complex failure down to the smallest possible sequence—often just “process the same thing twice”—making the bug obvious and undeniable.</p>

<p>That is the shape of a lot of consensus-client failures: not “wrong return value”, but “the second time through a path, something subtle breaks”.</p>

<h2 id="stateless--stateful-is-the-real-combination">Stateless + stateful is the real combination</h2>

<p>You want both:</p>

<ul>
  <li><strong>Stateless property-based testing (PBT)</strong> for pure-ish components: SSZ encoding, BLS wrappers, serialization, bitfields. This is where <a href="https://red.anthropic.com/2026/property-based-testing/">Anthropic’s approach</a> shines.</li>
  <li><strong>Stateful PBT</strong> for the hard parts: fork choice, finality logic, DB/replay, reorg handling, epoch boundaries.</li>
</ul>

<p>Stateless PBT finds bugs in the bricks (SSZ, BLS). Stateful PBT finds bugs in how the bricks stack—especially in the high-stakes world of PeerDAS and stake-weighted participation.</p>

<p>Anthropic showed us how to check the mortar really well. This post is about the wall.</p>

<h2 id="further-reading">Further Reading</h2>

<ul>
  <li><a href="https://eips.ethereum.org/EIPS/eip-7594">EIP-7594: Peer Data Availability Sampling (PeerDAS)</a></li>
  <li><a href="https://eips.ethereum.org/EIPS/eip-7251">EIP-7251: Increase the MAX_EFFECTIVE_BALANCE (MaxEB)</a></li>
  <li><a href="https://ethereum.github.io/consensus-specs/">Ethereum Consensus Specifications</a></li>
  <li><a href="https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/gasper/">Gasper: LMD-GHOST + Casper FFG</a></li>
  <li><a href="https://eth2book.info/latest/part2/consensus/lmd_ghost/">LMD GHOST Fork Choice (eth2book)</a></li>
  <li><a href="https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md">Ethereum Engine API Specification</a></li>
  <li><a href="https://clientdiversity.org/">Client Diversity Dashboard</a></li>
  <li><a href="https://prysm.offchainlabs.com/docs/misc/mainnet-postmortems/">Prysm Mainnet Postmortems (includes Fusaka incident)</a></li>
  <li><a href="https://medium.com/offchainlabs/post-mortem-report-ethereum-mainnet-finality-05-11-2023-95e271dfd8b2">Post-mortem: Ethereum Mainnet Finality Incident (May 2023)</a></li>
  <li><a href="https://forum.stacks.org/t/a-bug-in-stacks-increase-call-is-impacting-stacking-rewards-this-cycle/14867">Stacks PoX-2 <code class="language-plaintext highlighter-rouge">stack-increase</code> bug (April 2023)</a></li>
</ul>

<h2 id="related-posts">Related posts</h2>

<ul>
  <li><a href="/2024/12/01/model-based-stateful-testing-with-madhouse-rs/">Model-Based Stateful Testing with madhouse-rs</a></li>
  <li><a href="/2025/01/10/state-machine-testing-proptest">Model-Based Stateful Testing with proptest-state-machine</a></li>
  <li><a href="/2025/02/10/scaling-with-madhouse-rs">Scaling Model-Based Stateful Testing with madhouse-rs</a></li>
  <li><a href="/2025/03/25/expression-problem-in-practice">The Expression Problem in Practice: A Trait-Based Testing Harness</a></li>
  <li><a href="/2025/03/10/chaos-testing-stacks-node/">Chaos Testing stacks-node with Model-Based Stateful Testing</a></li>
  <li><a href="/2023/03/03/property-tests-vs-fuzzing/">Property Tests Are Not A Fuzzer</a></li>
  <li><a href="/2023/12/15/fuzzing-meets-property-testing/">Fuzzing meets property testing</a></li>
</ul>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Anthropic's recent work shows how far property-based testing can go when you can express properties at a function boundary. This follow-up argues that consensus clients need model-based, stateful testing to catch failures that only appear across sequences of events.]]></summary></entry><entry><title type="html">Oracles, Traces, Triage</title><link href="https://blog.nikosbaxevanis.com/2026/01/25/oracles-traces-triage/" rel="alternate" type="text/html" title="Oracles, Traces, Triage" /><published>2026-01-25T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/01/25/oracles-traces-triage</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/01/25/oracles-traces-triage/"><![CDATA[<p>In 2026, “agentic” tooling is moving fast enough that yesterday’s workflow advice goes stale quickly. This series is my attempt to write down the parts that seem durable: how to give agents norms instead of scripts, how to coordinate multiple agents through the repo, and how those patterns connect back to fuzzing and stateful testing.</p>

<h2 id="why">Why</h2>

<p>Skepticism is healthy here. Agent outputs still need oracles, reproducibility, and a bar for correctness. The tools are changing quickly; the craft is not.</p>

<p>Agents can draft tests fast; the hard part is still choosing the right oracles and insisting on reproducible failures.</p>

<p>Examples use <a href="https://docs.anthropic.com/en/docs/claude-code/overview">Claude Code</a> because that’s what I run day-to-day, but the patterns are meant to travel to any agent that can read a codebase, run checks, and write down findings.</p>

<p>This is not a tutorial. It’s a practitioner’s notebook.</p>

<h2 id="the-lens-oracles-traces-triage">The lens: oracles, traces, triage</h2>

<p>If there’s a unifying theme here, it’s that most bug-finding systems succeed or fail on three things:</p>

<ul>
  <li><strong>Oracles</strong> — how you decide something is wrong. Not just “did it crash?”, but invariants, spec checks, and properties that reflect what you actually care about.</li>
  <li><strong>Traces</strong> — many expensive bugs live in sequences, not calls. Stateful testing is about generating and shrinking traces until the failure is undeniable.</li>
  <li><strong>Triage</strong> — search produces noise. The work is making findings reproducible, minimal, deduped, and actionable (ideally as regression tests).</li>
</ul>

<p>That’s also a useful way to read the series:</p>

<ul>
  <li>Part 1 (skills/norms) mostly expands the <strong>oracle surface</strong> (“what matters”).</li>
  <li>Part 2 (agent teams) scales the search and improves the artifacts that enable <strong>triage</strong>.</li>
  <li>Part 3 (between calls/stateful) is explicitly about <strong>traces</strong>.</li>
  <li>Part 4 (agents as fuzzers) argues agents are search tools too—and the hard parts are still <strong>oracles + triage</strong>.</li>
  <li>Parts 5–8 cover the local model servers I set up for further agent
experiments, including their input limits, permissions, and recovery.</li>
</ul>

<h2 id="articles">Articles</h2>

<ul>
  <li><a href="/2026/02/01/agent-skills-and-claude-lint/">Part 1</a> - Agent Skills and claude-lint<br />
How to structure <code class="language-plaintext highlighter-rouge">.claude/</code> context so agents adapt from norms instead of blindly following scripts (plus a linter to keep it from drifting).</li>
  <li><a href="/2026/02/08/agent-teams-and-claude-swarm/">Part 2</a> - Agent Teams and claude-swarm<br />
A practical pattern for parallel agents that coordinate through git, with no orchestrator—because the repo can be the shared state.</li>
  <li><a href="/2026/02/15/testing-between-calls-with-agents/">Part 3</a> - Testing the Bugs Between Calls<br />
How agent skills, agent swarms, and stateful testing could combine to find consensus bugs that live in traces.</li>
  <li><a href="/2026/02/16/agents-as-fuzzers/">Part 4</a> - Agents as Fuzzers<br />
A structural analogy: both fuzzers and AI agents search for failures that require triage and oracles.</li>
</ul>

<h3 id="returning-to-the-series-running-the-models-locally">Returning to the series: running the models locally</h3>

<p>The February posts used Claude Code for the agent examples. In October,
I picked up the self-hosting work that was next in the outline. I ended up
using two Macs rather than Runpod. These four posts cover that setup and
its tests; comparing the local models on the bug-finding tasks is still
ahead.</p>

<ul>
  <li><a href="/2026/10/20/self-hosted-inference-on-apple-silicon/">Part 5</a> - Self-Hosted Inference on Apple Silicon<br />
Why I wanted local models for the agent experiments, and what it took
to run them on an M3 Ultra and an M5 Max.</li>
  <li><a href="/2026/10/21/a-context-window-is-a-testable-claim/">Part 6</a> - Testing Longer Contexts<br />
Moving beyond 8K defaults with measured inputs, continuation tests,
explicit rejection, and cold versus cached request timings.</li>
  <li><a href="/2026/10/22/local-models-and-tool-permissions/">Part 7</a> - Local Models and Tool Permissions<br />
Following a generated tool request through the runtime, agent harness,
filesystem permissions, and network boundary.</li>
  <li><a href="/2026/10/23/the-restore-is-the-test/">Part 8</a> - Restoring the Models on a Clean Mac<br />
An offline recovery library, a factory-reset M5, eight generation smoke
tests, and the limits of a quick SSD-copy verification.</li>
</ul>

<p>Parts 5–8 are retrospective entries dated October 20–23, assembled on
October 24. Later measurements and checks are identified in each article.
The <a href="/local-inference-operations/">operations reference</a> keeps the M3 and
M5 commands separate and links the supporting evidence summaries.</p>

<h2 id="companion-post">Companion post</h2>

<p>If you want the deeper motivation for “why traces,” start here:</p>

<ul>
  <li><a href="/2026/01/31/the-bugs-between-calls/">The Bugs Between Calls</a></li>
</ul>

<h2 id="next">Next</h2>

<ul>
  <li>Part 9 — Corpus, shrink, triage: turning agent output into a fuzzing pipeline<br />
How to dedupe/minimize failures and turn “agent finds” into reproducible bug packets and long-lived regression corpora.</li>
</ul>

<h2 id="background">Background</h2>

<p>Most of what I know about testing came from shipping production systems and learning in public through open source: contributing to <a href="https://github.com/AutoFixture/AutoFixture">AutoFixture</a> starting around 2011, then maintaining <a href="https://github.com/hedgehogqa">Hedgehog</a>, which once powered Echidna, an early and widely used property-based fuzzer for Ethereum smart contracts.</p>

<p>Along the way: <a href="https://github.com/moodmosaic/Fare">Fare</a> for regex-constrained test generation, a <a href="https://github.com/moodmosaic/splitmix">SplitMix</a> port for reproducible failure discovery. Consensus fuzzers at Stacks that <a href="/2025/03/10/chaos-testing-stacks-node/">caught a production bug</a> a 533-line integration test couldn’t reproduce.</p>

<p>That background is why I’m interested in AI tooling—not as a replacement for any of this, but as a way to do more of it.</p>

<h2 id="feedback">Feedback</h2>

<p>The ideas in this series come from daily practice—shipping agent-assisted testing tools for real protocol security work. But daily practice has blind spots.</p>

<p>If you think I’m wrong about something, I’d like to hear it. If you think I’m right but missing a nuance, I’d <em>especially</em> like to hear that.</p>

<hr />

<p><strong>Next:</strong> <a href="/2026/02/01/agent-skills-and-claude-lint/">Agent Skills and claude-lint</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[A series on agentic testing in 2026: how to build bug-finding systems that work because the oracles are real, traces are exercised, and findings are triaged.]]></summary></entry></feed>