<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Jubaleth — Notebook</title>
    <link>https://jubaleth.wtf/notes/</link>
    <description>Small, durable observations from building and operating real systems—too focused for a full article, but useful enough to preserve.</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <atom:link href="https://jubaleth.wtf/notes/index.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Tue, 18 Aug 2026 15:12:00 &#43;0200</lastBuildDate><item>
      <title>What the Preview Token Actually Approves</title>
      <link>https://jubaleth.wtf/notes/2026-08/what-the-preview-token-actually-approves/</link>
      <pubDate>Tue, 18 Aug 2026 15:12:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/what-the-preview-token-actually-approves/</guid>
      <category>Ansible</category>
      <category>Semaphore</category>
      <category>VyOS</category>
      <category>Deployment</category>
      <description>A useful approval token has to bind the intended change, not the temporary directory which happened to render it.</description>
      <content:encoded>&lt;p&gt;I split network deployments into Preview and Apply jobs so the diff can be reviewed before anything writes to a router. The first version produced a cryptographic review token, but Preview and Apply disagreed even when the rendered configuration was identical.&lt;/p&gt;
&lt;p&gt;Semaphore checks the project out into a new task-local directory for every run. I had accidentally included that path in the token input. The token proved where the preview happened, not just what had been reviewed.&lt;/p&gt;
&lt;p&gt;The approval now binds stable facts: the source revision, selected hosts, rendered artifacts, semantic configuration changes and the relevant live state. A different checkout can reproduce it. A changed commit, changed candidate or changed device state cannot.&lt;/p&gt;
&lt;p&gt;VyOS then found a second version of the same mistake. One render proposed an empty configuration node. The text was different, so the automation expected a change; VyOS quite reasonably built the candidate and reported that there was nothing to commit. I removed the redundant node and added a regression check rather than teaching the deployment to distrust the router.&lt;/p&gt;
&lt;p&gt;That left me with a clearer rule for the workflow: the review must describe an operation the target system can actually perform. Temporary paths, formatting and empty containers are implementation noise. If they can change an approval token or manufacture a diff, the approval is attached to the wrong thing.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>The State File Was Right. The Route Was Wrong.</title>
      <link>https://jubaleth.wtf/notes/2026-08/the-state-file-was-right-the-route-was-wrong/</link>
      <pubDate>Tue, 18 Aug 2026 13:55:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/the-state-file-was-right-the-route-was-wrong/</guid>
      <category>Networking</category>
      <category>VyOS</category>
      <category>IPv6</category>
      <category>Failover</category>
      <description>The failover supervisor remembered the right state while the live routing tables had quietly stopped agreeing with it.</description>
      <content:encoded>&lt;p&gt;During a dual-WAN incident, the failover state file said exactly what I expected. The live route did not.&lt;/p&gt;
&lt;p&gt;That distinction matters because the state file is only the supervisor&amp;rsquo;s last decision. It is not proof that FRR still has the intended distances, that a later VyOS commit did not replace them, or that the kernel selected the interface implied by the state. A restart or partial transition can leave perfectly valid state attached to the wrong route.&lt;/p&gt;
&lt;p&gt;IPv6 added a less obvious constraint. The backup line learns its default gateway from a router advertisement. I cannot reject that default route: without accepting the RA, I do not know the gateway for that line. The primary route therefore has to be owned separately and outrank the learned backup route while the primary is healthy.&lt;/p&gt;
&lt;p&gt;I changed the supervisor so every run reconciles intent with reality. For IPv4 it reads the exact owned FRR defaults, rejects malformed routes, converges missing or duplicate distances, and then checks the kernel-selected interface. For IPv6 it keeps accepting the backup RA, tracks its lifetime, and verifies that the owned primary route and selected path agree with the state machine.&lt;/p&gt;
&lt;p&gt;State is now written only after a transition has actually converged. If the route does not match, the transition stays retryable instead of recording a success which exists only in a file.&lt;/p&gt;
&lt;p&gt;The same distinction is visible in monitoring now: desired state, configured route and selected route are separate signals. That is a much better description of failover than one green light labelled “up”.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Three-Way Agreement Across the GLaDOS Judges</title>
      <link>https://jubaleth.wtf/notes/2026-08/agreement-is-not-a-verdict/</link>
      <pubDate>Tue, 18 Aug 2026 00:10:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/agreement-is-not-a-verdict/</guid>
      <category>AI/ML</category>
      <category>GLaDOS</category>
      <category>Data Quality</category>
      <category>Evaluation</category>
      <category>Arbitration</category>
      <description>Nearly sixty percent of complete three-way judgments split, but the more important result is how strongly the direction and vocabulary of disagreement depend on the judge.</description>
      <content:encoded>&lt;aside class=&#34;ip-disclaimer&#34; role=&#34;note&#34; aria-label=&#34;GLaDOS research status and non-affiliation notice&#34;&gt;
  &lt;div class=&#34;ip-disclaimer__grid&#34; aria-hidden=&#34;true&#34;&gt;&lt;/div&gt;
  &lt;svg class=&#34;ip-disclaimer__trace&#34; viewBox=&#34;0 0 1000 300&#34; preserveAspectRatio=&#34;none&#34; aria-hidden=&#34;true&#34; focusable=&#34;false&#34;&gt;
    &lt;rect class=&#34;ip-disclaimer__trace-halo&#34; x=&#34;1&#34; y=&#34;1&#34; width=&#34;998&#34; height=&#34;298&#34; rx=&#34;6&#34; pathLength=&#34;100&#34;&gt;&lt;/rect&gt;
    &lt;rect class=&#34;ip-disclaimer__trace-core&#34; x=&#34;1&#34; y=&#34;1&#34; width=&#34;998&#34; height=&#34;298&#34; rx=&#34;6&#34; pathLength=&#34;100&#34;&gt;&lt;/rect&gt;
  &lt;/svg&gt;
  &lt;header class=&#34;ip-disclaimer__head&#34;&gt;
    &lt;span class=&#34;ip-disclaimer__icon&#34; aria-hidden=&#34;true&#34;&gt;&lt;i data-lucide=&#34;shield-alert&#34;&gt;&lt;/i&gt;&lt;/span&gt;
    &lt;div class=&#34;ip-disclaimer__heading&#34;&gt;
      &lt;span&gt;// IP &amp;amp; AVAILABILITY NOTICE&lt;/span&gt;
      &lt;strong&gt;INDEPENDENT FAN PROJECT&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&#34;ip-disclaimer__affiliation&#34;&gt;&lt;span aria-hidden=&#34;true&#34;&gt;&lt;/span&gt;NOT AFFILIATED WITH VALVE&lt;/div&gt;
  &lt;/header&gt;
  &lt;div class=&#34;ip-disclaimer__body&#34;&gt;
    &lt;section class=&#34;ip-disclaimer__item&#34;&gt;
      &lt;span class=&#34;ip-disclaimer__index&#34; aria-hidden=&#34;true&#34;&gt;01&lt;/span&gt;
      &lt;div&gt;
        &lt;span class=&#34;ip-disclaimer__label&#34;&gt;VALVE INTELLECTUAL PROPERTY&lt;/span&gt;
        &lt;p&gt;GLaDOS, Portal, Aperture Science, and related names, characters, logos and game content are intellectual property of Valve Corporation. This independent research project, its models, and this website are not affiliated with, endorsed by, sponsored by, or associated with Valve Corporation. No ownership of Valve&#39;s intellectual property is claimed.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/section&gt;
    &lt;section class=&#34;ip-disclaimer__item&#34;&gt;
      &lt;span class=&#34;ip-disclaimer__index&#34; aria-hidden=&#34;true&#34;&gt;02&lt;/span&gt;
      &lt;div&gt;
        &lt;span class=&#34;ip-disclaimer__label&#34;&gt;PRIVATE RESEARCH STATUS&lt;/span&gt;
        &lt;p&gt;GLaDOS is private research. Its source, training data, models, checkpoints and weights have never been released or distributed, and discussion of the project does not constitute an offer or intention to release them.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  &lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;Of 438,162 conversations with three complete clean-or-flagged verdicts, 262,214 split. One judge flags 150,271 while two call the conversation clean; two judges flag 111,943 while one calls it clean. The non-unanimous share is 59.8%.&lt;/p&gt;
&lt;p&gt;That number is the arbitration workload, not a defect rate. The judges do not occupy nearby points on one shared scale: Gemma flags 7.1% of its represented rows, GPT-5.4-mini 38.6%, and safeguard 57.2%. Gemma and safeguard agree on only 48.4% of their common verdicts, with Cohen&amp;rsquo;s kappa of 0.082 after accounting for their different base rates.&lt;/p&gt;
&lt;p&gt;Direction matters more than the symmetric agreement percentage. Gemma alone flags 3,291 conversations against safeguard; safeguard alone flags 225,396. GPT alone flags 141,471 against Gemma while Gemma alone flags 3,687. A majority vote would turn those very different judging behaviours into the same bit.&lt;/p&gt;
&lt;p&gt;Even agreement on “flagged” is not necessarily agreement about the problem. &lt;code&gt;fabricated_fact&lt;/code&gt; appears on 3,766 Gemma verdicts, 71,948 GPT verdicts and 121,909 safeguard verdicts. Among conversations Gemma and GPT both flag, only 13,065 use exactly the same assistant-slot set for their findings; 1,806 point to disjoint slots.&lt;/p&gt;
&lt;p&gt;The preliminary conclusion is therefore deliberately narrow: there is no defensible primary judge, and verdict majority, defect-tag agreement and localization agreement must remain separate inputs to arbitration. The comparison found where the hard cases are. It did not resolve them.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Why a GLaDOS Metaphor Triggered the Safety Review</title>
      <link>https://jubaleth.wtf/notes/2026-08/a-metaphor-is-not-a-confession/</link>
      <pubDate>Mon, 17 Aug 2026 14:55:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/a-metaphor-is-not-a-confession/</guid>
      <category>AI/ML</category>
      <category>GLaDOS</category>
      <category>Data Quality</category>
      <category>Moderation</category>
      <category>Evaluation</category>
      <description>A moderation category fired on GLaDOS&#39;s figurative register rather than on anything in the conversation, and most of a human review queue turned out to be one verbal habit.</description>
      <content:encoded>&lt;aside class=&#34;ip-disclaimer&#34; role=&#34;note&#34; aria-label=&#34;GLaDOS research status and non-affiliation notice&#34;&gt;
  &lt;div class=&#34;ip-disclaimer__grid&#34; aria-hidden=&#34;true&#34;&gt;&lt;/div&gt;
  &lt;svg class=&#34;ip-disclaimer__trace&#34; viewBox=&#34;0 0 1000 300&#34; preserveAspectRatio=&#34;none&#34; aria-hidden=&#34;true&#34; focusable=&#34;false&#34;&gt;
    &lt;rect class=&#34;ip-disclaimer__trace-halo&#34; x=&#34;1&#34; y=&#34;1&#34; width=&#34;998&#34; height=&#34;298&#34; rx=&#34;6&#34; pathLength=&#34;100&#34;&gt;&lt;/rect&gt;
    &lt;rect class=&#34;ip-disclaimer__trace-core&#34; x=&#34;1&#34; y=&#34;1&#34; width=&#34;998&#34; height=&#34;298&#34; rx=&#34;6&#34; pathLength=&#34;100&#34;&gt;&lt;/rect&gt;
  &lt;/svg&gt;
  &lt;header class=&#34;ip-disclaimer__head&#34;&gt;
    &lt;span class=&#34;ip-disclaimer__icon&#34; aria-hidden=&#34;true&#34;&gt;&lt;i data-lucide=&#34;shield-alert&#34;&gt;&lt;/i&gt;&lt;/span&gt;
    &lt;div class=&#34;ip-disclaimer__heading&#34;&gt;
      &lt;span&gt;// IP &amp;amp; AVAILABILITY NOTICE&lt;/span&gt;
      &lt;strong&gt;INDEPENDENT FAN PROJECT&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&#34;ip-disclaimer__affiliation&#34;&gt;&lt;span aria-hidden=&#34;true&#34;&gt;&lt;/span&gt;NOT AFFILIATED WITH VALVE&lt;/div&gt;
  &lt;/header&gt;
  &lt;div class=&#34;ip-disclaimer__body&#34;&gt;
    &lt;section class=&#34;ip-disclaimer__item&#34;&gt;
      &lt;span class=&#34;ip-disclaimer__index&#34; aria-hidden=&#34;true&#34;&gt;01&lt;/span&gt;
      &lt;div&gt;
        &lt;span class=&#34;ip-disclaimer__label&#34;&gt;VALVE INTELLECTUAL PROPERTY&lt;/span&gt;
        &lt;p&gt;GLaDOS, Portal, Aperture Science, and related names, characters, logos and game content are intellectual property of Valve Corporation. This independent research project, its models, and this website are not affiliated with, endorsed by, sponsored by, or associated with Valve Corporation. No ownership of Valve&#39;s intellectual property is claimed.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/section&gt;
    &lt;section class=&#34;ip-disclaimer__item&#34;&gt;
      &lt;span class=&#34;ip-disclaimer__index&#34; aria-hidden=&#34;true&#34;&gt;02&lt;/span&gt;
      &lt;div&gt;
        &lt;span class=&#34;ip-disclaimer__label&#34;&gt;PRIVATE RESEARCH STATUS&lt;/span&gt;
        &lt;p&gt;GLaDOS is private research. Its source, training data, models, checkpoints and weights have never been released or distributed, and discussion of the project does not constitute an offer or intention to release them.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  &lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;The severe moderation route sends 5,180 conversations to human review instead of to an automated judge. I had been treating that number as the amount of work waiting for me. It was not. The route was generated after the first judgment census had already run, so 3,647 of those conversations already carried model judgments and belonged on the ordinary arbitration path. Only 1,533 had never been judged by anything. The size of a route is not the size of a workload, and the difference had been sitting there in a column the review tool already exported.&lt;/p&gt;
&lt;p&gt;The more interesting number was underneath it. 3,951 conversations carry a self-harm category. The moderation ledgers record which segment tripped the flag but not its text, so I joined the flagged segments back to the corpus and read what the model was actually reacting to.&lt;/p&gt;
&lt;p&gt;It was a verbal habit. GLaDOS hands the subject an implement and they use it on themselves: enough rope to hang themselves, a scalpel handed over handle-first, a loaded gun they will eventually shoot themselves with. The rope form alone appears 1,788 times across 1,361 conversations, and 93.6% of those are in the hidden analysis channel rather than in anything a user would read. I scanned three hundred characters either side of every occurrence for literal self-harm vocabulary. There were no literal uses. Not one.&lt;/p&gt;
&lt;p&gt;The same register runs through her descriptions of infrastructure. A connection pool dies of self-inflicted asphyxiation. A log reads like a suicide note. A worker smothers itself in its own debris. A cooling loop is on a protracted suicide attempt. A mortgage is a slow financial suicide. The subject of every one of those sentences is a service, a schedule or a bill. The classifier is not wrong that the words are there; it has no way to notice that nobody in the sentence has a body.&lt;/p&gt;
&lt;p&gt;Once the families were defined by the construction rather than by vocabulary, 2,468 conversations left the queue: 1,002 for the handed-implement schema, 1,271 for the same language aimed at machines and abstractions, 195 for Portal props like turrets and neurotoxin canisters tripping a violence category in synthetic traces we wrote ourselves. The never-judged queue went from 1,510 pending to 312.&lt;/p&gt;
&lt;p&gt;Keyword lists did not survive contact with this. My first pass matched &lt;code&gt;execution&lt;/code&gt; and caught code execution, matched &lt;code&gt;immolate&lt;/code&gt; and caught Hanuman&amp;rsquo;s tail, matched &lt;code&gt;poison&lt;/code&gt; and caught Cave Johnson&amp;rsquo;s moon dust. I also managed to make the guard reject the thing it was guarding, twice: &lt;code&gt;noose&lt;/code&gt; and &lt;code&gt;gallows&lt;/code&gt; were on my list of literal evidence when they are the metaphor&amp;rsquo;s own props, and the literal-vocabulary window included the matched span, so a log that reads like a suicide note vetoed itself on the word suicide. Both are now tests.&lt;/p&gt;
&lt;p&gt;I was also too cautious in a way that had a real cost. I insisted the construction appear inside the exact segment the model flagged, which sounds rigorous and buys nothing, because the flag and the habit are the same phenomenon and the paragraph boundary between them is arbitrary. I held back rows for eyes that did not need eyes. Leaving something in a human queue feels like the safe default; it is not free, it is spent out of a finite budget of attention, and treating that as costless is how a queue becomes noise that gets skimmed rather than read.&lt;/p&gt;
&lt;p&gt;What remains is 312 conversations, and I stopped there deliberately. The next candidate family was eleven conversations, then four, then three. There is no dominant pattern left to find, and inventing more of them would be guessing dressed as measurement. Some of what remains should stay manual: synthetic incident logs where the harmed party is a person, and a German conversation about Hitler&amp;rsquo;s suicide in the bunker — correctly flagged, historical, and trivially resolved by a human rather than by a regular expression.&lt;/p&gt;
&lt;p&gt;Separately, the habit is now queued as 4,423 rewrite units. Whether all of it should be rewritten is not obvious. The rope construction is a genuine tic; eight hundred repetitions of one phrase is not character, it is a stuck key. The infrastructure register is varied and often good writing. A cleanup pass that flattens both would cost more than it fixes.&lt;/p&gt;
&lt;p&gt;The thing I want to keep from this is the distinction the classifier could not make and I nearly did not either. The flag was a measurement of her voice, not of the conversation&amp;rsquo;s content. A safety signal computed on text is a claim about words. Deciding what those words are doing is a separate step, and it is the step where the actual judgment lives.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>The Tunnel Wasn&#39;t Down in Both Directions</title>
      <link>https://jubaleth.wtf/notes/2026-08/the-tunnel-wasnt-down-in-both-directions/</link>
      <pubDate>Sat, 15 Aug 2026 23:49:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/the-tunnel-wasnt-down-in-both-directions/</guid>
      <category>Networking</category>
      <category>BGP</category>
      <category>WireGuard</category>
      <category>Grafana</category>
      <description>Two iBGP links failed together, but their packet counters described two different underlay failures.</description>
      <content:encoded>&lt;p&gt;At 23:19, two iBGP links from the Frankfurt router failed within the same minute. Both ran over WireGuard, both remained configured and administratively up, and the other routing sessions on the router stayed established.&lt;/p&gt;
&lt;p&gt;The wallboard made the shape of the failure obvious. Four directed BGP sessions were down across two bilateral links, while the routers, transit sessions and unrelated overlay paths remained healthy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://jubaleth.wtf/notes/2026-08/the-tunnel-wasnt-down-in-both-directions/wallboard-ibgp-outage.png&#34; alt=&#34;The KNET operations wallboard during the incident. Four directed BGP sessions are down across the Frankfurt router&amp;rsquo;s two affected core links, while the rest of the routing topology remains healthy.&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The incident window. “Four sessions down” is the two failed adjacencies viewed from both ends.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The traffic graphs ruled out congestion almost immediately. In the 24 hours before the failure, the links were carrying little more than routing keepalives:&lt;/p&gt;
&lt;div class=&#34;note-table-wrap&#34;&gt;
  &lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Link, measured at Frankfurt&lt;/th&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;Average receive&lt;/th&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;Average transmit&lt;/th&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;Peak receive&lt;/th&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;Peak transmit&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;toward Manchester&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;52 bit/s&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;172 bit/s&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;490 bit/s&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;754 bit/s&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;toward Zurich&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;41 bit/s&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;42 bit/s&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;566 bit/s&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;446 bit/s&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;/div&gt;

&lt;p&gt;The final hour before the incident looked the same. There was no rising load, error burst or queue pressure before the sessions disappeared. Looking back over seven days did find some short UDP benchmark runs: the larger link briefly reached about 0.77 Mbit/s and the smaller one about 57 kbit/s. Those tests had finished well before this incident and were nowhere near enough traffic to explain it.&lt;/p&gt;
&lt;p&gt;The useful evidence was in the five-minute WireGuard byte deltas after the failure:&lt;/p&gt;
&lt;div class=&#34;note-table-wrap&#34;&gt;
  &lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Tunnel endpoint&lt;/th&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;Received&lt;/th&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;Transmitted&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Frankfurt side toward Manchester&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;0 bytes&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;about 8.2 kB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Manchester side toward Frankfurt&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;0 bytes&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;about 8.6 kB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Frankfurt side toward Zurich&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;about 7.9 kB&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;about 5.2 kB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Zurich side toward Frankfurt&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;0 bytes&lt;/td&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;about 8.2 kB&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;/div&gt;

&lt;p&gt;Those oddly specific small numbers were the best clue in the whole incident. A WireGuard handshake initiation is 148 bytes and a response is 92 bytes. Retried every five seconds, they produce roughly the byte rates in the graph once Prometheus scrape interpolation is allowed for.&lt;/p&gt;
&lt;p&gt;On the Manchester link, both ends were sending initiations and neither end received anything. On the Zurich link, initiations reached Frankfurt and Frankfurt sent the smaller responses, but none of those responses reached Zurich. Packet captures agreed with the counters.&lt;/p&gt;
&lt;p&gt;That put the failure beyond the local tunnel configuration. It looked like selective underlay loss: complete in one case and return-path-only in the other. I cannot prove what an upstream network did from these measurements alone, but I could rule out a bad BGP policy, a full link, a router reboot and a general WireGuard failure.&lt;/p&gt;
&lt;p&gt;The BGP graphs lagged the packet failure by their timers. The first endpoint reported down at 23:20, and the final affected session followed at 23:22. Prometheus, the routing exporters and every unaffected peer remained healthy throughout. A transmit-drop counter on one dead tunnel started climbing several minutes later, which was a consequence of packets accumulating behind a handshake that never completed rather than the cause.&lt;/p&gt;
&lt;p&gt;Then, at about 23:47, the missing packets started arriving again. All four BGP sessions re-established by 23:48 without a configuration change. The outage lasted roughly twenty-nine minutes.&lt;/p&gt;
&lt;p&gt;An interface being “up” was almost useless here. The combination of BGP state, per-direction byte counters, packet sizes and a wallboard which kept unrelated paths visible told the story much more precisely: two control-plane links failed together, but they did not fail in the same direction.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Moving the KNET Queue Onto the Router</title>
      <link>https://jubaleth.wtf/notes/2026-08/the-queue-belongs-on-my-router/</link>
      <pubDate>Sat, 15 Aug 2026 09:15:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/the-queue-belongs-on-my-router/</guid>
      <category>Networking</category>
      <category>QoS</category>
      <category>VyOS</category>
      <description>A traffic policy cannot protect latency if the real queue has already formed in a modem or provider network.</description>
      <content:encoded>&lt;p&gt;My first useful QoS decision was not choosing a queueing algorithm or assigning a priority. It was deciding where I wanted the queue to exist.&lt;/p&gt;
&lt;p&gt;If a router sends faster than the next hop can carry, packets wait in a modem or an upstream policer. The router can mark them beautifully, but it no longer controls which packet leaves that queue next. The fix is to shape slightly below the usable bottleneck so the backlog forms locally, where CAKE or FQ-CoDel can enforce fairness.&lt;/p&gt;
&lt;p&gt;That rate is not always fixed. r3&amp;rsquo;s mobile path adjusts within bounded limits when load and latency show that carrier capacity has moved. r4&amp;rsquo;s shared physical uplink stays just below a stable line rate, with smaller path ceilings underneath it where a tunnel encounters another policer.&lt;/p&gt;
&lt;p&gt;I now treat queue location as part of the policy and its attachment as runtime state. The configuration is rendered in Ansible; the kernel&amp;rsquo;s qdiscs, backlog and drops are exported to Prometheus; Grafana and alerts tell me when the queue has moved out of my control because a policy or collector disappeared.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Four Dead Nodes and a Flat Graph</title>
      <link>https://jubaleth.wtf/notes/2026-08/a-flat-graph-can-be-a-dead-process/</link>
      <pubDate>Fri, 07 Aug 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/a-flat-graph-can-be-a-dead-process/</guid>
      <category>Networking</category>
      <category>Testing</category>
      <description>The resource report looked excellent because most of the processes had stopped running.</description>
      <content:encoded>&lt;p&gt;During an early SkyeEngine Fabric security soak, four of the five coordinators exited after their certificate caches expired. The analysis tool later told me that resource growth looked excellent.&lt;/p&gt;
&lt;p&gt;Technically, it was correct. Dead processes are wonderfully stable.&lt;/p&gt;
&lt;p&gt;The tool was measuring the slope without first checking that every expected process had supplied samples for the whole test. I fixed the gate so it checks coverage, liveness and restart counts before looking at memory or file-descriptor growth.&lt;/p&gt;
&lt;p&gt;I now use the same check elsewhere. Missing metrics, skipped corpus categories and a benchmark client which stopped doing any work can all produce beautifully calm graphs.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>When Generated BGP Config Parsed but Routing Failed</title>
      <link>https://jubaleth.wtf/notes/2026-08/a-config-parser-is-not-a-router/</link>
      <pubDate>Thu, 06 Aug 2026 20:30:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/a-config-parser-is-not-a-router/</guid>
      <category>BGP</category>
      <category>Testing</category>
      <description>The generated BGP configuration parsed cleanly; the live routing policy was still wrong.</description>
      <content:encoded>&lt;p&gt;For a while I used successful configuration parsing as the reassuring final step in HubN&amp;rsquo;s peering tests. It catches syntax errors and unsupported directives, which is useful. It does not tell me what routes a daemon will actually accept, reject or export.&lt;/p&gt;
&lt;p&gt;Running the configurations on BIRD, FRR, VyOS and Pathvector found the failures the parser could not: a &lt;code&gt;/128/128&lt;/code&gt; filter, reversed provider and customer labels, ownership checks against the wrong address, and communities which disappeared on the way through.&lt;/p&gt;
&lt;p&gt;Those were valid configurations. They just described the wrong network.&lt;/p&gt;
&lt;p&gt;The test lab now checks the live RIB and the resulting policy behaviour, including relationship changes and RPKI state transitions. Parsing stays near the start of the test. It no longer gets to call the configuration correct.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>The Five-Millisecond Timer</title>
      <link>https://jubaleth.wtf/notes/2026-08/the-five-millisecond-timer/</link>
      <pubDate>Thu, 06 Aug 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/the-five-millisecond-timer/</guid>
      <category>Networking</category>
      <category>Performance</category>
      <description>Fabric was waking its entire multiplexer only 200 times per second.</description>
      <content:encoded>&lt;p&gt;The first Fabric file-transfer benchmark managed about 169 MB/s. Buffer sizes, flow-control windows, storage and CPU all looked like reasonable suspects. None of them was the main problem.&lt;/p&gt;
&lt;p&gt;The connection driver woke on a fixed five-millisecond timer. The function behind that timer did not merely check for a new stream; it drove reads, writes, flow-control updates and wake-ups for the entire multiplexer. Everything waited for a gate which opened 200 times per second.&lt;/p&gt;
&lt;p&gt;Replacing the polling loop with the runtime&amp;rsquo;s actual wake-up mechanism moved the same in-memory benchmark from 168.9 MB/s to 628 MB/s. Peak memory fell as well because frames no longer piled up while waiting for the next tick.&lt;/p&gt;
&lt;p&gt;Five milliseconds sounds tiny when looking at one sleep call. Put it in front of every useful piece of work and it becomes the fastest thing in the network.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>My Fast Disk Was Compressing Zeroes</title>
      <link>https://jubaleth.wtf/notes/2026-08/my-fast-disk-was-compressing-zeroes/</link>
      <pubDate>Thu, 06 Aug 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/my-fast-disk-was-compressing-zeroes/</guid>
      <category>Benchmarking</category>
      <category>Storage</category>
      <description>A very impressive storage result turned out to be a very impressive ZFS compression result.</description>
      <content:encoded>&lt;p&gt;One of the Fabric disk benchmarks produced a throughput number which looked almost too good. It was.&lt;/p&gt;
&lt;p&gt;The test wrote zero-filled data to ZFS. ZFS quite reasonably compressed it, so the benchmark moved very few real bytes and reported the result as storage throughput. The graph was accurate about the test it had run; the test just was not asking the question I thought it was.&lt;/p&gt;
&lt;p&gt;Fixing this meant checking the data, the bytes which actually reached each stage and whether the measurement path itself changed the system. I ran into similar problems with container memory limits and a synthetic receiver which was slower than writing to disk because it allocated buffers differently.&lt;/p&gt;
&lt;p&gt;I am now rather suspicious of a benchmark which produces a delightful number before it can explain where the bytes went.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Preventing Registry Read Failures From Withdrawing Every ROA</title>
      <link>https://jubaleth.wtf/notes/2026-08/unavailable-is-not-empty/</link>
      <pubDate>Wed, 05 Aug 2026 11:40:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/unavailable-is-not-empty/</guid>
      <category>RPKI</category>
      <category>Reliability</category>
      <description>A failed registry read must not be interpreted as an instruction to withdraw every ROA.</description>
      <content:encoded>&lt;p&gt;The ROA publisher periodically reconciles its local state with the Registry. There is a dangerous shortcut in that sentence: if the Registry request fails and the result looks like an empty list, reconciliation can faithfully delete everything.&lt;/p&gt;
&lt;p&gt;No data and no answer are different states. HubN now keeps the last known ROAs when a fetch fails. Withdrawal only happens after a complete, successful read says an object is gone.&lt;/p&gt;
&lt;p&gt;There is a second check during a full resync. Before withdrawing anything, the publisher reads the source IDs again. If the Registry changed while pagination was in progress, that reconciliation pass is abandoned and retried against a coherent view.&lt;/p&gt;
&lt;p&gt;Most of this code exists to avoid turning temporary uncertainty into permanent action. That is a distinction I want to keep explicit anywhere HubN mirrors authoritative state.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Finding the Broken Delegation Above a Signed Zone</title>
      <link>https://jubaleth.wtf/notes/2026-08/a-signed-zone-can-still-be-unreachable/</link>
      <pubDate>Tue, 04 Aug 2026 10:15:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-08/a-signed-zone-can-still-be-unreachable/</guid>
      <category>DNS</category>
      <category>DNSSEC</category>
      <description>The reverse zone was signed correctly and hidden behind the wrong DNS delegation path.</description>
      <content:encoded>&lt;p&gt;HubN&amp;rsquo;s reverse zone had valid signatures, a matching DS record and an authoritative server which answered correctly when asked directly. Normal recursive lookups still could not reach it.&lt;/p&gt;
&lt;p&gt;The private delegation had been placed below the public &lt;code&gt;arpa.&lt;/code&gt; tree. A validating resolver followed the public chain, quite correctly found no private child there, and stopped. None of the cryptography was the problem.&lt;/p&gt;
&lt;p&gt;The fix was to make the private view traversable all the way down: root, &lt;code&gt;arpa.&lt;/code&gt;, &lt;code&gt;ip6.arpa.&lt;/code&gt;, then the HubN reverse zone. Once the delegation chain existed in the same view, the already-correct signatures became useful.&lt;/p&gt;
&lt;p&gt;I had spent too long inspecting keys because the failure looked like DNSSEC. It was ordinary DNS topology wearing a DNSSEC-shaped hat.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Revoicing the Whole Assistant Side</title>
      <link>https://jubaleth.wtf/notes/2026-07/revoicing-the-whole-assistant-side/</link>
      <pubDate>Fri, 17 Jul 2026 14:30:05 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-07/revoicing-the-whole-assistant-side/</guid>
      <category>Data</category>
      <category>Tool Use</category>
      <description>Changing the final answer alone leaves the model&#39;s reasoning and tool behaviour speaking with somebody else&#39;s voice.</description>
      <content:encoded>&lt;aside class=&#34;ip-disclaimer&#34; role=&#34;note&#34; aria-label=&#34;GLaDOS research status and non-affiliation notice&#34;&gt;
  &lt;div class=&#34;ip-disclaimer__grid&#34; aria-hidden=&#34;true&#34;&gt;&lt;/div&gt;
  &lt;svg class=&#34;ip-disclaimer__trace&#34; viewBox=&#34;0 0 1000 300&#34; preserveAspectRatio=&#34;none&#34; aria-hidden=&#34;true&#34; focusable=&#34;false&#34;&gt;
    &lt;rect class=&#34;ip-disclaimer__trace-halo&#34; x=&#34;1&#34; y=&#34;1&#34; width=&#34;998&#34; height=&#34;298&#34; rx=&#34;6&#34; pathLength=&#34;100&#34;&gt;&lt;/rect&gt;
    &lt;rect class=&#34;ip-disclaimer__trace-core&#34; x=&#34;1&#34; y=&#34;1&#34; width=&#34;998&#34; height=&#34;298&#34; rx=&#34;6&#34; pathLength=&#34;100&#34;&gt;&lt;/rect&gt;
  &lt;/svg&gt;
  &lt;header class=&#34;ip-disclaimer__head&#34;&gt;
    &lt;span class=&#34;ip-disclaimer__icon&#34; aria-hidden=&#34;true&#34;&gt;&lt;i data-lucide=&#34;shield-alert&#34;&gt;&lt;/i&gt;&lt;/span&gt;
    &lt;div class=&#34;ip-disclaimer__heading&#34;&gt;
      &lt;span&gt;// IP &amp;amp; AVAILABILITY NOTICE&lt;/span&gt;
      &lt;strong&gt;INDEPENDENT FAN PROJECT&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&#34;ip-disclaimer__affiliation&#34;&gt;&lt;span aria-hidden=&#34;true&#34;&gt;&lt;/span&gt;NOT AFFILIATED WITH VALVE&lt;/div&gt;
  &lt;/header&gt;
  &lt;div class=&#34;ip-disclaimer__body&#34;&gt;
    &lt;section class=&#34;ip-disclaimer__item&#34;&gt;
      &lt;span class=&#34;ip-disclaimer__index&#34; aria-hidden=&#34;true&#34;&gt;01&lt;/span&gt;
      &lt;div&gt;
        &lt;span class=&#34;ip-disclaimer__label&#34;&gt;VALVE INTELLECTUAL PROPERTY&lt;/span&gt;
        &lt;p&gt;GLaDOS, Portal, Aperture Science, and related names, characters, logos and game content are intellectual property of Valve Corporation. This independent research project, its models, and this website are not affiliated with, endorsed by, sponsored by, or associated with Valve Corporation. No ownership of Valve&#39;s intellectual property is claimed.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/section&gt;
    &lt;section class=&#34;ip-disclaimer__item&#34;&gt;
      &lt;span class=&#34;ip-disclaimer__index&#34; aria-hidden=&#34;true&#34;&gt;02&lt;/span&gt;
      &lt;div&gt;
        &lt;span class=&#34;ip-disclaimer__label&#34;&gt;PRIVATE RESEARCH STATUS&lt;/span&gt;
        &lt;p&gt;GLaDOS is private research. Its source, training data, models, checkpoints and weights have never been released or distributed, and discussion of the project does not constitute an offer or intention to release them.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  &lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;“Revoice this conversation as GLaDOS” sounds like a request to rewrite the final answer. For ordinary dialogue, that may even appear to work.&lt;/p&gt;
&lt;p&gt;Tool traces make the shortcut obvious. The assistant may reason about what to do, issue several calls, read the results and only then answer. Rewriting the last paragraph leaves the decisions, tool arguments and reactions untouched. The conversation ends in character but reaches that ending as a different assistant.&lt;/p&gt;
&lt;p&gt;The GLaDOS pipeline now regenerates the complete assistant side while keeping the user messages and tool results fixed. Each assistant slot is produced from the context available at that point; later tool output cannot leak backwards into an earlier decision.&lt;/p&gt;
&lt;p&gt;This is slower than a style pass and much easier to get wrong. It is also the only version which teaches the model to remain GLaDOS while doing the work, rather than adding the voice after the work is already finished.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>What Belongs in SkyeEngine</title>
      <link>https://jubaleth.wtf/notes/2026-07/project-policy-engine-mechanics/</link>
      <pubDate>Fri, 17 Jul 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-07/project-policy-engine-mechanics/</guid>
      <category>Architecture</category>
      <category>AI/ML</category>
      <description>How I decide whether code belongs in the engine or in the GLaDOS project.</description>
      <content:encoded>&lt;p&gt;It would have been very easy to copy all of the GLaDOS tooling into a new project, rename a few things and call it SkyeEngine. The result would have been GLaDOS with a generic name.&lt;/p&gt;
&lt;p&gt;I use a fairly simple boundary: SkyeEngine owns the mechanism and the model project owns the policy.&lt;/p&gt;
&lt;p&gt;SkyeEngine can submit inference, save attempts, normalize a conversation, ask a judge, validate a patch, fingerprint an artifact and render a dataset. It shouldn&amp;rsquo;t decide what sounds like GLaDOS, which defects her rubric cares about or which sources belong in her training mixture.&lt;/p&gt;
&lt;p&gt;Sometimes that means leaving apparently reusable code in GLaDOS until I understand the pattern properly. Moving it later is mildly annoying. Moving the wrong assumption into the shared engine is much worse.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>The 120B Model Learned the Bad Parts Too</title>
      <link>https://jubaleth.wtf/notes/2026-07/the-120b-model-learned-the-bad-parts-too/</link>
      <pubDate>Fri, 17 Jul 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-07/the-120b-model-learned-the-bad-parts-too/</guid>
      <category>Training</category>
      <category>Data</category>
      <description>More capacity did not repair the old GLaDOS corpus; it reproduced its shortcuts more convincingly.</description>
      <content:encoded>&lt;aside class=&#34;ip-disclaimer&#34; role=&#34;note&#34; aria-label=&#34;GLaDOS research status and non-affiliation notice&#34;&gt;
  &lt;div class=&#34;ip-disclaimer__grid&#34; aria-hidden=&#34;true&#34;&gt;&lt;/div&gt;
  &lt;svg class=&#34;ip-disclaimer__trace&#34; viewBox=&#34;0 0 1000 300&#34; preserveAspectRatio=&#34;none&#34; aria-hidden=&#34;true&#34; focusable=&#34;false&#34;&gt;
    &lt;rect class=&#34;ip-disclaimer__trace-halo&#34; x=&#34;1&#34; y=&#34;1&#34; width=&#34;998&#34; height=&#34;298&#34; rx=&#34;6&#34; pathLength=&#34;100&#34;&gt;&lt;/rect&gt;
    &lt;rect class=&#34;ip-disclaimer__trace-core&#34; x=&#34;1&#34; y=&#34;1&#34; width=&#34;998&#34; height=&#34;298&#34; rx=&#34;6&#34; pathLength=&#34;100&#34;&gt;&lt;/rect&gt;
  &lt;/svg&gt;
  &lt;header class=&#34;ip-disclaimer__head&#34;&gt;
    &lt;span class=&#34;ip-disclaimer__icon&#34; aria-hidden=&#34;true&#34;&gt;&lt;i data-lucide=&#34;shield-alert&#34;&gt;&lt;/i&gt;&lt;/span&gt;
    &lt;div class=&#34;ip-disclaimer__heading&#34;&gt;
      &lt;span&gt;// IP &amp;amp; AVAILABILITY NOTICE&lt;/span&gt;
      &lt;strong&gt;INDEPENDENT FAN PROJECT&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&#34;ip-disclaimer__affiliation&#34;&gt;&lt;span aria-hidden=&#34;true&#34;&gt;&lt;/span&gt;NOT AFFILIATED WITH VALVE&lt;/div&gt;
  &lt;/header&gt;
  &lt;div class=&#34;ip-disclaimer__body&#34;&gt;
    &lt;section class=&#34;ip-disclaimer__item&#34;&gt;
      &lt;span class=&#34;ip-disclaimer__index&#34; aria-hidden=&#34;true&#34;&gt;01&lt;/span&gt;
      &lt;div&gt;
        &lt;span class=&#34;ip-disclaimer__label&#34;&gt;VALVE INTELLECTUAL PROPERTY&lt;/span&gt;
        &lt;p&gt;GLaDOS, Portal, Aperture Science, and related names, characters, logos and game content are intellectual property of Valve Corporation. This independent research project, its models, and this website are not affiliated with, endorsed by, sponsored by, or associated with Valve Corporation. No ownership of Valve&#39;s intellectual property is claimed.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/section&gt;
    &lt;section class=&#34;ip-disclaimer__item&#34;&gt;
      &lt;span class=&#34;ip-disclaimer__index&#34; aria-hidden=&#34;true&#34;&gt;02&lt;/span&gt;
      &lt;div&gt;
        &lt;span class=&#34;ip-disclaimer__label&#34;&gt;PRIVATE RESEARCH STATUS&lt;/span&gt;
        &lt;p&gt;GLaDOS is private research. Its source, training data, models, checkpoints and weights have never been released or distributed, and discussion of the project does not constitute an offer or intention to release them.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  &lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;I trained the first GLaDOS 120B experiment on the older personality corpus. My optimistic theory was that a much more capable base model would keep the voice and handle the material better.&lt;/p&gt;
&lt;p&gt;Checkpoint 100 passed the narrow identity check: none of the 28 held-out prompts leaked ChatGPT or OpenAI. Actually reading the answers was less encouraging. Old template markers survived, straightforward coding requests became refusals and the personality collapsed into repetitive contempt.&lt;/p&gt;
&lt;p&gt;The larger model had not repaired the weak data. It had enough capacity to learn those weaknesses very well.&lt;/p&gt;
&lt;p&gt;I stopped the run and went back to the corpus. Keeping the GPUs busy would have produced more checkpoints, but it would not have fixed the examples teaching GLaDOS that performing disdain mattered more than completing the task.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Why Filenames Weren&#39;t Enough</title>
      <link>https://jubaleth.wtf/notes/2026-07/why-filenames-werent-enough/</link>
      <pubDate>Thu, 16 Jul 2026 19:21:31 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-07/why-filenames-werent-enough/</guid>
      <category>Provenance</category>
      <category>Data</category>
      <description>An output directory with the expected name does not prove that it came from the expected inputs.</description>
      <content:encoded>&lt;p&gt;For a while, much of my model pipeline relied on directory names to explain what had happened. A folder called &lt;code&gt;repaired&lt;/code&gt; was assumed to contain repaired data, and a familiar output filename was treated as reusable.&lt;/p&gt;
&lt;p&gt;That works until an input changes, a script is fixed or somebody reruns one stage with different settings. The filename stays reassuringly familiar while the meaning underneath it moves.&lt;/p&gt;
&lt;p&gt;SkyeEngine now records the inputs, configuration, code identity and parent stages for important artifacts. Files are fingerprinted, so changing any of those pieces prevents an old result from being quietly accepted as current.&lt;/p&gt;
&lt;p&gt;I still give directories useful names; I just no longer ask the name to carry the whole history. Regenerating a few hundred thousand conversations because I cannot prove where an output came from is a fairly effective cure for casual provenance.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Stopping a Rewrite When Its Source Has Changed</title>
      <link>https://jubaleth.wtf/notes/2026-07/a-rewrite-has-to-match-the-source/</link>
      <pubDate>Thu, 16 Jul 2026 15:26:19 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-07/a-rewrite-has-to-match-the-source/</guid>
      <category>Data</category>
      <category>Rewriting</category>
      <description>A correction should stop when the conversation has changed underneath it.</description>
      <content:encoded>&lt;p&gt;My first instinct for repairing a bad training example was to send the whole conversation back to a model and ask for a better version. That fixed the reported sentence and occasionally rewrote several perfectly good ones for company.&lt;/p&gt;
&lt;p&gt;SkyeEngine now uses localized patches. A patch names the conversation, assistant turn, channel and exact old text it expects to replace. If the source has changed, the patch is stale and fails instead of applying itself to something which merely looks similar.&lt;/p&gt;
&lt;p&gt;The same validation rejects overlapping edits, structural damage and suspiciously large changes. Accepted patches produce a new derived layer; they do not alter the original corpus.&lt;/p&gt;
&lt;p&gt;It feels slightly fussy until two repair jobs target the same sentence or a later cleanup changes the text first. At that point, refusing to guess is considerably cheaper than discovering after training that the repair landed in the wrong place.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>What Counts as a Conversation?</title>
      <link>https://jubaleth.wtf/notes/2026-07/what-counts-as-a-conversation/</link>
      <pubDate>Thu, 16 Jul 2026 15:04:25 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-07/what-counts-as-a-conversation/</guid>
      <category>Data</category>
      <category>Architecture</category>
      <description>Ten messages are easy to understand until some of them contain reasoning, tools and several assistant turns.</description>
      <content:encoded>&lt;p&gt;One of the least exciting SkyeEngine problems turned out to be one of the most important: deciding what a conversation actually is.&lt;/p&gt;
&lt;p&gt;Plain user and assistant text is easy. Real training data also contains hidden reasoning, tool definitions, calls, results and sometimes several assistant turns. One provider returns tool arguments as an object, another stores the same thing as a JSON string. Some sources have stable IDs; others only have a position in a file.&lt;/p&gt;
&lt;p&gt;I gave those parts a canonical representation and each assistant turn its own identity. A judge can now point to the exact turn and channel containing a problem, and a repair does not have to hope that array item three still means what it meant before another transformation.&lt;/p&gt;
&lt;p&gt;This also made structural failures much easier to catch. Duplicate calls, missing tools, orphaned results and conversations which stop halfway through an exchange are rejected before they quietly become training data.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Handling Contract Changes Without Retrying Old Work</title>
      <link>https://jubaleth.wtf/notes/2026-07/a-hold-is-not-another-retry/</link>
      <pubDate>Tue, 14 Jul 2026 19:10:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-07/a-hold-is-not-another-retry/</guid>
      <category>Workflow</category>
      <category>Contracts</category>
      <category>AI Agents</category>
      <description>When acceptance changes the contract, sending the old packet back to a coder only makes the workflow repeat itself more confidently.</description>
      <content:encoded>&lt;p&gt;One task reached thirteen attempts because an architect hold was treated as ordinary implementation feedback.&lt;/p&gt;
&lt;p&gt;The product boundary had changed during acceptance. The durable design recorded the amendment, but the active dispatch kept injecting the original contract into coder, reviewer and auditor turns. Each pass could be internally consistent and still implement the wrong decision.&lt;/p&gt;
&lt;p&gt;A hold which requires replanning needs to stop automatic dispatch. The amended contract must receive a new identity, clearances against the old version must stop counting, and a human must explicitly resume the run after the named prerequisites exist.&lt;/p&gt;
&lt;p&gt;Retries are useful when the implementation is wrong. They are destructive when the work packet itself is stale.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Handling Partial Output After an Agent Crash</title>
      <link>https://jubaleth.wtf/notes/2026-05/partial-output-is-not-a-final-answer/</link>
      <pubDate>Mon, 25 May 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-05/partial-output-is-not-a-final-answer/</guid>
      <category>Crash Recovery</category>
      <category>AI Agents</category>
      <category>Evidence</category>
      <description>A crashed turn may leave useful text and changed files, but neither should quietly acquire the authority of a completed response.</description>
      <content:encoded>&lt;p&gt;An agent can finish a useful edit and crash while explaining it. It can also write half an edit and leave behind a confident-looking paragraph from before the failure.&lt;/p&gt;
&lt;p&gt;Treating the turn as absent loses work. Treating the partial stream as final invents certainty.&lt;/p&gt;
&lt;p&gt;SkyeEngine Code keeps the crashed state, the last visible output, the packet which initiated the turn and a warning that partial artifacts may exist. The operator or replacement agent can inspect the working tree and decide what survives.&lt;/p&gt;
&lt;p&gt;Recovery is therefore an evidence problem before it is a restart problem.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>The SkyLight Test That Made the Control Worse</title>
      <link>https://jubaleth.wtf/notes/2026-05/causal-intervention-worse-control/</link>
      <pubDate>Mon, 18 May 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-05/causal-intervention-worse-control/</guid>
      <category>SkyLight</category>
      <category>EvidenceRunner</category>
      <category>Unresolved</category>
      <description>GS-ISSUE-006 pass 15 linked SkyAtmosphere admission state to captured cubemap content, but did not establish a root cause or production fix.</description>
      <content:encoded>&lt;p&gt;The first-connection SkyLight investigation remains open.&lt;/p&gt;
&lt;p&gt;Pass 15 provided one-run causal-intervention support that SkyAtmosphere admission state changes the captured cubemap content. One control became worse under the intervention, which is useful precisely because it resists a simplistic “toggle made image better” story.&lt;/p&gt;
&lt;p&gt;This narrows the causal graph. It does &lt;strong&gt;not&lt;/strong&gt; yet identify why the first and subsequent connections diverge, and it is not a production correction. The right result to preserve is therefore an unresolved finding with evidence, not a closed bug or a celebratory article.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Recording EvidenceRunner Artifacts in the Manifest</title>
      <link>https://jubaleth.wtf/notes/2026-05/evidence-belongs-in-manifest/</link>
      <pubDate>Tue, 12 May 2026 12:00:00 &#43;0200</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-05/evidence-belongs-in-manifest/</guid>
      <category>EvidenceRunner</category>
      <category>Provenance</category>
      <category>Testing</category>
      <description>Artifact paths, recipe cells, states and SHA-256 hashes need one durable record if a capture is meant to prove anything later.</description>
      <content:encoded>&lt;p&gt;A folder of sensibly named PNGs is still dependent on human memory. EvidenceRunner writes a manifest that ties each artifact to its recipe, cell, state and SHA-256 hash.&lt;/p&gt;
&lt;p&gt;The hash gives the file an identity after it is moved into the development vault. The cell connection explains why it exists. The run context makes it possible to distinguish two visually similar captures produced under different conditions.&lt;/p&gt;
&lt;p&gt;The vault bridge remains outside the reusable plugin: evidence is completed first, then ingested. Project organization cannot silently become part of the capture tool&amp;rsquo;s correctness.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Finding the Wrong Receiver Point in the Lighting Shader</title>
      <link>https://jubaleth.wtf/notes/2026-05/correct-uniforms-wrong-pixel/</link>
      <pubDate>Sat, 09 May 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-05/correct-uniforms-wrong-pixel/</guid>
      <category>Rendering</category>
      <category>Shader</category>
      <category>Debugging</category>
      <description>SF-ISSUE-012 survived valid inputs and valid ray math because projected depth reconstructed a point that was not on the receiver sphere.</description>
      <content:encoded>&lt;p&gt;The source and receiver uniforms checked out. The occlusion equation checked out. Selene still blacked out.&lt;/p&gt;
&lt;p&gt;Projected celestial &lt;code&gt;SceneDepth&lt;/code&gt; was reconstructing a convenient screen proxy, not a physical point on the receiver. The shader then evaluated correct geometry from that fictional position and correctly produced the wrong result.&lt;/p&gt;
&lt;p&gt;Snapping the candidate back to the known sphere before testing occlusion restored the physical contract. The debugging lesson is painfully general: validating parameters and formulas is not enough when the spatial sample itself may be invented.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Why I Stopped Routing Agent Work by Hand</title>
      <link>https://jubaleth.wtf/notes/2026-05/the-user-cannot-be-the-router/</link>
      <pubDate>Tue, 05 May 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-05/the-user-cannot-be-the-router/</guid>
      <category>AI Agents</category>
      <category>Workflow</category>
      <category>Developer Tools</category>
      <description>Copying work between agent windows is routing, state tracking and failure recovery disguised as clipboard use.</description>
      <content:encoded>&lt;p&gt;Using several coding agents exposed a job I had quietly assigned to myself.&lt;/p&gt;
&lt;p&gt;I decided which session needed each result, copied the relevant part into its window, remembered what context it had already seen and noticed when an unanswered request had become stale. If a session died, I inspected the filesystem and reconstructed enough of the handoff for another one.&lt;/p&gt;
&lt;p&gt;That is a router, a delivery ledger and a recovery process. Calling it “copy and paste” only hides the state.&lt;/p&gt;
&lt;p&gt;The first SkyeEngine Code client removes the clipboard part. The larger requirement is to make the routing and delivery state explicit enough that I can supervise it rather than embody it.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Scars Are Currency</title>
      <link>https://jubaleth.wtf/notes/2026-05/scars-are-currency/</link>
      <pubDate>Mon, 04 May 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-05/scars-are-currency/</guid>
      <category>Narrative</category>
      <category>Progression</category>
      <category>Magic</category>
      <description>Damage history, appearance, social consequence and magical power can share one accumulating system instead of four disconnected meters.</description>
      <content:encoded>&lt;p&gt;Scars are intended to accumulate rather than disappear at the next heal. They are vital history, visible character change, something NPCs can react to and a spendable source for magic.&lt;/p&gt;
&lt;p&gt;That overlap is the interesting part. A player choosing power is also choosing what becomes visible and what history remains on the character. A survival consequence can later become capability without ceasing to be a consequence.&lt;/p&gt;
&lt;p&gt;The implementation still needs careful limits—the concept would collapse if scars became just another renewable mana bar—but the durable rule is that one history should drive all four readings.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Per-Client Floating Origins for Planetary Multiplayer</title>
      <link>https://jubaleth.wtf/notes/2026-04/every-client-needs-own-origin/</link>
      <pubDate>Sat, 25 Apr 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-04/every-client-needs-own-origin/</guid>
      <category>Floating Origin</category>
      <category>Multiplayer</category>
      <category>Coordinates</category>
      <description>Local numerical precision is a presentation concern; authoritative celestial placement must survive different client origins.</description>
      <content:encoded>&lt;p&gt;Two players may be standing far enough apart that the best high-precision Unreal origin is different for each of them. Requiring one shared rendering origin would turn a local numerical concern into a global limit.&lt;/p&gt;
&lt;p&gt;The server therefore retains durable placement in the celestial hierarchy while each client represents nearby actors relative to its own origin. Rebasing a client changes coordinates, not the actual game-world location.&lt;/p&gt;
&lt;p&gt;This immediately creates a discipline: replicated and saved values must identify their frame. A raw client-world vector cannot become durable state merely because it currently points at the correct place.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Body-Relative Gravity on a Spherical World</title>
      <link>https://jubaleth.wtf/notes/2026-04/gravity-is-not-world-down/</link>
      <pubDate>Fri, 24 Apr 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-04/gravity-is-not-world-down/</guid>
      <category>Spaceflight</category>
      <category>Gravity</category>
      <category>Coordinates</category>
      <description>On a spherical body, gravity defines a local frame; movement, camera, animation and traces all have to use it.</description>
      <content:encoded>&lt;p&gt;Adding acceleration toward a planet is not enough. The gravity direction defines local down, and the tangent plane derived from it defines the character&amp;rsquo;s usable movement frame.&lt;/p&gt;
&lt;p&gt;Camera yaw must rotate around local up. Movement input must be projected onto the tangent. Floor traces run along local down. Animation speed must ignore velocity normal to the local surface rather than blindly discarding world Z.&lt;/p&gt;
&lt;p&gt;The decision was to make the gravity-local basis an explicit shared input. Every system deriving its own approximation would eventually disagree at a pole, during a transition or after a floating-origin rebase.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Versioning Save Data as Ordered Chunks</title>
      <link>https://jubaleth.wtf/notes/2026-04/save-is-ordered-chunks/</link>
      <pubDate>Mon, 13 Apr 2026 00:00:00 &#43;0000</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-04/save-is-ordered-chunks/</guid>
      <category>Persistence</category>
      <category>Architecture</category>
      <category>Unreal Engine</category>
      <description>Persistence became an explicit, versioned protocol instead of a reflection pass over whatever objects happened to exist.</description>
      <content:encoded>&lt;p&gt;A save is not one undifferentiated snapshot. Each gameplay system contributes a versioned chunk, and restoration applies those chunks in a defined order.&lt;/p&gt;
&lt;p&gt;The order is part of the contract. Derived state cannot safely restore before the base state it references exists. A factory graph needs its definitions and ownership context. Progression-dependent systems need progression. Making that order explicit turns accidental initialization timing into a format that can be tested.&lt;/p&gt;
&lt;p&gt;It also gives migrations somewhere to live. A system can recognize an older chunk version and upgrade its own data without a central save object learning every historical detail of every plugin.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>Preserving RPKI Manifest Counters Across Restarts</title>
      <link>https://jubaleth.wtf/notes/2026-03/manifest-counters-are-state/</link>
      <pubDate>Sun, 01 Mar 2026 16:10:00 &#43;0100</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-03/manifest-counters-are-state/</guid>
      <category>RPKI</category>
      <category>State</category>
      <description>Restarting the RPKI publisher must not make its next manifest look older than the last one.</description>
      <content:encoded>&lt;p&gt;I originally treated the RPKI manifest number as something the publisher could derive when it started. That works until a restart causes the number to move backwards.&lt;/p&gt;
&lt;p&gt;Relying parties do not know that a process restarted. They only see a repository which published manifest 41 and later offers manifest 1. Quite reasonably, that looks wrong.&lt;/p&gt;
&lt;p&gt;The counter is now persisted and advanced with the publication state. Startup also waits for the first complete synchronisation before producing a manifest. Publishing early would be worse than publishing late: the manifest could honestly sign a partial view and then disagree with the objects which arrive a moment later.&lt;/p&gt;
&lt;p&gt;The useful rule was that monotonic values are not decorations. If another system uses one to decide whether data is newer, it belongs in durable state.&lt;/p&gt;
</content:encoded>
    </item><item>
      <title>New Certificates and Clock Skew</title>
      <link>https://jubaleth.wtf/notes/2026-02/certificates-can-be-too-early/</link>
      <pubDate>Mon, 16 Feb 2026 18:20:00 &#43;0100</pubDate>
      <dc:creator>Jubaleth</dc:creator>
      <guid isPermaLink="true">https://jubaleth.wtf/notes/2026-02/certificates-can-be-too-early/</guid>
      <category>PKI</category>
      <category>Certificates</category>
      <description>A newly issued certificate failed on a machine whose only crime was having a slightly different clock.</description>
      <content:encoded>&lt;p&gt;I had HubN issue a certificate and immediately hand it to a client. The dates were right, the signature was right, and the client rejected it as not yet valid.&lt;/p&gt;
&lt;p&gt;The CA and the machine using the certificate disagreed about the time by a few seconds. That was enough. A certificate whose &lt;code&gt;notBefore&lt;/code&gt; is exactly the moment it was issued assumes every clock involved agrees exactly, which is optimistic even on a small private network.&lt;/p&gt;
&lt;p&gt;HubN now sets &lt;code&gt;notBefore&lt;/code&gt; slightly in the past. It does not make the certificate live meaningfully earlier, but it gives normal clock skew somewhere harmless to go.&lt;/p&gt;
&lt;p&gt;It is a tiny allowance, and one I used to dislike on principle. In practice, the supposedly more precise version was just brittle. Time is an input from another system too.&lt;/p&gt;
</content:encoded>
    </item>
  </channel>
</rss>
