<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>C Anthony Risinger</title>
<link>https://anthonyrisinger.com/tags/</link>
<atom:link href="https://anthonyrisinger.com/tags/index.xml" rel="self" type="application/rss+xml"/>
<description>Articles and notes by C Anthony Risinger, ordered by when the work and events happened.</description>
<image>
<url>https://anthonyrisinger.com/assets/skull.png?v=99ae14c3f6a0</url>
<title>C Anthony Risinger</title>
<link>https://anthonyrisinger.com/tags/</link>
</image>
<generator>quarto-1.10.18</generator>
<lastBuildDate>Wed, 23 Sep 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Base52 moment stamps</title>
  <link>https://anthonyrisinger.com/base52-stamps/</link>
  <description><![CDATA[ 





<p>Task identity in the <a href="../spice/">Spice</a> harness is a nine-character stamp <a href="https://github.com/infimalabs/spice/blob/e6d82f9207fcac28b9bb316b9295e0ca93f40547/spice/tasks/identity.py">minted from this alphabet</a>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1">ALPHABET <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0123456789BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz"</span></span></code></pre></div></div>
<p>Base52: the ten digits plus both cases of every letter except A, E, I, O, and U. The ordering and the omissions do different jobs.</p>
<p>The conventional vowels are dropped to make ordinary-word collisions rarer. These keys are embedded in model-generated prose and matched back out of it by the harness’s <a href="../ack-grammar/">ACK grammar</a>, so reducing the surface shared with English is useful. Fixed width and the surrounding ACK syntax identify the stamp.</p>
<p>The ordering is the other trick. Digits, then uppercase consonants, then lowercase is ASCII-monotonic—the alphabet’s rank order equals byte order—so a fixed-width, zero-padded encoding of epoch microseconds sorts lexicographically in time order under bytewise comparison. Python’s ordinary string order for these ASCII characters, SQLite’s <code>BINARY</code> collation, and <code>LC_ALL=C sort</code> agree on chronology without parsing anything. Locale-aware sorting need not.</p>
<p>Width and unit are one decision. A stamp of <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>w</mi><annotation encoding="application/x-tex">w</annotation></semantics></math> characters counting unit <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>u</mi><annotation encoding="application/x-tex">u</annotation></semantics></math> spans <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>52</mn><mi>w</mi></msup><mi>u</mi></mrow><annotation encoding="application/x-tex">52^w u</annotation></semantics></math> from the epoch. Each row below adds a character and a thousandfold finer unit, keeping about a nineteenth of the span above it:</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mtable><mtr><mtd columnalign="right" style="text-align: right; padding-right: 0"><msup><mn>52</mn><mn>8</mn></msup><mspace width="0.167em"></mspace><mtext mathvariant="normal">ms</mtext></mtd><mtd columnalign="left" style="text-align: left; padding-left: 0"><mo>≈</mo><mn>1694</mn><mspace width="0.222em"></mspace><mtext mathvariant="normal">years</mtext></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right; padding-right: 0"><msup><mn>52</mn><mn>9</mn></msup><mspace width="0.167em"></mspace><mtext mathvariant="normal">µs</mtext></mtd><mtd columnalign="left" style="text-align: left; padding-left: 0"><mo>≈</mo><mn>88.1</mn><mspace width="0.222em"></mspace><mtext mathvariant="normal">years</mtext></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right; padding-right: 0"><msup><mn>52</mn><mn>10</mn></msup><mspace width="0.167em"></mspace><mtext mathvariant="normal">ns</mtext></mtd><mtd columnalign="left" style="text-align: left; padding-left: 0"><mo>≈</mo><mn>4.58</mn><mspace width="0.222em"></mspace><mtext mathvariant="normal">years</mtext></mtd></mtr></mtable><annotation encoding="application/x-tex">
\begin{aligned}
52^{8}\,\text{ms} &amp;\approx 1694\ \text{years} \\
52^{9}\,\text{µs} &amp;\approx 88.1\ \text{years} \\
52^{10}\,\text{ns} &amp;\approx 4.58\ \text{years}
\end{aligned}
</annotation></semantics></math></p>
<p>The first character turns once per fifty-second of the span: every 32.6 years, 1.69 years, and 32 days. Eight characters of milliseconds reach 3664, and their first character reads <code>1</code> from 2002 to 2035. Ten characters of nanoseconds ran out in 1974. Nanoseconds need eleven characters, and Python’s <code>datetime</code> stops at microseconds. Ten characters of microseconds are the same nine behind a <code>0</code> until 2058.</p>
<p>Nine characters of microseconds reach February 2058. Their first character reads <code>d</code> from November 2025 to August 2027, and each character after it turns 52 times as often: about every 12 days, 5.5 hours, 6.3 minutes, 7.3 seconds, 141 ms, 2.7 ms, 52 µs, and 1 µs. A shared prefix is a shared span: stamps that agree on two characters were minted in the same 12 days, on three in the same 5.5 hours, on four in the same 6.3 minutes.</p>
<p>Stamps were eight characters of milliseconds until September 2026. The ninth character went to precision rather than range, which suits what the stamp is for: a stable name spread through the time it is actually used. At microseconds, every position turns at a useful rate. Eight-character stamps keep their spelling and decode as milliseconds. Bytewise order holds within one width, so comparisons across widths use the decoded instant.</p>
<p>Collisions advance the clock rather than adding entropy:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> mint_incepted(existing: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>[<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>:</span>
<span id="cb2-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Fresh nine-character stamp, advanced one microsecond past collisions."""</span></span>
<span id="cb2-3">    ...</span>
<span id="cb2-4">    micros <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> epoch_micros()</span>
<span id="cb2-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>:</span>
<span id="cb2-6">        stamp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> encode_width(micros)</span>
<span id="cb2-7">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> stamp <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> existing:</span>
<span id="cb2-8">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> stamp</span>
<span id="cb2-9">        micros <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span></code></pre></div></div>
<p>For serialized callers sharing an up-to-date <code>existing</code> set, two tasks minted in the same microsecond get adjacent stamps: still sorted, unique inside that set, and each stamp remains a pure function of one integer. No random suffix, no second identity component to reason about. This is local collision avoidance, not atomic allocation: two processes can read the same rows and the same microsecond, then mint the same value before either persists it. Fixed width is enforced as an error rather than a widening—an oversized value would silently break the sort property, so it refuses.</p>
<p>The rendered handle is <code>KEY-INCEPTED</code>, and only the stamp is stored. <code>KEY</code> derives from the current project’s rightmost segment at render time, so re-homing a task renames it for free; resolution matches on the stamp alone. The human-friendly part of the name is a projection, never data.</p>



 ]]></description>
  <category>agents</category>
  <category>python</category>
  <guid>https://anthonyrisinger.com/base52-stamps/</guid>
  <pubDate>Wed, 23 Sep 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Taekwondo with my daughter</title>
  <link>https://anthonyrisinger.com/blue-belt-yellow-belt/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="medal.webp" class="lightbox" data-gallery="photos" title="Yellow belt, silver medal."><img src="https://anthonyrisinger.com/blue-belt-yellow-belt/medal.webp?v=406b8d7aa0ac" class="ar-photo img-fluid figure-img" alt="Anthony in a white dobok with a yellow belt and a silver medal on a patterned ribbon, standing in front of a tournament banner."></a></p>
<figcaption>Yellow belt, silver medal.</figcaption>
</figure>
</div>
<p>My daughter started in the junior taekwondo class. I was helping her with it, then joined the adult class about a year later.</p>
<p>Now we both practice. She is the blue belt; I am the yellow.</p>
<p>We have our own classes and our own progression. Being her dad did not give me a head start on the belts.</p>
<p>On September 19, 2026 we both competed at <a href="https://s5.goeshow.com/nmaa/WarriorLegacy/">The Warrior’s Legacy</a>, a National Martial Arts Alliance tournament in Eagan hosted by our own school’s <a href="https://legacyama.net/personnel/jena-bushey/">Senior Master Jena Bushey</a>. My daughter took first place in her division. I took second in sparring. The order held.</p>
<div class="ar-photos ar-photos-wide quarto-layout-panel" data-layout-ncol="2">
<div class="quarto-layout-row">
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="sparring.webp" class="lightbox" data-gallery="photos" title="My daughter sparring, guard up."><img src="https://anthonyrisinger.com/blue-belt-yellow-belt/sparring.webp?v=fea01c39569e" class="img-fluid figure-img" alt="A girl in a white dobok, blue belt, black headgear, chest protector, and sparring gloves, guard up on a blue mat, seen from behind and to the side; a judge sits at the left and other competitors stand along the back wall."></a></p>
<figcaption>My daughter sparring, guard up.</figcaption>
</figure>
</div>
</div>
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="mat.webp" class="lightbox" data-gallery="photos" title="On the mat."><img src="https://anthonyrisinger.com/blue-belt-yellow-belt/mat.webp?v=5d15e2e85af3" class="img-fluid figure-img" alt="A girl in a white dobok and blue belt standing barefoot on a blue mat with her back to the camera, the dobok lettered Taekwondo St. Paul MN, with competitors and spectators seated along the far wall."></a></p>
<figcaption>On the mat.</figcaption>
</figure>
</div>
</div>
</div>
</div>



 ]]></description>
  <category>family</category>
  <guid>https://anthonyrisinger.com/blue-belt-yellow-belt/</guid>
  <pubDate>Sat, 19 Sep 2026 00:00:00 GMT</pubDate>
  <media:content url="https://anthonyrisinger.com/blue-belt-yellow-belt/medal.webp?v=406b8d7aa0ac" medium="image" type="image/webp"/>
</item>
<item>
  <title>Deleting Duplicate Schemas</title>
  <link>https://anthonyrisinger.com/duplicate-schemas/</link>
  <description><![CDATA[ 





<p>The tag filter on this site keeps its state in the URL hash, and the first version of multi-select kept it twice: a single click wrote <code>#tag=gaming</code>, while a Cmd/Ctrl-click multi-select wrote a separate custom key holding one comma-joined value. The review question was:</p>
<blockquote class="blockquote">
<p>why also tag? why not e.g.&nbsp;<code>tag=abc[&amp;tag=…]</code></p>
</blockquote>
<p>The resolution was one key, repeated, so a single click’s <code>#tag=gaming</code> and a multi-select’s <code>#tag=gaming&amp;tag=infra</code> have the same shape at every count, needing one parser and one restore path.</p>
<p>That fix is the newest entry in a habit I can trace across years of my own repositories: two representations of the same truth appear, one of them gets deleted, and the survivor becomes the source everything else is derived from.</p>
<section id="one-ordered-import-path" class="level2">
<h2 class="anchored" data-anchor-id="one-ordered-import-path">One ordered import path</h2>
<p><a href="../settingsd/">settingsd</a> applied the rule to application settings, a place where a second schema tends to accrete—a layered config on disk for prod, qa, and staging, and a settings module mirroring it in code. settingsd deleted the mirror. <code>settings.d/</code> holds numbered Python fragments, and one call:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1">settings <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">__import__</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'settingsd'</span>).install(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">__name__</span>)</span></code></pre></div></div>
<p>assembles numbered fragments in one shared namespace and installs the result as <code>&lt;pkg&gt;.settings</code>. The fragments are not a source that settings get loaded <em>from</em>; executed in order, they are the settings, and the object every other import sees is their sum.</p>
<p>Deleting the duplicate did not delete the audit trail. The shared namespace <a href="https://github.com/anthonyrisinger/settingsd/blob/e95d6e3ee1629237d37e421ff5e37367a6e988fb/settingsd/base.py">records which fragment and directory assigned a setting</a>. It traces assignments of different objects to uppercase names, not in-place mutations. Within that boundary, “where did this setting come from” is answered by the assembly namespace rather than by a second registry kept alongside it.</p>
</section>
<section id="one-table-two-json-copies-three-consumers" class="level2">
<h2 class="anchored" data-anchor-id="one-table-two-json-copies-three-consumers">One table, two JSON copies, three consumers</h2>
<p><a href="../static-python/">zippy</a>, from the same stretch—a waf-based tool that built CPython plus a whole Python application into a single artifact—pushed the rule into a concrete early form: one ConfigSet table serving three unrelated consumers through two generated JSON copies.</p>
<p>Its <a href="https://github.com/anthonyrisinger/zippy/blob/1c5d34d89805c47188a18cfbe17cfc39a9cb4480/zippy/configset.py">ConfigSet writer</a> tees the same table into <code>build/config.json</code> and the install-tree <code>zippy.&lt;identifier&gt;.json</code>. The install copy doubles as the <a href="https://github.com/anthonyrisinger/zippy/blob/1c5d34d89805c47188a18cfbe17cfc39a9cb4480/zippy/task.py">landmark compiled into the interpreter</a> to find its Python home, and carries metadata the packed application reads back at runtime. Waf build state, <code>PYTHONHOME</code> discovery, runtime metadata—three consumers of one table. Two files exist, but neither is a separately maintained schema.</p>
</section>
<section id="where-duplication-stayed" class="level2">
<h2 class="anchored" data-anchor-id="where-duplication-stayed">Where duplication stayed</h2>
<p>The same pipeline marks the boundary of the habit. zippy wrote metadata 2.0—<code>pydist.json</code>, before the ecosystem settled—and then wrote the legacy formats right back out beside it: <a href="https://github.com/anthonyrisinger/zippy/blob/1c5d34d89805c47188a18cfbe17cfc39a9cb4480/zippy/apps.py"><code>METADATA</code> and <code>entry_points.txt</code></a>. The readers of those files belonged to other people’s tools. Deleting a duplicate schema is only safe when every reader is yours to change; when it isn’t, you write both and stay clear about which one is authoritative.</p>
</section>
<section id="one-file-three-agents" class="level2">
<h2 class="anchored" data-anchor-id="one-file-three-agents">One file, three agents</h2>
<p>My <a href="https://github.com/anthonyrisinger/.../blob/d096eb38873d71cc7b60505252fd8bb65b1fef91/.claude/CLAUDE.md">dotfiles</a> apply the rule to agent configuration. One protocol document, <code>AGENTS.md</code>, is served to three coding agents under each vendor’s expected filename:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb2-1">.claude/CLAUDE.md -&gt; ../AGENTS.md</span>
<span id="cb2-2">.codex/AGENTS.md  -&gt; ../AGENTS.md</span>
<span id="cb2-3">.gemini/GEMINI.md -&gt; ../AGENTS.md</span></code></pre></div></div>
<p>Three tools, three filename conventions, one source; the symlink layer is the entire adapter. Updating the document updates what every tool reads.</p>
</section>
<section id="one-walk-that-decides" class="level2">
<h2 class="anchored" data-anchor-id="one-walk-that-decides">One walk that decides</h2>
<p>The current statement of the rule is in <a href="../spice/">Spice</a>, the agent harness, where the schema at risk is not a file format but a procedure. Control tokens ride inside model prose, and a <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/mail/ackgrammar.py">single iterator</a> decides, line by line, whether a token is being displayed rather than issued—inside a code fence, under a <code>&gt;</code> quote, behind four-space indentation, or on a rendered source-context line. The doctrine is written on the iterator itself:</p>
<blockquote class="blockquote">
<p>Every reader that decides whether a line acts must share this walk, so a display and the supervisor cannot disagree about which lines are real.</p>
</blockquote>
<p>Even trimming blank lines off a segment is centralized. The comment explains:</p>
<blockquote class="blockquote">
<p>four spaces there is the difference between code being shown and a directive being issued.</p>
</blockquote>
<p>A second reader carrying its own copy of the walk would drift from the first, and drift here means a shown directive gets executed. The grammar around it has <a href="../ack-grammar/">its own note</a>; the point for this thread is why the walk exists exactly once.</p>


</section>

 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/duplicate-schemas/</guid>
  <pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Recovery Paths</title>
  <link>https://anthonyrisinger.com/recovery-paths/</link>
  <description><![CDATA[ 





<p>vps-lxc—my bash container manager, told in full in <a href="../containers-2010/">Containers in 2010</a>—registered compensation before its template build and upgrade paths took their risky steps. Each handler was prepended to an array, so entering the explicit fatal path ran the newest one first. SIGINT and SIGTERM routed into that same path:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">trap</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'vps_exception ${LINENO} fatal "caught SIGTERM"'</span> TERM</span>
<span id="cb1-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">trap</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'vps_exception ${LINENO} fatal "caught SIGINT"'</span> INT</span></code></pre></div></div>
<p>Ctrl-C halfway through a template build lands in the handler that deletes the half-built template. An explicitly routed pacman failure with <code>/dev</code>, <code>/sys</code>, and <code>/proc</code> bind-mounted into a template rootfs unmounts them before the rootfs is deleted, because the unmount handler was registered second and runs first. Unchecked shell failures could bypass the path; the handler stack was manual compensation, not language-level <code>try</code>/<code>finally</code>.</p>
<p>Over the years, that approach kept reappearing in other work: btrfs subvolumes, a vim plugin directory, a venv builder, a game-server fleet, and now a Python installer’s append-only receipts.</p>
<section id="boot-the-copy-never-the-original" class="level2">
<h2 class="anchored" data-anchor-id="boot-the-copy-never-the-original">Boot the copy, never the original</h2>
<p>The boot-time version is the btrfs hook I put on AUR, covered in <a href="../boot-rollback/">Never Boot the Real Root</a>. Its layout makes the risk land on copies from the start: <code>/__active</code>, a snapshot of the installed system, is the default boot target, and choosing an older snapshot at boot still does not boot it—the hook deletes any stale <code>/__rollback</code>, re-snapshots the choice there, and boots the new clone. <code>__active</code> survives and the chosen snapshot survives; the previous disposable rollback does not. The recovery path is not taken after damage. The layout arranges for the tree being risked to be replaceable.</p>
</section>
<section id="a-.old-beside-the-.new" class="level2">
<h2 class="anchored" data-anchor-id="a-.old-beside-the-.new">A .old beside the .new</h2>
<p>The humblest version is two files in my dotfiles:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb2-1">vim/autoload/plug.vim</span>
<span id="cb2-2">vim/autoload/plug.vim.old</span></code></pre></div></div>
<p>vim-plug, the plugin manager itself, is vendored—committed to <code>autoload/</code>, so a fresh machine bootstraps offline and pinned—and beside it sits its own previous version. Nearly every upgrade carried its predecessor along as a <code>.old</code> file: one step of reversibility, maintained by hand.</p>
</section>
<section id="rename-then-delete" class="level2">
<h2 class="anchored" data-anchor-id="rename-then-delete">Rename, then delete</h2>
<p>pyproject, an experiment in virtual environments, applied the instinct to teardown itself. Its universal <code>bin/activate</code> elects one venv builder with an atomic <code>mkdir</code>—<a href="../mkdir-mutex/">useful, but not a complete mutex</a>—and on an ordinary nonzero failure after that, attempts to move the half-built venv away before deleting it:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cleanup ()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$VIRTUAL_ENV</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">{</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$$</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">}</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-r</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$VIRTUAL_ENV</span>.<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$$</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span></span></code></pre></div></div>
<p>Rename first, delete second. When cleanup receives a nonzero status, the rename atomically removes the failed venv from the published name, and the slow <code>rm -r</code> grinds away on a name new runners will not resolve. The <a href="../mkdir-mutex/">activation note</a> covers the remaining defects: a signal could skip cleanup, and another runner could use the unfinished venv.</p>
</section>
<section id="hide-the-partial-verify-before-publish" class="level2">
<h2 class="anchored" data-anchor-id="hide-the-partial-verify-before-publish">Hide the partial; verify before publish</h2>
<p><a href="../snap-gs/">snap-gs</a>, the Go supervisor for a VR game-server fleet, carried the avoidance form of the instinct. The intended protocol wrote match data as <code>file.lock</code>, closed the gzip stream and file, attached the S3 metadata as an xattr, and only then renamed it to the public name. The co-designed watcher rewrote <code>foo.lock</code> events into Create of <code>foo</code>, so even the notification layer spoke in terms of the completed name. The two-party convention has <a href="../lock-rename/">its own note</a>.</p>
<p>The historical writer discarded the errors from its deferred finalization steps, so rename could still publish an incomplete result. The <a href="../xattr-s3/">failure analysis</a> is the missing half of the technique: ordering says what should happen; checked results decide whether publication is allowed.</p>
</section>
<section id="reversal-values-as-data" class="level2">
<h2 class="anchored" data-anchor-id="reversal-values-as-data">Reversal values as data</h2>
<p>The current end of the thread is the public Python implementation of <a href="../spice/">Spice</a>. Its <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/hooks/initplanning.py">initializer</a> first produces a side-effect-free ordered plan. Each operation carries the value and mode observed before the change, the value and mode Spice intends to generate, its scope, and an ownership digest. Apply stores each completed operation in <code>&lt;worktree-git-dir&gt;/.spice/init-receipt.jsonl</code>.</p>
<p>That <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/hooks/initplan.py">receipt</a> is an append-only fact log, not a status document rewritten after each step. Spice encodes and bounds a complete record before opening the file, then emits it with one unbuffered <code>write</code> to a regular file opened with <code>O_APPEND</code>. A changed repository approval becomes another record; earlier bytes remain intact.</p>
<p><a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/hooks/deinitplan.py"><code>spice init --unapply</code></a> reads those recorded operations in exact reverse order. It restores a file or Git value only while the live state still matches what Spice generated. If someone changed the file after initialization, unapply keeps their version and reports a <code>retained-diverged</code> residue instead of replacing it with the old bytes. If another worktree still owns a shared config setting, ownership transfers to that worktree rather than removing a dependency from under it.</p>
<p>The preview includes the current receipt digest. Apply recomputes it and refuses if the receipt changed between inspection and execution. The thing the operator approved is therefore one observed reversal plan, not a vague promise that uninstall will probably be safe.</p>
</section>
<section id="rollback-compensation-avoidance" class="level2">
<h2 class="anchored" data-anchor-id="rollback-compensation-avoidance">Rollback, compensation, avoidance</h2>
<p>Lined up, the six are not one idea but three. The handler stack and the virtualenv teardown are compensation: attempt to undo partial work in the required order. The boot scheme, the <code>.old</code> file, and the receipt are rollback: retain the observed prior state and restore it only while ownership is still clear. A correctly checked <code>.lock</code> rename is avoidance: keep the intermediate state hidden behind the <code>.lock</code> name until publication.</p>


</section>

 ]]></description>
  <category>shell</category>
  <category>linux</category>
  <guid>https://anthonyrisinger.com/recovery-paths/</guid>
  <pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Where CONTRIBUTORS.txt Ends Up</title>
  <link>https://anthonyrisinger.com/where-contributors-ends-up/</link>
  <description><![CDATA[ 





<p>For years I carried a one-line version of this story: because of distutils2, my name is technically in Android phones and some Cisco stuff. It is the kind of sentence you say once and then repeat for a decade without checking. I finally followed the files. The Cisco part had a concrete answer; the Android part was harder to establish.</p>
<section id="the-vehicle-was-distlib" class="level2">
<h2 class="anchored" data-anchor-id="the-vehicle-was-distlib">The vehicle was distlib</h2>
<p>Start with the name in the sentence. The recovered public distutils2 surfaces contain no commit, issue, or mention under my name. The surviving chain leads somewhere else.</p>
<p>The actual vehicle was <a href="https://github.com/pypa/distlib">distlib</a>, which grew out of the distutils2 effort—almost certainly where my memory picked up the wrong name. I filed a run of <a href="../distlib/">issues against it</a>, sent a pull request while the project still lived on Bitbucket—my own build tool’s commit log preserves the link, <code>bitbucket.org/pypa/distlib/pull-request/13/</code>—and landed a <a href="https://github.com/pypa/distlib/commit/8799681d9f">metadata patch</a>. The result is one line in the record: my name in CONTRIBUTORS.txt.</p>
<p>The issues came out of <a href="../static-python/">a static single-file Python build system</a> that used distlib as its resolver and metadata engine.</p>
</section>
<section id="the-cisco-half-confirmed" class="level2">
<h2 class="anchored" data-anchor-id="the-cisco-half-confirmed">The Cisco half: confirmed</h2>
<p>Cisco publishes license-compliance documents for its products, and one of them is titled <a href="https://www.cisco.com/content/dam/en_us/about/doing_business/open_source/docs/collectorless_hpna-201-1669098492.pdf">“Open Source Used In collectorless_hpna 2.0.1”</a>. It is a long PDF of licenses and attribution text for everything the product bundles. <code>- C. Anthony Risinger</code> appears in it four times, across three bundled versions of distlib.</p>
<p>CONTRIBUTORS.txt travels inside the distlib source distribution, and the Cisco notice retains it across three bundled versions. That accounts for the repeated name.</p>
</section>
<section id="the-android-route-i-checked" class="level2">
<h2 class="anchored" data-anchor-id="the-android-route-i-checked">The Android route I checked</h2>
<p>The Android theory was always the better story, and it was plausible. In 2012 I landed two patches in WebKit: a <a href="https://github.com/WebKit/WebKit/commit/ae14c3b5774069d1244695019c2ff1d981090907">GTK build fix</a>, reviewed by Philippe Normand, and a <a href="https://github.com/WebKit/WebKit/commit/c9f71cdc3e8e7548f06a47d33eff5f679a7d5fc9">GObject IDL parser change</a> reviewed by Kentaro Hara. Android’s WebView really was WebKit at the time. Code in WebKit ships in billions of devices. Therefore: my name is in Android phones.</p>
<p>That leap needed evidence. The Android <code>jb-mr2-release</code> tree I checked has a <a href="https://android.googlesource.com/platform/external/webkit/+/refs/heads/jb-mr2-release/Source/WebCore/ChangeLog">WebCore ChangeLog ending in 2011</a>, before those patches. It contains older copies of the GTK tooling, so finding a familiar filename there does not establish that my changes entered an Android build. The two patches themselves concern GTK build and binding machinery.</p>
<p>I could follow the credit into WebKitGTK ChangeLogs. I did not establish the corresponding route into an Android phone. That leaves the Android memory unconfirmed.</p>
</section>
<section id="code-and-credit-travel-differently" class="level2">
<h2 class="anchored" data-anchor-id="code-and-credit-travel-differently">Code and credit travel differently</h2>
<p>pip vendors distlib without CONTRIBUTORS.txt. Its vendoring process carries the code and leaves the name behind.</p>
<p>The Cisco notice kept the source distribution’s credits; pip’s vendored copy kept the code without them. A source contribution and a shipped attribution are different things to trace.</p>
<p>I had remembered Android phones and some Cisco equipment. The Cisco part held up; the Android part remains unconfirmed, and the library was distlib rather than distutils2.</p>


</section>

 ]]></description>
  <category>python</category>
  <guid>https://anthonyrisinger.com/where-contributors-ends-up/</guid>
  <pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>EOF after the parent disappears</title>
  <link>https://anthonyrisinger.com/parent-exit-pipe/</link>
  <description><![CDATA[ 





<p>A Python wrapper can clean up a child in <code>finally</code> only if Python gets to run that block. An abrupt parent exit can leave the command’s processes alive and their inherited pipes open.</p>
<p>In <a href="../spice/">Python Spice</a>’s <a href="https://github.com/infimalabs/spice/commit/7c1765ece0776cdabed38b99c89abc2e7f803be1">process wrapper</a>, the POSIX path starts the command in a new process group and a small shell guardian in a separate session. The guardian waits on a pipe whose writer belongs to the Python parent.</p>
<p>Normal completion writes a newline-terminated marker. A successful read disarms the guardian. If the parent disappears and the pipe reaches EOF without that marker, the guardian sends <code>TERM</code> to the command group, waits a quarter second, and follows with <code>KILL</code> if the first signal succeeded.</p>
<p>The normal wait adds no polling loop to Python. The pipe’s lifetime carries the signal. A separate executable also avoids running a custom Python child after forking a process that may already have threads.</p>
<p>The group matters because its leader can exit before descendants that still hold output pipes. Cleanup targets the process group rather than assuming that a finished leader means all its work is finished. Reaping the direct child remains a separate obligation.</p>
<p>There are explicit limits. This guardian is the POSIX implementation; Windows retains a different cleanup path. If spawning the guardian fails, the helper returns an unarmed guard. Processes that leave the command group are not covered merely by being descendants. The guarantee belongs to the group and a successfully started observer.</p>
<p>The implementation also waits for or terminates the guardian when disarming it. A lifetime helper should not become the process the caller forgets to collect.</p>



 ]]></description>
  <category>python</category>
  <guid>https://anthonyrisinger.com/parent-exit-pipe/</guid>
  <pubDate>Mon, 17 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>A read that rewrites the index</title>
  <link>https://anthonyrisinger.com/git-optional-locks/</link>
  <description><![CDATA[ 





<p>A command asking Git about a worktree can refresh index metadata as a side effect. That can replace the index file even when the user has not changed the staged content.</p>
<p>This mattered when a <a href="../spice/">Spice</a> hook was validating the index generation while another internal probe inspected the repository. The observation itself could move the artifact being checked.</p>
<p>The <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/process/groups.py">shared subprocess boundary</a> sets an environment policy for Python-managed Git children:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1">child_env[GIT_OPTIONAL_LOCKS_ENV] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0"</span></span></code></pre></div></div>
<p>It copies the child’s environment before setting the value. It does not modify the parent process’s environment or the user’s shell configuration. The command’s executable basename identifies the Git children to which the policy applies.</p>
<p>This disables optional locking work such as an opportunistic index refresh. It does not remove the mandatory locks needed by actual Git mutations. The scope is every internal Git child passing through this boundary, not a list of commands guessed to be read-only.</p>
<p>The distinction is useful whenever observation and mutation run concurrently. “This command does not change my files” is weaker than “this command cannot change the identity of an artifact another process is validating.” The index is maintained state even when the visible question is only about status.</p>



 ]]></description>
  <category>git</category>
  <guid>https://anthonyrisinger.com/git-optional-locks/</guid>
  <pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The hook’s old value</title>
  <link>https://anthonyrisinger.com/ref-transaction-tip/</link>
  <description><![CDATA[ 





<p>Git’s reference-transaction hook receives lines with an old object, a new object, and a reference name. It is tempting to read the old field as a snapshot of what the reference currently contains.</p>
<p><a href="../spice/">Spice</a>’s <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/hooks/refguard.py">checked-out-branch guard</a> cannot make that assumption. A caller can update a ref without declaring the old value. The hook input can then contain the zero object ID, even though the ref already exists. A rewind can look like creation if the guard trusts that field.</p>
<p>During the <code>prepared</code> stage, the reference still holds its pre-transaction value. The guard resolves it from the repository before deciding whether the proposed value would abandon commits already shared with the branch’s upstream.</p>
<p>The parsed update deliberately retains only <code>new</code> and <code>ref</code>. It still requires the three-field input grammar, but does not carry the misleading old value farther into the decision.</p>
<p>The upstream also needs an exact source. Spice’s per-process Git configuration can redirect apparent tracking information, so this guard reads the branch’s pairing from local repository configuration instead of trusting a shadowed <code>@{upstream}</code>. It does not assume the remote is named <code>origin</code> or the branch is named <code>main</code>.</p>
<p>This is a guard for the current checked-out branch, not a universal policy for every reference transaction. Appends that retain the old tip can proceed; a rewind requiring an upstream judgment cannot silently pass when the declared upstream cannot be resolved.</p>
<p>The <a href="https://github.com/infimalabs/spice/commit/0260af4f08883fb69a8c22002b29a080dd59b58c">repair</a> made those evidence sources explicit. The hook input describes the requested transaction. The repository supplies the state against which this particular guard has to judge it.</p>



 ]]></description>
  <category>git</category>
  <guid>https://anthonyrisinger.com/ref-transaction-tip/</guid>
  <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Proving a database disposable</title>
  <link>https://anthonyrisinger.com/disposable-projections/</link>
  <description><![CDATA[ 





<p>Calling a database a cache does not make it safe to delete. The claim depends on where its facts came from and whether those sources still exist.</p>
<p><a href="../spice/">Python Spice</a> moved replayable Serve observations into a separate <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/serve/team/projection.py">projection store</a>. Durable team and directive authority stayed elsewhere. A corrupt, missing, or incompatible projection could then be discarded without discarding the decisions it was displaying.</p>
<p>A rebuild populated a fresh staging database first. Readers could retain the previous servable generation while that work ran. Only after population finished did the implementation copy the complete family’s rows into the published database inside one transaction, together with its generation and status.</p>
<p>That is an atomic row publication, not a filesystem rename of the live database. A failed staging callback never copied partial rows into the live family. The status remained explicitly stale if older data was still servable, or unavailable if there was no usable prior generation.</p>
<p>The <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/tests/test_serveprojectionparity.py">integration tests</a> seed authority and observation data, discard the projection, rebuild it, and compare the resulting combined view. Rebuilding must not reinterpret durable decisions as though they were merely observations that could be recreated from whatever transcript happens to remain.</p>
<p>Retention matters. Transcript-derived activity can be rebuilt from retained transcript bytes; deleting the source limits what any rebuild can recover.</p>
<p>The separation made failure handling simpler because the ownership was specific. This file could be recreated. The files carrying authority could not be treated the same way.</p>



 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/disposable-projections/</guid>
  <pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The wake file is not the store</title>
  <link>https://anthonyrisinger.com/store-witnesses/</link>
  <description><![CDATA[ 





<p><a href="../spice/">Spice</a> cached an observed task board by its logical revision. The revision lived in a wake file; the task rows lived in another file. Replacing the store while leaving the wake file alone could make the old cached rows look current.</p>
<p>The <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/serve/taskboard.py">task-board observation code</a> therefore pairs the logical revision with a store generation derived from device, inode, and change time. Between observations, a different generation requires another read even if the logical revision has not advanced.</p>
<p>The coherence check <em>during</em> an export needs a different physical witness. Exporting through the backend can itself write the store. Including change time in that comparison would make an ordinary successful read appear to have crossed a replacement. Across the export, the code compares device and inode instead.</p>
<p>Both witnesses come from one stat result at each boundary. Taking separate stats for the fields could combine evidence from opposite sides of a file replacement and call it one observation.</p>
<p>These witnesses have explicit limits. An inode can be reused, which is why change time helps between observations. Coarse timestamps can still collide. The across-export identity check cannot detect every unlink-and-recreate case that receives the same inode during that single interval.</p>
<p>The two comparisons serve different purposes: deciding when to rebuild a cached board, and checking whether an export crossed a store replacement. Both work alongside the logical wake revision.</p>



 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/store-witnesses/</guid>
  <pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The two-offset contract</title>
  <link>https://anthonyrisinger.com/two-offset-tailing/</link>
  <description><![CDATA[ 





<p>A live writer can flush one JSONL record in several writes. A follower that treats each read as complete input may try to decode half a record, then resume in the middle of it. The reader needs to retain the unfinished tail or return to its beginning on the next pass.</p>
<p>The fix, from the <a href="../spice/">Spice</a> harness’s <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/transcript/reader.py">transcript reader</a>, is to report two offsets for each pass:</p>
<ul>
<li><code>end_offset</code>—where reading stopped. This may sit inside a half-written record, and that is fine, because its consumer is forensics: a static or bounded read can still show the partial tail. A live cursor must never resume from it.</li>
<li><code>resume_offset</code>—the byte after the last <em>terminated</em> line. This is the only offset a resuming reader may use. The two differ only when a pass delivered an unterminated final line.</li>
</ul>
<p>Forward reads for live cursors go further and hold the unterminated tail back entirely:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> hold_partial_end <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">and</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> raw.endswith(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">b"</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>):</span>
<span id="cb1-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># A writer mid-flush leaves its last line unterminated. Resuming</span></span>
<span id="cb1-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># after that prefix would decode it as garbage and then decode the</span></span>
<span id="cb1-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># rest of the same line as more garbage, so the complete record</span></span>
<span id="cb1-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># would never be seen. Leave it for the pass that finds it whole.</span></span>
<span id="cb1-6">    handle.seek(offset)</span>
<span id="cb1-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">break</span></span></code></pre></div></div>
<p>Two more rules complete the contract. Replacement is detected by filesystem identity—device and inode—not by size, because a rotated-and-recreated file can easily be larger than the old cursor position and a size check would happily read garbage from the middle of an unrelated file. Replacement (identity changed) and truncation (cursor offset beyond current file size) both reset the cursor to zero rather than guessing.</p>
<p>Spice uses this for live Claude and Codex transcripts. Separating bytes observed from complete records lets a read show the unfinished tail while keeping the next live read at a valid record boundary.</p>



 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/two-offset-tailing/</guid>
  <pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>When waiting begins</title>
  <link>https://anthonyrisinger.com/ready-age/</link>
  <description><![CDATA[ 





<p>A task created last week but blocked until now has not spent a week waiting for an allocator. A task ready since last week has. Fairness depends on dating the interval in the right state.</p>
<p>Spice’s <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/tasks/readiness.py">readiness code</a> stores <code>ready_at</code> for later transitions into READY. The timestamp belongs with the mutation that exposes the work. Writing it in a second command would leave a window where Serve could observe a newly ready task with its old age.</p>
<p>Dependency edits that leave readiness unchanged retain the existing origin. An unrelated modification must not reset a task’s wait. When completion of one task exposes dependents, the code prepares their transition stamps before the final blocker completes, rather than requiring a fallible write after that irreversible completion.</p>
<p>Those are two ordered writes, not one transaction. If the final completion fails, the dependents remain blocked with prepared timestamps. Retrying through the same completion path replaces those stamps before trying again. Completing the blocker outside that path can leave the earlier origin in place: the resolver trusts <code>ready_at</code>; it does not reconstruct when the blocker actually finished.</p>
<p>Initial readiness also has existing evidence: the native entry timestamp and any wait or scheduled horizon. The resolver uses the latest applicable origin when there is no explicit later-transition timestamp. It does not substitute the current process’s startup time.</p>
<p>That keeps queue age stable across Serve restarts. A malformed timestamp or a ready row with no usable origin is an error; silently using “now” would make old waiting work look newly eligible.</p>
<p>The wider <a href="../spice/">allocator</a> has other ranking inputs. This note concerns only its clock: before using age to prevent starvation, establish which state is aging and make the transition into that state durable.</p>



 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/ready-age/</guid>
  <pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The three Git roots</title>
  <link>https://anthonyrisinger.com/hooks-common-dir/</link>
  <description><![CDATA[ 





<p>In a linked worktree, <code>.git</code> is a file containing a <code>gitdir:</code> pointer, usually into the main repository’s <code>.git/worktrees/&lt;name&gt;/</code>. Following that pointer does not reveal one canonical place for repository state. It reveals three places with different owners:</p>
<ul>
<li><code>git rev-parse --git-common-dir</code> finds the state shared by every worktree: objects, refs, the main config file, and repository-wide tool state.</li>
<li><code>git rev-parse --git-dir</code> finds this worktree’s HEAD, index, <code>config.worktree</code>, and operator receipt.</li>
<li><code>git rev-parse --show-toplevel</code> finds files meant to be visible from this checkout, including generated hook shims and live steering inputs.</li>
</ul>
<p>Without a configured total-state backend, the current Python implementation of <a href="../spice/">Spice</a> uses all three. Its <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/paths.py">path resolver</a> puts shared managed state under <code>&lt;git-common-dir&gt;/.spice</code>, durable worktree-local state under <code>&lt;git-dir&gt;/.spice</code>, and mutable visible state under <code>&lt;worktree&gt;/.spice</code>.</p>
<p>Hook installation belongs to the third root. The <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/hooks/initplanning.py">initializer</a> plans this hook-selection spine in order:</p>
<ol type="1">
<li>enable <code>extensions.worktreeConfig</code> in the common config;</li>
<li>record <code>core.bare=false</code> in this worktree’s config;</li>
<li>write executable shims under <code>&lt;worktree&gt;/.spice/hooks/</code>;</li>
<li>set this worktree’s <code>core.hooksPath</code> to <code>.spice/hooks</code>.</li>
</ol>
<p>The hook files exist before Git is pointed at them, and one worktree can be initialized or reversed without silently changing which hooks another worktree runs. The one shared mutation, enabling worktree config, is tracked separately from the worktree-local selection that depends on it.</p>
<p>This corrected an earlier design of mine that put the hook directory under the common Git dir and selected it through shared local config. That design handled the <code>.git</code> indirection but assigned a worktree choice to repository-wide state. It solved the path problem and missed the ownership problem.</p>
<p>The portable rule is not “always use the common dir.” Ask which scope owns the fact, then resolve that scope through Git. Objects and refs are common. HEAD, the index, worktree config, and installation receipts are not. Files an operator should see in a checkout may belong in the worktree itself. <code>.git</code> is not one directory, and “the real Git directory” is usually the wrong question.</p>



 ]]></description>
  <category>git</category>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/hooks-common-dir/</guid>
  <pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The sqlite3 close trap</title>
  <link>https://anthonyrisinger.com/sqlite-close/</link>
  <description><![CDATA[ 





<p>Python’s <code>sqlite3.Connection</code> context manager handles a transaction: it commits on success and rolls back on failure. It leaves the connection open. When the caller retains that connection, its file descriptor stays open until explicit close or finalization.</p>
<p>The <a href="https://github.com/infimalabs/spice/blob/8119859e21eaff5058274473eaa8b56a144eea3a/spice/sqliteconnection.py">fix</a>, from the <a href="../spice/">Spice</a> harness:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@contextmanager</span></span>
<span id="cb1-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> sqlite_connection(path, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>, busy_timeout_ms<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, wal<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>,</span>
<span id="cb1-3">                      ensure_parent<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>):</span>
<span id="cb1-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Yield a connection that commits on success and always closes."""</span></span>
<span id="cb1-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> ensure_parent:</span>
<span id="cb1-6">        Path(path).parent.mkdir(parents<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>, exist_ok<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb1-7">    connection <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sqlite3.<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">connect</span>(path)</span>
<span id="cb1-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">try</span>:</span>
<span id="cb1-9">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> busy_timeout_ms <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">is</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>:</span>
<span id="cb1-10">            connection.execute(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"PRAGMA busy_timeout = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(busy_timeout_ms)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb1-11">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> wal:</span>
<span id="cb1-12">            connection.execute(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PRAGMA journal_mode = WAL"</span>)</span>
<span id="cb1-13">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">yield</span> connection</span>
<span id="cb1-14">        connection.commit()</span>
<span id="cb1-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">except</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">BaseException</span>:</span>
<span id="cb1-16">        connection.rollback()</span>
<span id="cb1-17">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">raise</span></span>
<span id="cb1-18">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">finally</span>:</span>
<span id="cb1-19">        connection.close()</span></code></pre></div></div>
<p>Commit on success, attempt rollback on failure, and close in <code>finally</code>. <code>BaseException</code> rather than <code>Exception</code> includes a <code>KeyboardInterrupt</code> mid-transaction. A failed commit also enters the rollback path. If rollback itself raises, <code>finally</code> still attempts the close; if close raises, that error propagates. The helper guarantees the cleanup path, not successful cleanup or preservation of the original exception as the one the caller catches.</p>
<p>The same module avoids repeating schema setup on every open. <code>ensure_sqlite_schema_once</code> keeps retrying the schema initializer until the first successful commit, then skips it for that path for the rest of the process. That cache assumes one schema owner per path; it does not identify the file currently at that path. A double-checked lock keeps the cached path to one set-membership test when <code>validate</code> is absent. Callers that supply a validation callback deliberately open the database and run that check even on a cached path, catching a file replaced or advanced by another writer. The helper opens WAL so a reader and the single writer can proceed together. The path is recorded only after the DDL commits, so an initializer that raises leaves the next caller to try again rather than marking a database that was never built.</p>



 ]]></description>
  <category>python</category>
  <guid>https://anthonyrisinger.com/sqlite-close/</guid>
  <pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The ACK grammar</title>
  <link>https://anthonyrisinger.com/ack-grammar/</link>
  <description><![CDATA[ 





<p>The <a href="../spice/">Spice</a> harness steers agents without any API on the agent’s side. A steering item carries a durable key—a nine-character <a href="../base52-stamps/">base52 stamp</a>—and the item counts as handled only when the assistant’s reply contains an acknowledgment that echoes it:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb1-1">ACK dMbnngX3j: &lt;what changed or was captured&gt;</span></code></pre></div></div>
<p>The <a href="https://github.com/infimalabs/spice/blob/e6d82f9207fcac28b9bb316b9295e0ca93f40547/spice/mail/ackgrammar.py">header parser</a> requires <code>ACK</code> as a standalone ALL-CAPS token followed, in order, by one or more key-shaped tokens (optionally carrying a <code>-N</code> collision suffix). NACK is the same grammar with opposite sign, and both polarities are walked in source order. The hot path is a plain <code>"ACK" in text</code> substring check, so ACK-free replies cost one scan.</p>
<p>The grammar’s real work is tolerating markdown reality. Models bold their headers (<code>**ACK k:** ...</code>), decorate them as list items or headings, and pad them with filler (“ACK inbox key <code>dMbnngX3j</code>”). Each tolerance is an explicit consumption rule with stated widths—wrapper characters, key closers, separators, filler words—rather than a regex shrug, so every reader of the grammar parses the same header the same way.</p>
<p>Then two guards keep prose about the protocol from acting as the protocol. The use-versus-mention guard discards a candidate ACK that context shows is being talked about rather than issued: quoted or backticked, preceded by explicit negation, or sitting in a clause window with hypothetical words or narration words:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (</span>
<span id="cb2-2">    textcontext.has_explicit_negation_before(text, ack_pos)</span>
<span id="cb2-3">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">or</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">bool</span>(_ACK_HYPOTHETICAL_WORDS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>(recent))</span>
<span id="cb2-4">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">or</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">bool</span>(_ACK_NARRATION_WORDS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>(recent))</span>
<span id="cb2-5">)</span></code></pre></div></div>
<p>“I could write ACK here” does not acknowledge anything. Neither does an example, a phrase, or a saying—the wordlists are cheap and the failure class they close is real. A keyless <code>ACK:</code> shaped like a directive is detected too, as its own failure: said ACK, named no key.</p>
<p>The suppression walk is the second guard. One shared iterator decides, per line, whether a control token is <em>displayed</em> rather than issued—inside a code fence (fence character and size matched), a <code>&gt;</code> quote, 4-space indentation, or a rendered <code>path:line:</code> source-context line. Shown control tokens never act. The doctrine is stated at the trim helper, which refuses to strip indentation off a segment’s first content line:</p>
<blockquote class="blockquote">
<p>four spaces there is the difference between code being shown and a directive being issued</p>
</blockquote>
<p>Every reader must drop exactly the same lines, so the walk exists once.</p>
<p>The prose is the interface. There is no extra tool call on the agent side, but there is still a protocol: token, key shape, header tolerances, mention guards, and suppression rules. The shared parser makes its readers agree. It cannot prove that the work described after an ACK actually happened; that needs its own evidence.</p>



 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/ack-grammar/</guid>
  <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Spice Harness</title>
  <link>https://anthonyrisinger.com/spice/</link>
  <description><![CDATA[ 





<p>Spice grew out of <code>gritctl</code>, the development tools around <a href="../grit-paintball/">Grit Paintball</a>. The first version gathered the scripts for building, testing, running Unity, working with the Quest headset, and reading the resulting artifacts behind one command. The game had machinery for checking what it actually did. The agents working on it needed that too.</p>
<p>The whole reason it exists is to power Grit. It came out of a dumb loop: I was trying to squeeze as much out of Codex as I could while keeping a project with several languages, and VR in the middle of it, from becoming horrendously slow to develop or to ship. It let several agents share one machine and reuse pre-warmed test beds without exploding my disk. Mostly it is about managing entropy, keeping the project trending the way I want, because it cannot get there in one move and never without mistakes.</p>
<p>The session reader came next. It read the provider’s transcript so an agent could recover what it had been doing before compaction: the conversation, commands, failures, changed files, and commits. Then a mailbox let me leave steering for a particular worktree. The agent’s command wrapper exposed it on stderr before and after commands, preserving stdout for whatever the command was supposed to produce. A later version checked during long-running commands too. I could correct a running agent without rebuilding the conversation around every correction.</p>
<p>The browser initially put the assistant’s replies beside that mailbox. Speech made replies audible while work continued. Work ledgers, Taskwarrior, agent launching, and shared lanes grew around the same loop. By June I was pulling the coordination layer into a separate project. Grit kept its Unity, device, and runtime verification code; Spice could invoke those repository-owned commands without needing to understand the game.</p>
<p><a href="https://github.com/infimalabs/spice">Spice Harness</a> is the agent harness I built through <a href="https://infimalabs.com">Infima Labs</a>, my small personal business, to watch that work and steer it. The initials stand for Software Production, Integration, and Control Environment. It keeps the record of what happened separate from the instructions about what should happen next.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="spice.webp" class="lightbox" data-gallery="quarto-lightbox-gallery-1" title="Spice fleet operations console across active agent lanes."><img src="https://anthonyrisinger.com/spice/spice.webp?v=ac2ebdb3c470" class="img-fluid figure-img" alt="A dark fleet operations console with several agent lanes, live transcript cards, task controls, and a shared operator view."></a></p>
<figcaption>Spice fleet operations console across active agent lanes.</figcaption>
</figure>
</div>
<section id="delivery-is-not-acknowledgment" class="level2">
<h2 class="anchored" data-anchor-id="delivery-is-not-acknowledgment">Delivery is not acknowledgment</h2>
<p>The early mailbox archived an item after displaying it. That was too weak: the agent could read the correction, compact, and continue without answering it. I first added a watcher that put unacknowledged items back. The simpler rule was to leave them pending until the transcript contained an acknowledgment. Opening the mailbox no longer cleared it.</p>
<p>When steering is enabled, an instruction becomes a durable inbox item with a small key. Displaying it does not clear it. It counts as handled when the agent writes that key back in assistant prose with an ACK or NACK and enough substance to show how it interpreted the instruction. The grammar is small enough to test independently; <a href="../ack-grammar/">ACK grammar</a> covers the record, while <a href="../base52-stamps/">Base52 moment stamps</a> covers the sortable key.</p>
<p>The provider transcript records what occurred; durable instructions and task records hold what is being asked now. A pending correction survives a restart, and its acknowledgment points back to the transcript that contained it. That is evidence of an answer, not proof that the promised work happened. Even following the transcript needs a <a href="../two-offset-tailing/">separate rule for partial records</a>, so a restart cannot silently skip an unfinished line.</p>
</section>
<section id="allocate-the-graph-not-the-queue" class="level2">
<h2 class="anchored" data-anchor-id="allocate-the-graph-not-the-queue">Allocate the graph, not the queue</h2>
<p>The first <code>gritctl</code> allocator used Taskwarrior urgency, then spread agents across project and phase and preferred the smallest move from their previous work. Spice puts the dependency graph ahead of that locality policy.</p>
<p><code>spice task next</code> is the fleet’s work-selection boundary. A flat priority queue gets the obvious cases wrong: a low-priority prerequisite can be the only thing blocking critical work, and sending every available agent toward the same project-phase cell can increase contention while looking busy.</p>
<p>The allocator lets a prerequisite inherit the highest priority it transitively unblocks, then favors the candidate that releases more downstream work. Only after graph priority and urgency do peer spread and the cost of moving between project and phase break ties. Locality is useful, but it never outranks the graph.</p>
<p>Claims are leases, not permanent labels. Claimant, worktree, and lease land together in one guarded mutation. If another lane wins the race, allocation continues. A lapsed claim still requires an explicit takeover of the stale claim the allocator saw; it does not become free work merely because time passed. Renewal can carry a held claim to a successor session without releasing it.</p>
<p>Task phases also own the Git pressure. Claim, phase completion, and agent launch are the places that move Git state. Activation is deliberately not a fourth: it replays after compaction and would otherwise land Git work halfway through a live turn.</p>
</section>
<section id="one-authority-gets-to-move-a-lane" class="level2">
<h2 class="anchored" data-anchor-id="one-authority-gets-to-move-a-lane">One authority gets to move a lane</h2>
<p>The browser is a <a href="../disposable-projections/">projection</a>, not a scheduler. Opening or refreshing a lane cannot start an agent. One lifecycle authority serializes sends, wakes, renewals, and launches for each worktree, so two observers cannot each decide that the same stopped lane needs an occupant.</p>
<p>The lane is what I operate; the agent is its current occupant. A renewal may replace the thread while the stream, draft, filters, task, and team attribution stay with the lane.</p>
</section>
<section id="corrections-can-become-law-but-not-by-accident" class="level2">
<h2 class="anchored" data-anchor-id="corrections-can-become-law-but-not-by-accident">Corrections can become law, but not by accident</h2>
<p>I saw agents claiming to run test shards while repeatedly running one test that had grown to roughly twenty thousand lines. I asked them to split it. The result was a family of same-named test files with numbered suffixes. The correction became what I called the shard rule. I needed to keep it somewhere the next agent would inherit it.</p>
<p>Spice’s conscience watches for prose that may need one of those reminders. A maxim states an opinion and the words that should trigger a check. The trigger can return the reminder directly or ask judges whether the sampled statement violates the maxim. Ambiguous answers are retried within a bound and then abandoned, not interpreted generously.</p>
<p>A confirmed reminder returns as ordinary steering. Recurring acknowledged corrections can become proposed maxims, filed as tasks for human review. The harness can notice that the same lesson keeps being taught; it cannot promote its own taste into the constitution.</p>
<p>The constitution supplies pre-commit checks for rules that can be measured. Some size limits have a <strong>sticky-flex</strong> interval: a file may temporarily occupy the space above its base cap, but once it breaches the flex cap it must shrink below the base before the gate clears. Existing pressure can be tolerated without becoming permission to keep expanding.</p>
</section>
<section id="bounded-on-purpose" class="level2">
<h2 class="anchored" data-anchor-id="bounded-on-purpose">Bounded on purpose</h2>
<p>The entry ladder is <strong>watch → retrospect → gates → steer → fleet</strong>. <code>spice watch</code> can render existing Codex and Claude transcripts without taking control of their sessions. <code>spice session briefing</code> lets an agent recover the relevant part of its own record. Only a recurring need earns the next rung.</p>
<p><code>spice serve</code> is a single-operator console: a Python standard-library server, SQLite authority, loopback by default, and one shared token when explicitly reached beyond loopback. There are no accounts, user sessions, or per-user privilege channels. Remote reach for one operator belongs in transport—an SSH tunnel or tailnet bind—not in a gradual rewrite into a multi-tenant service.</p>
<p>Spice remains a work in progress, published as <a href="https://pypi.org/project/spice-harness/"><code>spice-harness</code></a> and exercised on its own development. I can start by observing one session, then add steering, shared work, and policy as I need them.</p>


</section>

 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/spice/</guid>
  <pubDate>Fri, 12 Jun 2026 00:00:00 GMT</pubDate>
  <media:content url="https://anthonyrisinger.com/spice/spice.webp?v=ac2ebdb3c470" medium="image" type="image/webp"/>
</item>
<item>
  <title>gotta</title>
  <link>https://anthonyrisinger.com/gotta/</link>
  <description><![CDATA[ 





<p>I built <a href="https://github.com/anthonyrisinger/gotta">gotta</a> because terminal work dies in scrollback, browser tabs, and compacted model context. The first failure is not retrieval. Usually I found the document. The failure is that the document, the path that found it, and the reason it mattered all vanish at different speeds.</p>
<p>I use it mostly at work. The usual starting point is one Slack or Jira link and perhaps a sentence of context. From there an agent can follow the links, materialize the ticket, discussion, document, or build it finds, and keep moving until the work is near a pull request. I have used it less outside work.</p>
<p>A typical evidence path might begin with a Jira issue that points to a Slack thread, which names a repository and a failed build. Gotta stores each result once, gives it a handle, and can reopen only the relevant section later. Its transcript search and <code>leads</code> view point to the next likely source without making the agent fetch and read the whole chain again. The session becomes the agent’s workspace across those systems without a separate MCP setup for each one.</p>
<p>The name is the command grammar. <code>gotta read</code>, <code>gotta search</code>, <code>gotta todo append</code>, and <code>gotta oops</code> read as sentences about what needs to happen, whether the next step is finding evidence or recording a mistake.</p>
<p>Early on, gotta ate its own oops: after a task ran, the friction it had recorded became the input for polishing the task. A daily run’s exhaust was the next day’s to-do list for the run itself. That loop moved into <a href="../spice/">Spice</a> with me.</p>
<section id="the-terminal-is-already-a-key" class="level2">
<h2 class="anchored" data-anchor-id="the-terminal-is-already-a-key">The terminal is already a key</h2>
<p>The tool is session-rooted rather than request-rooted. A request is ephemeral. A session can reopen, extend, hand off, and rehydrate after compaction without reconstructing the work from memory. That only helps if the next invocation can find the same session without requiring me to <code>cd</code> into a magic directory first.</p>
<p>The <a href="https://github.com/anthonyrisinger/gotta/blob/c520f6febfa1a55150813ca8364de7b8d23bc1ed/src/gotta/content/context.py">context resolver</a> prefers an explicit override or a stable host identifier, such as <code>CODEX_THREAD_ID</code> or <code>TERM_SESSION_ID</code>. It falls back to a hash of terminal facts when necessary. That fingerprint changes if enough of the environment changes; a real identifier is better when the host supplies one.</p>
<p>The binding holds a relative <code>root</code> symlink to the session and a JSON sidecar saying who made the binding, when, and why. Resolution follows the symlink. The JSON explains it. A shell can follow the pointer without parsing a schema, and the binding survives moving the data root.</p>
</section>
<section id="bytes-once-names-and-events-as-links" class="level2">
<h2 class="anchored" data-anchor-id="bytes-once-names-and-events-as-links">Bytes once, names and events as links</h2>
<p><code>gotta read</code> does more than print a remote result. It materializes the bytes into a content-addressed store:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb1-1">content/&lt;sha256&gt;/</span>
<span id="cb1-2">  data</span>
<span id="cb1-3">  meta.json</span>
<span id="cb1-4">  names/&lt;preferred-name&gt; -&gt; ../data</span>
<span id="cb1-5">  logs/&lt;timestamp&gt;       -&gt; ../names/&lt;preferred-name&gt;</span></code></pre></div></div>
<p>The <a href="https://github.com/anthonyrisinger/gotta/blob/c520f6febfa1a55150813ca8364de7b8d23bc1ed/src/gotta/content/filesystem.py">filesystem ledger</a> writes <code>data</code> once per digest. A human name is a symlink to those bytes. Each fetch event is another timestamped symlink to the name used for that fetch, so the tree records both <em>when</em> the artifact arrived and <em>what it was called</em> at the time. Fetching identical bytes again adds provenance without manufacturing a second object.</p>
<p>Two locators fall out of the layout: <code>artifact:&lt;name&gt;@&lt;digest12&gt;</code> for a human handle and <code>content:&lt;sha256&gt;</code> for exact identity. <code>manifest.jsonl</code> indexes the events for fast views, but it is not the authority. If the manifest is lost, the digest directories, metadata, aliases, and event links still describe the store. I wanted a cache that could explain itself by being walked.</p>
<p>The <code>timeline</code>, <code>graph</code>, and <code>leads</code> views derive from that evidence web. Gotta was built by agents for agents, and those views are theirs. I rarely run them myself. I step in when a view is not revealing something I thought it could, or when I catch an agent complaining about the tool.</p>
</section>
<section id="an-attached-pipe-is-not-input" class="level2">
<h2 class="anchored" data-anchor-id="an-attached-pipe-is-not-input">An attached pipe is not input</h2>
<p>Agent harnesses regularly launch a command with stdin attached even when no bytes are coming. The usual test, <code>not sys.stdin.isatty()</code>, then answers the wrong question. A bare <code>gotta oops</code> should show the friction ledger; piped prose should append to it. Treating an empty inherited pipe as prose turns a read into an empty write.</p>
<p>The <a href="https://github.com/anthonyrisinger/gotta/blob/c520f6febfa1a55150813ca8364de7b8d23bc1ed/src/gotta/content/context.py">stdin check</a> looks for readable bytes without waiting. If the descriptor cannot support the check, it does not infer a write. The command needs evidence of content, not merely a pipe attached to it.</p>
</section>
<section id="bound-the-answer-keep-the-evidence" class="level2">
<h2 class="anchored" data-anchor-id="bound-the-answer-keep-the-evidence">Bound the answer, keep the evidence</h2>
<p>Retrieval tools have an easy failure mode with models: return everything and consume the context that was supposed to reason about it. Gotta bounds interactive output by line count and byte size. Its <a href="https://github.com/anthonyrisinger/gotta/blob/c520f6febfa1a55150813ca8364de7b8d23bc1ed/src/gotta/dispatch/budget.py">cutoff code</a> allows a small flex band to find a clean stopping point without splitting a UTF-8 character.</p>
<p>JSON cannot be clipped like prose and remain JSON. Oversized JSON becomes a small envelope with a preview and a follow command. The full bytes are still in the artifact store; only the conversational view is bounded.</p>
<p>Stdout stays the requested payload. A compact machine receipt goes to stderr when something non-obvious happened: truncation facts, artifact and content locators, and a shell-quoted command to reopen the result or rerun with full output. The output can be small without making the operation forgetful.</p>
<p>Reading the <a href="https://github.com/anthonyrisinger/gotta/blob/c520f6febfa1a55150813ca8364de7b8d23bc1ed/README.md">public README</a> with <code>gotta read</code> and <code>--head 4</code> returns four lines on stdout. Its receipt includes <code>artifact:README.txt@2bc5c32d9c42</code>. The same session can then run:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gotta</span> read artifact:README.txt@2bc5c32d9c42 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--section</span> Installation</span></code></pre></div></div>
<p>That returns the installation section from the saved document. The first four lines do not contain that section, but the initial read keeps the whole document. Remote providers still need their own access configured; keeping evidence does not supply permission to retrieve it.</p>
</section>
<section id="record-the-seam" class="level2">
<h2 class="anchored" data-anchor-id="record-the-seam">Record the seam</h2>
<p><code>gotta oops</code> records what went wrong, how to reproduce it, and any workaround in <code>state/oops.jsonl</code>. Later invocations can reopen the same ledger and mark the problem resolved. The friction has somewhere to live after the agent that hit it has forgotten the conversation.</p>
<p>Gotta is Python and Apache-2.0, published on <a href="https://pypi.org/project/gotta/">PyPI</a>: <code>uv tool install gotta</code>.</p>


</section>

 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/gotta/</guid>
  <pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>nftables listed the wrong byte order</title>
  <link>https://anthonyrisinger.com/nft-byteorder/</link>
  <description><![CDATA[ 





<p>A mark value of <code>0x00000001</code> could appear as <code>0x01000000</code> in <code>nft list ruleset</code> even when the kernel was using the intended value. The bytes and the metadata explaining how to display those bytes had diverged.</p>
<p>My <a href="https://github.com/google/nftables/pull/355">patch</a> addressed two paths in the Go nftables library. Anonymous, constant, and interval sets could emit big-endian key metadata even when their key byte order had been explicitly set to native endian. Maps also lacked a way to emit the data byte-order metadata used for their values.</p>
<p>The first correction gave explicit native-endian keys precedence over the set-kind fallback. The second added <code>DataByteOrder</code> and emitted the corresponding userdata value for maps. The decoding path read both key and data byte-order fields back into the Go representation.</p>
<p>Those userdata fields told the <code>nft</code> userspace tool how to reconstruct a human-readable rule. They were not a replacement for the element bytes themselves. Changing the data to compensate for a misleading listing would have risked breaking the behavior that was already correct.</p>
<p>The patch included round-trip system tests for several set and map forms, including preserving the existing big-endian default when no explicit native-endian setting was supplied. The public report also describes the display symptoms for marks and interface-name maps.</p>
<p>The PR remains open as of September 21, 2026.</p>



 ]]></description>
  <category>linux</category>
  <category>go</category>
  <guid>https://anthonyrisinger.com/nft-byteorder/</guid>
  <pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Still on the Couch</title>
  <link>https://anthonyrisinger.com/still-on-the-couch/</link>
  <description><![CDATA[ 





<div class="ar-photos quarto-layout-panel" data-layout-ncol="2">
<div class="quarto-layout-row">
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="xbox-controller.webp" class="lightbox" data-gallery="photos" title="Xbox in the living room."><img src="https://anthonyrisinger.com/still-on-the-couch/xbox-controller.webp?v=ddfa40bde56b" class="img-fluid figure-img" alt="A blue and purple Xbox controller held in front of a living-room television, with another controller on the coffee table."></a></p>
<figcaption>Xbox in the living room.</figcaption>
</figure>
</div>
</div>
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="dont-starve.webp" class="lightbox" data-gallery="photos" title="Don’t Starve on the television, with an Xbox controller in hand."><img src="https://anthonyrisinger.com/still-on-the-couch/dont-starve.webp?v=d04303fca795" class="img-fluid figure-img" alt="A red Xbox controller in the foreground of a living room, with a snowy scene from Don’t Starve on the wall-mounted television."></a></p>
<figcaption>Don’t Starve on the television, with an Xbox controller in hand.</figcaption>
</figure>
</div>
</div>
</div>
</div>
<p>Couch co-op is big in our family. <em>SpiderHeck</em>, <em>Don’t Starve Together</em>, <em>BattleBlock Theater</em>, and plenty of others have been part of it. Xbox is still a big part of our house.</p>
<p>When we wanted several Minecraft players on one Linux box and one TV, I <a href="../couch-multiseat/">wrote a window manager</a> so each controller could have its own part of the screen.</p>
<p>My youngest has reached the point where I can barely beat her, if I beat her at all. We play <a href="https://www.spiderheck.com/">SpiderHeck</a> almost every day lately, and she seems to win most of the time even when her older brother is playing. He is already scary enough.</p>
<p>She caught me off guard years earlier. I let her play <em>Skyrim</em> when she was young enough that I did not expect her to get very far. I left the room for a little while. When I came back, she had found a powerful dragon and was kiting it through the window of what I remember as a church. It had about a quarter of its health left.</p>
<p>She finished it. I asked what she was going to do next. She collected the loot and said she was going to find the next one.</p>



 ]]></description>
  <category>family</category>
  <category>gaming</category>
  <guid>https://anthonyrisinger.com/still-on-the-couch/</guid>
  <pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate>
  <media:content url="https://anthonyrisinger.com/still-on-the-couch/xbox-controller.webp?v=ddfa40bde56b" medium="image" type="image/webp"/>
</item>
<item>
  <title>Where the Markdown stream stopped</title>
  <link>https://anthonyrisinger.com/streaming-markdown/</link>
  <description><![CDATA[ 





<p>I wanted to pipe arriving model output into Glow and see formatted text before the producer finished. I submitted a <a href="https://github.com/charmbracelet/glow/pull/823">streaming implementation</a> with a configurable buffer around its Markdown renderer.</p>
<p>The idea was to collect enough text to find a safe boundary, render that chunk, and continue. A line of ordinary prose looked manageable. Fences, tables, frontmatter, and whitespace made the word “safe” progressively more complicated.</p>
<section id="a-chunk-was-not-a-document" class="level2">
<h2 class="anchored" data-anchor-id="a-chunk-was-not-a-document">A chunk was not a document</h2>
<p>The new <code>flow</code> package tracked accumulated bytes, whether it was inside a fence, where a fence had closed, and whether frontmatter had been resolved. It offered aggressive, fully buffered, and windowed modes through <code>--flow</code>.</p>
<p>Those modes traded waiting against the amount of context available to the renderer. Calling a complete-document renderer repeatedly on partial documents meant the caller had to predict where document boundaries would not change the output.</p>
<p>One field, <code>pendingBlank</code>, held rendered blank lines until later output clarified whether they belonged between chunks or at the end. That state existed because whitespace decisions were being made by one layer and reconciled by another after rendering.</p>
<p>Fences made the limitation more visible. Buffering an entire code block could protect its structure, but it also meant a long block stopped appearing progressively. Streaming the interior required the renderer to understand that the next chunk continued the same block. A buffer outside the renderer could not supply that understanding merely by choosing a better byte count.</p>
<p>Reference-style links created another kind of future dependency: the definition needed to render an earlier reference might arrive near the end of the document. A finite window could not promise to see every possible definition in an arbitrarily long stream.</p>
</section>
<section id="the-tests-had-their-own-boundary" class="level2">
<h2 class="anchored" data-anchor-id="the-tests-had-their-own-boundary">The tests had their own boundary</h2>
<p>The patch grew well beyond the buffer itself, with extensive tests and documentation around it.</p>
<p>I spent time debugging tests whose input names ended in <code>.txt</code> instead of <code>.md</code>. Glow treated them differently, including automatic fence wrapping. The harness had varied more than the streaming behavior it was meant to compare.</p>
<p>A maintainer then tested a shell producer with delays between Markdown lines. Small flow values still mishandled syntax highlighting and a heading. The failure was visible in the exact use case the feature existed to serve, despite the extensive local test surface.</p>
<p>I also kept a separate <a href="https://gist.github.com/anthonyrisinger/dfcd8a534a974e38160b5d45cc0cc066">development tool</a> for generating Markdown shapes, simulating delivery modes, and inspecting rendered bytes and whitespace. That helped make disagreements concrete. It exposed differences in the rendered bytes, including whitespace that was difficult to distinguish by eye.</p>
</section>
<section id="what-became-of-it" class="level2">
<h2 class="anchored" data-anchor-id="what-became-of-it">What became of it</h2>
<p>The maintainers declined the approach. They had lower-level work planned that they expected would make streaming simpler, without a promised timeline. I agreed with the decision and said plainly that the version in the PR was still broken. I had obtained useful line-buffered behavior in other versions, but had not found an approach I was fully satisfied with. <code>bat</code> was adequate for my immediate use in the meantime.</p>
<p>The failure located the missing boundary. The renderer needed to carry information about the structure currently being rendered. The outer stream could then manage delivery and buffering without reconstructing that structure from fragments of input and finished output.</p>
<p>The PR closed without merging.</p>


</section>

 ]]></description>
  <category>agents</category>
  <guid>https://anthonyrisinger.com/streaming-markdown/</guid>
  <pubDate>Fri, 12 Sep 2025 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
