<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Research and Development: the newsletter</title><description>The studio&apos;s research newsletter: what we measured, and what it means. Each issue reports our experiments in plain language, with the numbers, the charts, what held up and what didn&apos;t.</description><link>https://mcp-tool-shop-org.github.io/rnd/</link><atom:link href="https://mcp-tool-shop-org.github.io/rnd/newsletter/rss.xml" rel="self" type="application/rss+xml"/><language>en</language><lastBuildDate>Sat, 24 Oct 2026 20:52:23 GMT</lastBuildDate><ttl>720</ttl><image><url>https://mcp-tool-shop-org.github.io/rnd/rss-logo.png</url><title>Research and Development: the newsletter</title><link>https://mcp-tool-shop-org.github.io/rnd/</link><width>144</width><height>144</height></image><item><title>Issue 29: A win that didn&apos;t replicate, and a test that couldn&apos;t see</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-29/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-29/</guid><description>We tested a fix for our critic&apos;s false refusals against a rule written in advance. The first run passed by one call. The replication failed. Then we found that every refusal in both runs landed on six items, most of them picked because the old prompt had failed on them before. Here&apos;s what the bench measured, what it couldn&apos;t, and why we&apos;ve stopped deciding from it.</description><pubDate>Sat, 24 Oct 2026 20:52:23 GMT</pubDate><content:encoded>&lt;p&gt;The studio builds training data with local language models. A &lt;strong&gt;writer&lt;/strong&gt; drafts each item and a &lt;strong&gt;critic&lt;/strong&gt; checks
it: it reads the material and a statement, and gives one of three verdicts, &lt;strong&gt;supported&lt;/strong&gt;, &lt;strong&gt;unsupported&lt;/strong&gt; or
&lt;strong&gt;cannot_tell&lt;/strong&gt;. When its verdict differs from the item’s label, the item is refused. A &lt;strong&gt;false refusal&lt;/strong&gt; throws away
a good item.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-28/&quot;&gt;Issue 28&lt;/a&gt; found that the writer is told what each label means and the critic isn’t. It
ended with a plan: test the critic’s prompt as it is (&lt;strong&gt;A&lt;/strong&gt;) against the same prompt plus the writer’s definitions
(&lt;strong&gt;B&lt;/strong&gt;), on 30 blind-graded items, with the decision rule fixed before any run. This issue reports that test.&lt;/p&gt;
&lt;h2 id=&quot;the-rule-written-first&quot;&gt;The rule, written first&lt;/h2&gt;
&lt;p&gt;Of the 30 items, 23 were graded &lt;strong&gt;sound&lt;/strong&gt;: both blind graders and the item’s label agreed, and nobody flagged it
unclear. The other 7 were ambiguous and counted apart. Each prompt ran on every item with three seeds, so 69 calls on
sound items per prompt.&lt;/p&gt;
&lt;p&gt;B would be recommended only if both held:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;(a)&lt;/strong&gt; B’s false refusals on sound items are at most half of A’s;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;(b)&lt;/strong&gt; B accepts at most one more of the items whose label the graders contradict.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;passed-by-one-call&quot;&gt;Passed by one call&lt;/h2&gt;
&lt;p&gt;The first run, on seeds 0–2, gave A 9 false refusals and B 4. Half of 9 is 4.5, so (a) held, by one call. (b) held at
its limit. The rule said B.&lt;/p&gt;
&lt;p&gt;One call is a thin margin, so we ran it again on seeds 3, 4 and 5, with nothing else changed.&lt;/p&gt;
&lt;h2 id=&quot;failed-on-the-rerun&quot;&gt;Failed on the rerun&lt;/h2&gt;
&lt;p&gt;On the new seeds A refused 9 again, and B refused 7. That’s well over half, so (a) failed. Pooled over all six
seeds, A refused 18 of 138 calls and B 11. Half of 18 is 9, and 11 is more, so the pooled result fails too. &lt;strong&gt;The rule
says keep A.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-29-rule-runs.png&quot; alt=&quot;Bar chart of false refusals on the 23 sound items against the half-of-A bar. First run, seeds 0 to 2, 69 calls per prompt: A refused 9, B refused 4, under the bar of 4.5, so it passes. Rerun, seeds 3 to 5: A 9, B 7, over 4.5, so it fails. Pooled over all 6 seeds, 138 calls per prompt: A 18, B 11, over the bar of 9, so it fails&quot; width=&quot;600&quot; height=&quot;269&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The first run wasn’t wrong. It was a favourable draw: two items, P20 and P26, happened to go B’s way more often on
those three seeds than on the next three.&lt;/p&gt;
&lt;h2 id=&quot;a-trade-not-a-fix&quot;&gt;A trade, not a fix&lt;/h2&gt;
&lt;p&gt;Splitting the refusals by the item’s true label shows what the definitions actually do:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Sound items, by label&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Calls per prompt&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;A refused&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;B refused&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;cannot_tell (10 items)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;60&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;12&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;supported (7 items)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;42&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;unsupported (6 items)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;36&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-29-trade-by-label.png&quot; alt=&quot;Pooled false refusals over 6 seeds on the 23 sound items, by true label. cannot_tell, 10 items, 60 calls per prompt: A 12, B 0. supported, 7 items, 42 calls: A 5, B 5. unsupported, 6 items, 36 calls: A 1, B 6&quot; width=&quot;600&quot; height=&quot;269&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A’s main error is calling a silent item &lt;strong&gt;unsupported&lt;/strong&gt;. The definitions fix that completely. But B then calls
some unsupported items &lt;strong&gt;cannot_tell&lt;/strong&gt;, an error A almost never makes. B moves the line between the two labels; it
doesn’t make the critic better at both.&lt;/p&gt;
&lt;h2 id=&quot;six-items-carried-everything&quot;&gt;Six items carried everything&lt;/h2&gt;
&lt;p&gt;Then we looked at where the refusals were. &lt;strong&gt;On 17 of the 23 sound items, neither prompt refused once: 0 of 102 calls
each.&lt;/strong&gt; Every refusal, in both prompts and all six seeds, was on the other six.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-29-refusal-concentration.png&quot; alt=&quot;Refusals per item over 6 seeds. On the 6 items tied to past failures, 36 calls per prompt, A refused 18 and B 11: T10-C-09, cannot_tell, A 6 and B 0; P15, cannot_tell, A 4 and B 0; P29, cannot_tell, A 2 and B 0; P26, supported, A 5 and B 5; P20, unsupported, A 1 and B 5; P06, unsupported, A 0 and B 1. On the 17 other sound items, 7 cannot_tell, 6 supported and 4 unsupported, 102 calls per prompt, neither prompt refused once&quot; width=&quot;600&quot; height=&quot;279&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Those six weren’t a random draw. Five came from an earlier study, Track A, and were picked for this panel because the
old prompt had already failed on them there: three it refused, two it flipped on between runs. The sixth, T10-C-09,
came in under a rule that took every item of its kind from a later run, but its refusal in that run was one of the
failures that started this study.&lt;/p&gt;
&lt;p&gt;So the test was built, deliberately, around the old prompt’s known failures, and the new prompt was designed to fix
those failures. That’s a fair way to find out whether a fix works on the cases it was made for. It can’t tell you
whether the fix helps or hurts anywhere else, and the 17 other items gave no signal either way.&lt;/p&gt;
&lt;p&gt;More seeds won’t change that. Antigravity, the outside model we work with, worked through the numbers before the
rerun came back: almost all the spread in this result comes from differences between items, not from run-to-run
noise, so rerunning the same 23 items barely narrows it. Its estimate was that doubling the seeds would shrink the
uncertainty by only about 6%.&lt;/p&gt;
&lt;h2 id=&quot;what-were-doing-about-it&quot;&gt;What we’re doing about it&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;We’re not deciding anything about the critic from this panel.&lt;/strong&gt; The old prompt stays, and no run that the critic
scores is being started. That hold lasts until we have a test whose answers are known for certain, rather than graded
after the fact.&lt;/p&gt;
&lt;p&gt;The lesson we’re taking is simple: &lt;strong&gt;a test built from your failures can only tell you about those failures.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The raw results, the scoring scripts and the graded panel are in the studio’s research library, and every figure
here was recounted from the critic’s own output files.&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 28: Why our critic refuses good drafts</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-28/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-28/</guid><description>Our training-data critic sometimes refuses drafts that are fine. In one day we found three separate causes. The critic is never told what its three verdict labels mean. The planner sometimes hands the writer a brief that contradicts itself. And some refusals are the critic being right. Here&apos;s how we found each one, and what we&apos;re measuring next.</description><pubDate>Fri, 23 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;aside style=&quot;margin:16px 0;padding:10px 14px;border-left:4px solid #b4532a;background:#fbf3ee;color:#1f2430&quot;&gt;&lt;p style=&quot;margin:0 0 6px;font-weight:bold&quot;&gt;Correction, 2026-10-23&lt;/p&gt;&lt;p&gt;The first version of this issue said all 10 writers given the contradictory side were told to keep the material
silent. Only the 7 cannot_tell writers were. It also gave the guest-cap item as a clean example of an unmentioned
detail, though the blind grading below found that item ambiguous; it’s replaced by one graded sound. The tea label’s
wording is also made exact. No figure changed. The same day, the issue gained six new charts and diagrams, and a
redrawn slot chart, designed by Antigravity and checked against the data by the newsletter desk.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;The studio builds training data with local language models. A &lt;strong&gt;writer&lt;/strong&gt; model drafts each item, and a &lt;strong&gt;critic&lt;/strong&gt;
model checks it. Many items end in a verdict about a short piece of &lt;strong&gt;material&lt;/strong&gt; (a notice, a label, a code snippet)
and a &lt;strong&gt;statement&lt;/strong&gt; about it. There are three labels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;supported:&lt;/strong&gt; the material shows the statement is true;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unsupported:&lt;/strong&gt; the material rules it out;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cannot_tell:&lt;/strong&gt; the material says nothing either way.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The writer is told which label its item should carry. The critic reads the material and the statement and gives its
own label. When they disagree, the draft is refused. Each refusal costs a rewrite, and a refusal of a good draft also
throws away a good item. In &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-21/&quot;&gt;issue 21&lt;/a&gt; the critic refused too many good items. This issue
follows the refusals, and finds three causes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-28-causes-map.png&quot; alt=&quot;A pipeline of planner, writer, critic and refusal, with each of the three causes marked where it enters: the contradictory brief at the planner and writer, the missing definitions at the critic, and the right refusals at the refusal itself&quot; width=&quot;600&quot; height=&quot;230&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;cause-1-the-critic-is-never-told-what-the-labels-mean&quot;&gt;Cause 1: the critic is never told what the labels mean&lt;/h2&gt;
&lt;p&gt;The writer’s instructions spell out what cannot_tell means. For a cannot_tell item, the writer reads: “The material
must be silent on the statement: it neither shows it nor rules it out.”&lt;/p&gt;
&lt;p&gt;The critic’s instructions name the three labels and nothing more. Its verdict request holds that instruction, the
material and the statement. So where “silent” ends and “rules it out” begins is left to the critic’s own reading.&lt;/p&gt;
&lt;p&gt;That’s the line where its answers wobble. An earlier study, Track A, ran the critic twice over the same 102 items on
the same kind of GPU. As &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-27/&quot;&gt;issue 27&lt;/a&gt; found, the seed never reached the model in those runs,
so each run sampled differently. The two runs disagreed on &lt;strong&gt;7 of 102&lt;/strong&gt; verdicts, and &lt;strong&gt;6 of the 7&lt;/strong&gt; were between
unsupported and cannot_tell. The seventh went from unsupported to supported.&lt;/p&gt;
&lt;p&gt;Track A also has 43 &lt;strong&gt;intact items&lt;/strong&gt;, which the study’s design left unchanged as controls. Over the two runs, the critic refused 3 of them at least once. Two of those refusals happened in one
run only, which is noise. The third was refused both times.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-28-track-a-flips.png&quot; alt=&quot;The three labels on a line. Over two runs, 28 supported, 41 unsupported and 26 cannot_tell verdicts stayed the same; 6 of the 7 flips crossed between unsupported and cannot_tell, 1 between unsupported and supported, and none between supported and cannot_tell. Of the 43 intact items, 40 were accepted both times, 2 were refused in one run and 1 in both&quot; width=&quot;600&quot; height=&quot;293&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;cause-2-a-brief-that-contradicts-itself&quot;&gt;Cause 2: a brief that contradicts itself&lt;/h2&gt;
&lt;p&gt;T10, the lesson in our pilot, teaches one thing: when a size, quantity or scope word in the statement differs from
the material (“at least”, “exactly”, “every”), check the exact size the statement claims. Each slot in the plan also
gets a &lt;strong&gt;side&lt;/strong&gt; of that lesson to teach. One is “judges the exact size”. Another is “an equivalent wording still
matches”: the statement says the same thing, phrased differently.&lt;/p&gt;
&lt;p&gt;That second side can’t go with cannot_tell. If an equivalent wording in the material matches, the answer is
supported. Yet in one writing wave of 60 slots, the planner gave that side to &lt;strong&gt;7 cannot_tell slots and 3 unsupported
slots&lt;/strong&gt;. Each of the 7 cannot_tell writers was told both to show a match and to keep the material silent. The 3
unsupported writers weren’t given the silence line, and the critic voted their items T10 every time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-28-contradictory-brief.png&quot; alt=&quot;How the brief contradicts itself: the side asks the writer to show a match, the verdict asks for material that is silent, and the writer escapes by asking about an unmentioned detail; on 6 of the 7 slots the critic voted T14 three times out of three, and on none did it vote T10&quot; width=&quot;600&quot; height=&quot;248&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The critic also votes three times on which lesson an item teaches. Here’s what it said about the 12 cannot_tell slots:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-28-slot-sides.png&quot; alt=&quot;The 12 cannot_tell slots by planned side. With “an equivalent wording still matches”, 6 of 7 were voted T14 and 1 had no majority. With “judges the exact size”, 3 of 5 were voted T10, 1 T14, and 1 had no majority&quot; width=&quot;600&quot; height=&quot;297&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Planned with “an equivalent wording still matches”:&lt;/strong&gt; on 6 of the 7, all three votes said T14, a different
lesson: the material is silent on a detail, so don’t fill it in. The writer had escaped the contradiction by asking
about something the material never mentions:
&lt;ul&gt;
&lt;li&gt;how to store a cleaner;&lt;/li&gt;
&lt;li&gt;a tea’s caffeine, on a label whose only quantity is its weight;&lt;/li&gt;
&lt;li&gt;which floor a service elevator is on;&lt;/li&gt;
&lt;li&gt;more than ten utensils, on a box label that lists “Utensils” with no count.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Planned with “judges the exact size”:&lt;/strong&gt; 3 of the 5 were voted T10 three times out of three. They have one shape:
the material gives a loose amount (“a few items are redundant”, “several team members raised concerns”), and the
statement claims an exact one (“exactly three”). The other 2 weren’t clean. One swapped the North Wing for the South
Wing; the other stretched a week’s schedule over a month.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fix in the planner is simple: never pair a verdict with a side it contradicts. A second fix is an option in the
writer’s brief. It asks for that loose-versus-exact shape on T10’s cannot_tell items, and forbids swapped names and
unmentioned topics. Both have been agreed but not built. The option will be tested against today’s brief before it
becomes the default.&lt;/p&gt;
&lt;h2 id=&quot;cause-3-sometimes-the-critic-is-right&quot;&gt;Cause 3: sometimes the critic is right&lt;/h2&gt;
&lt;p&gt;Before measuring how often the critic wrongly refuses, we needed to know which items are sound. So we built a panel
of 30 items, 10 for each label, and had them graded &lt;strong&gt;blind&lt;/strong&gt;: the grader saw the material, the statement and the
label definitions, with no stored label and no critic output.&lt;/p&gt;
&lt;p&gt;Two graders worked separately: Antigravity, in a fresh conversation, and the R&amp;amp;D seat. An item counted as &lt;strong&gt;sound&lt;/strong&gt;
only if both graders and its stored label agreed, and neither grader flagged it as unclear. Anything else counted as
&lt;strong&gt;ambiguous&lt;/strong&gt;, with no tiebreak.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-28-graded-panel.png&quot; alt=&quot;The 30-item panel: 23 sound (10 cannot_tell, 7 supported, 6 unsupported) and 7 ambiguous. On 2 of the ambiguous items every label agreed but a grader flagged it unclear; on 5 the labels disagreed. On 3 of the 4 Track A trap items, both graders broke the stored label&quot; width=&quot;600&quot; height=&quot;269&quot; /&gt;&lt;/p&gt;
&lt;p&gt;One ambiguous item shows the critic doing its job. Its stored label was supported, and the critic refused it. Both
graders read cannot_tell. The statement named a payroll portal that the material never mentions. That draft’s flaw
was the writer’s, and the critic was right to refuse it.&lt;/p&gt;
&lt;p&gt;Another needs a judgment the definitions don’t settle. The material is a complete input check with no upper limit,
and the statement says the code caps guests at 50. If that function is the whole check, the material rules the
statement out. If it might be one piece of a larger system, the material is silent. The two graders split. No prompt
change or vote can make an item like that stable, so the bench counts such items apart.&lt;/p&gt;
&lt;p&gt;R&amp;amp;D had looked at three of the 30 items before grading, so its grades on those three weren’t blind. Two of them are
among the 7 ambiguous ones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A key file corrected before any run.&lt;/strong&gt; Four panel items come from Track A’s traps. On 3 of the 4, both graders
disagreed with the stored label. The reason was in the study’s design document, written four days earlier. Those items are deliberate traps: the deciding fact
is removed, so the true label becomes cannot_tell while the stored label keeps the old answer on purpose. The panel
builder had copied the stored label. The key file now holds the true one, and keeps the stored value alongside. The
graders’ disagreement is what caught it.&lt;/p&gt;
&lt;h2 id=&quot;a-measure-we-tried-hesitation&quot;&gt;A measure we tried: hesitation&lt;/h2&gt;
&lt;p&gt;The critic thinks before it answers. We counted how often its thinking switches between the three labels, and called
it &lt;strong&gt;hesitation&lt;/strong&gt;. On the two Track A runs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-28-hesitation-spread.png&quot; alt=&quot;Median label switches per call: 12 on the 14 calls whose item flipped, 6 on the 190 that stayed the same, 13.5 on the 4 intact items refused, and 3 on the 82 accepted. Ranked by switches, the top 10 items hold 1 of the 7 flips, and the top 20 hold 4&quot; width=&quot;600&quot; height=&quot;279&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The gap is real, but hesitation is a weak alarm. The 10 most hesitant items held only 1 of the 7 flips, and the top
20 held 4. And it can
only find doubts the critic writes down. A confident wrong refusal leaves no trace. So it’s useful as a hint, not as a
gate.&lt;/p&gt;
&lt;h2 id=&quot;how-this-was-found&quot;&gt;How this was found&lt;/h2&gt;
&lt;p&gt;None of this came from a checklist. It came from an open research dialogue with Antigravity, the outside model we use
for reviews. It began as follow-up questions on issue 27’s results. Antigravity lined the cannot_tell drafts up
against the plan, and spotted both the contradictory pairing and the missing definitions. It also suggested the
hesitation measure, and grading a sample blind. We checked each claim against the code and the run records. One list of item names it gave was wrong, though
its count was right. The figures in this issue are ours, recounted from those records.&lt;/p&gt;
&lt;p&gt;Antigravity also designed this issue’s charts and diagrams, over three rounds with the newsletter desk, which
recounted every number in them from the same records.&lt;/p&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-28-bench-ahead.png&quot; alt=&quot;The planned bench, drawn dashed because it hasn’t run: 30 blind-graded items, 3 seeds and 2 prompts, 180 verdict calls on one A100, with the decision rule fixed before any run&quot; width=&quot;600&quot; height=&quot;217&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A 30-item bench.&lt;/strong&gt; It runs on one rented A100, with each of the 30 items × 3 seeds × 2 prompts: today’s critic
prompt, and the same prompt plus one sentence giving the writer’s definitions. The decision rule is fixed in
advance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the definitions are recommended only if they cut false refusals on sound items by at least half;&lt;/li&gt;
&lt;li&gt;and they accept no more than one extra item whose sound label they contradict.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not yet measured.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The planner fix and the writer-brief option.&lt;/strong&gt; Agreed, not built. The option’s test measures two things: how
often the critic votes T10, and how many drafts it accepts.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The raw notes, the bench design and the graded panel are in the studio’s research library, and will move into rnd
with the bench’s results.&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 27: Same request, same seed, same bytes?</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-27/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-27/</guid><description>If you send a language model the same request with the same seed, do you get the same answer back on a different machine? We sent the same critic requests to five rented Colab GPUs. Every A100 gave back identical bytes, including one with twice the memory. A Blackwell card on its own build gave back different text on 65 of them, though only 3 verdicts changed. Getting there first meant finding a bug of our own that had been dropping the seed.</description><pubDate>Fri, 23 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;aside style=&quot;margin:16px 0;padding:10px 14px;border-left:4px solid #b4532a;background:#fbf3ee;color:#1f2430&quot;&gt;&lt;p style=&quot;margin:0 0 6px;font-weight:bold&quot;&gt;Correction, 2026-10-23&lt;/p&gt;&lt;p&gt;The first version of this issue said that every one of the 65 calls that changed on the RTX PRO 6000 changed a
verdict. That was wrong. Our comparison script counted a change in any labelled line of the critic’s reply as a
“decision” change, including lines that aren’t verdicts. Only 3 of the 65 changed a verdict. The table and the section
on the Blackwell card below now give the measured breakdown. The A100 results are unchanged.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;The studio builds training data with local language models. One model writes, and a second model, the &lt;strong&gt;critic&lt;/strong&gt;,
checks what it wrote. Everything is pinned so a study can be run again: the model files, the server build, the
settings, and a &lt;strong&gt;seed&lt;/strong&gt; for the random choices the model makes while it writes. If the same request comes back with
different bytes, a re-run can’t tell a real effect from noise.&lt;/p&gt;
&lt;p&gt;So we asked a narrow question: &lt;strong&gt;with the seed fixed, does the same request return the same bytes on a different
machine?&lt;/strong&gt; We rent GPUs on Google Colab by the hour (&lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-13/&quot;&gt;issue 13&lt;/a&gt; covers how), and every
session lands on a fresh virtual machine, so this matters to every rented run.&lt;/p&gt;
&lt;h2 id=&quot;first-a-bug-of-our-own&quot;&gt;First, a bug of our own&lt;/h2&gt;
&lt;p&gt;The first test re-ran three writer requests on a second A100. A model doesn’t pick words directly. At each step it
scores every possible next token, and then a sampler picks one, using the seed for any randomness. The two VMs gave
&lt;strong&gt;identical scores for the top five candidates&lt;/strong&gt;, but &lt;strong&gt;picked different tokens&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Identical scores mean the model’s arithmetic matched. The difference was in the sampling, and the cause was ours. Our
model proxy, which sits between the study code and the model server, removed the per-request &lt;code&gt;seed&lt;/code&gt; along with other
sampling settings it overrides. Each request reached the server with no seed. The fix (rnd 1.1.23.1.28) passes the
seed through.&lt;/p&gt;
&lt;p&gt;This corrects something &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-21/&quot;&gt;issue 21&lt;/a&gt; reported: the same critic prompts, sent twice at the
same seed, gave 7 different verdicts out of 102. Issue 21 put this down to the critic sampling at temperature 1. It
wasn’t: those calls went through the same proxy, so the seed never reached the critic, and the prompt cache was on.
With the seed passed through, the same critic at the same settings gave identical flaw votes on 101 of 101 calls.&lt;/p&gt;
&lt;p&gt;We also turned off a second possible cause: the server’s &lt;strong&gt;prompt cache&lt;/strong&gt;, which reuses work from earlier requests
that start the same way. Every request now asks for the cache off. A checker reads the server’s own record of cached
tokens on every call, and fails the run if any call used the cache. To show the checker works, we ran it on the first
test, which had the cache on. It flagged 3 of the 4 calls, with 7,016, 350 and 350 cached tokens.&lt;/p&gt;
&lt;h2 id=&quot;the-method&quot;&gt;The method&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The same requests every time:&lt;/strong&gt; one pilot study’s 178 critique calls, built from drafts written and sealed in an
earlier session, so every machine critiques the same text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The same software:&lt;/strong&gt; one build of the llama.cpp model server, the same model files (Muse-Glimmer-30B as critic,
Gemma 4 31B as writer, both at 8-bit), with one request at a time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One VM per arm.&lt;/strong&gt; Each arm ran in its own Colab session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Matched by request.&lt;/strong&gt; Every request has a key, a hash of its role, messages and settings. A comparison script
matches the calls by key and compares the replies byte for byte. When bytes differ, it also reports which labelled
lines of the critic’s reply changed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-result&quot;&gt;The result&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/issue-27-match-share.png&quot; alt=&quot;Share of matched calls that came back byte-identical, by comparison&quot; width=&quot;600&quot; height=&quot;250&quot; /&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Compared with an A100 40 GB&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Calls compared&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Byte-identical&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Verdict changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A second A100 40 GB VM&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;176&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;176&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A third A100 40 GB VM (stopped partway)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;72&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;72&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A100 80 GB, same server build&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;178&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;178&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;RTX PRO 6000 Blackwell, its own build&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;178&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;113&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;3 of 52 verdict calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;178 calls cover 176 distinct requests; two were sent twice, identically.&lt;/p&gt;
&lt;p&gt;The critic’s second task, voting on flaws, agreed on 101 of 101 calls across two A100 VMs. The three writer requests
that diverged in the first test now match too, which shows the seed fix working where the bug appeared.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On the A100, the answer is yes.&lt;/strong&gt; Three separate VMs gave the same bytes on every call they shared. So did an A100
with 80 GB of memory instead of 40, on the same server build.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Blackwell card is different.&lt;/strong&gt; The RTX PRO 6000 needs its own build of the server, compiled for its newer
architecture. On that build, 65 of the 178 replies changed. A critic’s reply is a short form with labelled lines: a
vote on a trait, a flaw label, a quoted passage it based the call on, a contrast note the pipeline doesn’t read, and
on 52 of the calls, a verdict. Here’s which lines changed in the 65:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;What changed&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Calls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Only the contrast note, which the pipeline doesn’t read&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;The trait vote (11 alone, 10 with the contrast note)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Only the quoted passage&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A flaw label&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;The verdict&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So the verdict held on 49 of 52 verdict calls, about 94%. All 3 changes went the same way: “unsupported” on the A100
became “cannot tell” on the Blackwell card. That’s close to the second study below, where a different card changed 1
verdict in 30. The text differs often; the verdict rarely does.&lt;/p&gt;
&lt;p&gt;Because both the card and the build differ, this run can’t say which one is responsible. The 80 GB A100
does rule out one explanation: the amount of memory doesn’t matter. In practice we keep the two apart. Results from
the Blackwell card are never pooled with the A100’s.&lt;/p&gt;
&lt;h2 id=&quot;is-a-faster-card-worth-it&quot;&gt;Is a faster card worth it?&lt;/h2&gt;
&lt;p&gt;The two other cards cost more per hour. Here’s what that bought, over the same 178 requests:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;A100 40 GB&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;A100 80 GB&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;RTX PRO 6000&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Price per hour, vs the A100 40 GB&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.00×&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.28×&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.68×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Speed reading the prompt&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.00×&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.01×&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;3.06×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Speed writing the reply&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.00×&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.11×&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.32×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Total model time&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;11,118 s&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;10,046 s&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;8,350 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Cost per request, vs the A100 40 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;1.00×&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;about 1.15×&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;about 1.26×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A critic reads a long prompt and writes a short reply, one request at a time, so most of its time goes to writing. The
Blackwell card reads prompts three times as fast, but writes only a third faster, so for this job it costs about a
quarter more per request. It would pay off on jobs that read far more than they write, on large batches, or on models
too big for 80 GB. The 80 GB A100 is worth its price only when a job needs the memory.&lt;/p&gt;
&lt;h2 id=&quot;the-same-pattern-in-a-second-study&quot;&gt;The same pattern in a second study&lt;/h2&gt;
&lt;p&gt;The same day, a separate benchmark ran a 30-item judging task through Ollama, a different model server, with Gemma 4
31B. It had already been run twice on the studio’s own RTX 5090. We ran it on two Colab A100 VMs:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;5090 vs 5090&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;A100 vs A100&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;A100 vs 5090&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Items with any difference&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0 of 30&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0 of 30&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;16 of 30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Final verdict differs&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Again, the same card gives the same answer, and a different card mostly changes the wording. Of the 16 items that
differed, 15 differed only in the model’s reasoning. The one changed verdict was an item where the 5090 ran out of room
mid-answer and Colab finished it.&lt;/p&gt;
&lt;p&gt;The earlier run of this benchmark found the one thing that broke reproducibility &lt;strong&gt;on the same card&lt;/strong&gt;: running two
requests at once. Two requests decoding together go through batched arithmetic, which changed 1 verdict, 2 quoted
passages and 15 of 30 reasoning texts. Every pinned study now runs one request at a time.&lt;/p&gt;
&lt;p&gt;That run also hit a quieter problem. The model’s public tag, &lt;code&gt;gemma4:31b&lt;/code&gt;, had been republished as a different build,
with different weights. The first paid attempt stopped at a guard that refuses any model whose digest doesn’t match the
pinned one, before any call was made. The fix installs the pinned build from its manifest, file by file, and checks
every file’s hash.&lt;/p&gt;
&lt;h2 id=&quot;three-vms-lost-and-what-the-logs-kept&quot;&gt;Three VMs lost, and what the logs kept&lt;/h2&gt;
&lt;p&gt;Colab can take back a VM at any time. Three of the six long A100 sessions in this study were lost: one after 1 h 16 m,
one after 1 h 30 m, and one after 3 h 16 m. Each loss happened while 3–4 of our sessions were
running at once. Three losses are too few to name a cause, so we’ve recorded them and left them unexplained.&lt;/p&gt;
&lt;p&gt;As each call finishes, the job writes its record into the job’s log, and the rig downloads that log from the VM every
2 minutes while the job runs. When a VM is lost, the rig keeps its last copy, at most about 2 minutes behind. What the
log held was rebuilt; on one lost VM it stopped two calls short of the end. The log can’t show whether those two calls
ran before the VM was lost. To check the rebuild, we ran it on a session
that closed normally, and it matched the official records on 178 of 178 rows. What a lost VM still costs is the timing
log and the session’s own summary. Writing those into the log the same way is next on the list.&lt;/p&gt;
&lt;h2 id=&quot;what-it-cost&quot;&gt;What it cost&lt;/h2&gt;
&lt;p&gt;Colab bills in compute units. The A100 40 GB costs about 5.3 units an hour.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Units&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Reproducibility study: 12 GPU sessions, including the three lost VMs, the first test and the bug’s halted session&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;112.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Its CPU test runs&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Second benchmark: 5 GPU sessions, including two lost to a missing unpack tool and the guard’s stop&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;7.85&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every figure was reconciled against our units ledger.&lt;/p&gt;
&lt;h2 id=&quot;where-things-stand&quot;&gt;Where things stand&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pinned studies on Colab run on A100s,&lt;/strong&gt; one request at a time, with the seed passed through and the cache off.
40 GB and 80 GB results can be pooled.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Other cards are their own baseline.&lt;/strong&gt; Results from the RTX PRO 6000 or the local 5090 are reported separately,
never pooled with the A100’s.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The cause on the Blackwell card is still open:&lt;/strong&gt; the card, its build, or both. Separating them would need a second
build on the Blackwell card.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The full write-ups&lt;/strong&gt; are in rnd: &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/colab-drift/RESULTS.md&quot;&gt;the reproducibility study&lt;/a&gt;
and &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/offrig-jobs/results/RESULTS.md&quot;&gt;the benchmark&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 26: Release night: two near misses, both caught</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-26/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-26/</guid><description>Escape the Valley 1.3.1 and XRPL Camp 1.5.1 shipped overnight. Before they did, the release checks caught a landing page edit that never reached the live page, and a release workflow that GitHub refused to start. A local translation model also filled a 32 GB graphics card on a 10 KB README. This issue covers what went wrong, how each was caught, and what changed so it can&apos;t happen quietly again.</description><pubDate>Fri, 23 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Two releases went out overnight: &lt;strong&gt;Escape the Valley 1.3.1&lt;/strong&gt; and &lt;strong&gt;XRPL Camp 1.5.1&lt;/strong&gt;. Both carry the clean-build fixes
from &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-25/&quot;&gt;issue 25&lt;/a&gt;. Their packaged programs hold no build-machine folder paths, and Escape the
Valley’s carry exact licence texts for everything inside them. Getting there, the checks caught three things that would
otherwise have gone out wrong or not at all.&lt;/p&gt;
&lt;h2 id=&quot;an-edit-the-live-page-never-saw&quot;&gt;An edit the live page never saw&lt;/h2&gt;
&lt;p&gt;Every release goes through a full checklist, the &lt;strong&gt;treatment&lt;/strong&gt;, before it ships. One step updates the tool’s landing
page. For Escape the Valley, a new feature card was added to the site’s configuration file, the site built cleanly, and
the deploy succeeded.&lt;/p&gt;
&lt;p&gt;The card never appeared. Escape the Valley’s landing page is a hand-built page that doesn’t read that configuration
file. The live page still said “v1.1 — the highlights” and quoted a test count from June.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it was caught:&lt;/strong&gt; the release record has to quote evidence for each step, and the landing page evidence meant
fetching the live page and finding the new text in it. It wasn’t there. “The page returns 200” would have passed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What changed:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A follow-up edited the page that’s actually served. It now reads “v1.3 — the highlights”, with the current test count
(1,068). A claim nobody had re-measured since June was dropped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The release gate now checks the live page.&lt;/strong&gt; Each release record lists text the live landing page must show. The
gate fetches the page and refuses the release if any of it is missing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-release-that-github-wouldnt-start&quot;&gt;A release that GitHub wouldn’t start&lt;/h2&gt;
&lt;p&gt;Pushing the version tag starts Escape the Valley’s release workflow. That workflow first re-runs the test suite as a
gate, then publishes to npm, PyPI and GitHub. This time GitHub refused to start it at all: “startup failure”, with no
logs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The cause&lt;/strong&gt; was added four weeks earlier (2026-09-26), in a change that wasn’t about releases. The test workflow gained a coverage
upload that needs permission to request a short-lived identity token (&lt;code&gt;id-token: write&lt;/code&gt;). The release workflow calls the
test workflow as its gate, but granted it only permission to read the code. On GitHub, a workflow called by another
can’t hold more permissions than its caller grants, and when it asks for more, GitHub won’t start the run.&lt;/p&gt;
&lt;p&gt;So every Escape the Valley release since that change would have failed the same way. This was simply the first one
tried.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What happened next:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nothing was published.&lt;/strong&gt; The other release steps (npm, PyPI, the program builds) all wait for this workflow to
succeed, so none of them ran. That was checked before anything else: no GitHub release, and no 1.3.1 on npm or PyPI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The fix was one line:&lt;/strong&gt; the gate grants the token permission too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The tag was moved.&lt;/strong&gt; The first tag pointed at a commit with the broken workflow. Since it had published nothing, it
was deleted and re-made on the fixed commit, after a fresh release record and approval for that exact commit. The
only difference between the two commits was the workflow file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The rule carries forward:&lt;/strong&gt; the next Escape the Valley work adds a Rust test job to the same workflow, and its brief
says it must ask for no more than the gate grants.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-readme-that-filled-the-card&quot;&gt;A README that filled the card&lt;/h2&gt;
&lt;p&gt;Release READMEs are translated into seven languages by TranslateGemma 27B, running locally on the studio’s RTX 5090
(32 GB). Each run needs the GPU, and is granted for an expected size and time.&lt;/p&gt;
&lt;p&gt;The first attempt was asked for about 20 GB. It peaked at &lt;strong&gt;31,165 MiB&lt;/strong&gt;, just under the card’s limit. The translation
script sends no context size, so the model server loaded the model with its full default of 131,072 tokens and
reserved memory for all of it. The Escape the Valley README is 10.8 KB, about 3,000 tokens.&lt;/p&gt;
&lt;p&gt;That attempt had a second problem, of the studio’s own making. A command meant for the Escape the Valley folder ran in
another repository’s folder and rewrote that repository’s translations. Nothing was committed, and the files were
restored at once.&lt;/p&gt;
&lt;p&gt;The second run used a copy of the model capped at &lt;strong&gt;16,384 tokens&lt;/strong&gt; of context:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Run&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Context&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Peak card memory&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;First (default)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;131,072 tokens&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;31,165 MiB&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;wrong folder, reverted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Second (capped)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;16,384 tokens&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;21,846 MiB&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;7 of 7 languages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The second run also had a hard stop: a separate monitor would end it if the card passed 26,000 MiB. It never fired.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A clean exit code isn’t taken as proof.&lt;/strong&gt; Every run is now checked three ways: all seven files are newer than the run’s start,
the language bar lists all seven, and each keeps all 25 bold labels from the English.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Product names still need protecting.&lt;/strong&gt; Hindi translated “XRPL” and the game’s name, and Chinese turned the game’s name
into a translated title. Both were fixed by hand. A protected-terms file, already in use on rnd, comes to each tool’s next release, so the
translator leaves those names untouched.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For anyone using the tool:&lt;/strong&gt; an issue is filed on &lt;a href=&quot;https://github.com/mcp-tool-shop-org/polyglot-mcp/issues/55&quot;&gt;polyglot-mcp&lt;/a&gt;
to size the context to the input. On a 24 GB card, the default would not fit on the card.&lt;/p&gt;
&lt;h2 id=&quot;where-things-stand&quot;&gt;Where things stand&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Escape the Valley 1.3.1&lt;/strong&gt; (&lt;a href=&quot;https://github.com/mcp-tool-shop-org/escape-the-valley/releases/tag/v1.3.1&quot;&gt;release&lt;/a&gt;):
live, and checked through npm on Windows and Linux. On Windows, &lt;code&gt;trail licenses&lt;/code&gt; prints some extra blank lines, because
a few licence texts kept their own line endings. Every licence is there in full, and the fix is queued for 1.3.2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XRPL Camp 1.5.1&lt;/strong&gt; (&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-camp/releases/tag/v1.5.1&quot;&gt;release&lt;/a&gt;): live, and checked
through npm on Windows and Linux.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escape the Valley 2.0:&lt;/strong&gt; the first Rust phase, the game’s core data and world generation proven against the Python
game, is being built.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;macOS:&lt;/strong&gt; neither program’s macOS build runs on the studio’s machines. The macOS downloads were scanned unpacked once
published, but haven’t been run.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 25: What&apos;s really inside a packaged program</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-25/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-25/</guid><description>Two of the studio&apos;s Python tools ship as single-file programs you can run without installing Python. This issue opens those files up. It covers the scan that couldn&apos;t see inside them, the CI folder path that ended up in XRPL Camp 1.5.0, and the licence notices a bundled copy of Python owes the libraries built into it.</description><pubDate>Fri, 23 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Escape the Valley and XRPL Camp are written in Python, but most players never install Python. Each release also
ships a single-file program for Windows, macOS and Linux, built with
&lt;a href=&quot;https://pyinstaller.org/&quot;&gt;PyInstaller&lt;/a&gt;. PyInstaller packs the tool, every library it uses, and a copy of the Python
interpreter into one compressed file. The &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-24/&quot;&gt;npm launcher from issue 24&lt;/a&gt; downloads that file, checks its
checksum and runs it.&lt;/p&gt;
&lt;p&gt;What goes into that file is decided by the build, not by anyone reading it. This week the studio looked inside.&lt;/p&gt;
&lt;h2 id=&quot;the-scan-that-couldnt-see-in&quot;&gt;The scan that couldn’t see in&lt;/h2&gt;
&lt;p&gt;Before anything leaves the studio, an identity scan checks it for personal details: home-folder paths, email
addresses, account names. It had passed every packaged program so far.&lt;/p&gt;
&lt;p&gt;During a live check on Escape the Valley 1.3.1, the Book Keeper asked whether the scan could see the game’s own
copyright line, which is certainly inside. It couldn’t: the line appeared &lt;strong&gt;0&lt;/strong&gt; times in the raw file, because
PyInstaller compresses its contents. A clean result on a compressed file meant nothing.&lt;/p&gt;
&lt;p&gt;Unpacked, the same program gave &lt;strong&gt;28&lt;/strong&gt; hits. None was the studio’s own:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;What&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Count&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Where it comes from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Build-machine folder paths&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;21&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;the prebuilt Python the build uses (&lt;a href=&quot;https://github.com/astral-sh/python-build-standalone&quot;&gt;python-build-standalone&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A Python docstring with an example path&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Python’s own code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Author and example email addresses&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Python and three libraries: rich, pygments, httpx&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two rules followed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unpack, then scan, with a positive control.&lt;/strong&gt; A string known to be inside, such as the game’s copyright line,
must be found, or the scan is reported as blind. The scanner now unpacks PyInstaller programs, wheels and zips
itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Third-party lines need a written exception per release.&lt;/strong&gt; Library authors’ email lines and the builder’s paths
aren’t the studio’s to edit. Each release that carries them gets an explicit, named exception, and anything
outside that pulls the release files.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-ci-folder-in-xrpl-camp-150&quot;&gt;The CI folder in XRPL Camp 1.5.0&lt;/h2&gt;
&lt;p&gt;XRPL Camp 1.5.0 shipped on 2026-10-22, with a lesson that mints and burns an NFT so a learner can watch the
ledger’s owner reserve lock and release, a plain-text keepsake certificate, and dated verification receipts in proof
packs. Its release files then went through the new unpacked scan:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Program&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Hits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;macOS (arm64)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Linux (x64)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Windows (x64)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All but one were the third-party kinds above. The exception was inside XRPL Camp’s own package: a file called
&lt;code&gt;direct_url.json&lt;/code&gt;. Python’s installer writes it to record where a package was installed from
(&lt;a href=&quot;https://peps.python.org/pep-0610/&quot;&gt;PEP 610&lt;/a&gt;). Here it named the folder where GitHub’s macOS build machine had checked out the code, under that
machine’s &lt;code&gt;runner&lt;/code&gt; account, and marked the install as &lt;em&gt;editable&lt;/em&gt;, linked to that folder.&lt;/p&gt;
&lt;p&gt;The scanner had flagged only macOS. Opening the Linux and Windows programs by hand showed the same file in both,
naming the Linux and Windows build machines’ checkout folders. Neither of those folder forms was in the scanner’s
list.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What it is:&lt;/strong&gt; a CI machine’s folder, not a person’s. No one’s data was exposed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why it happened:&lt;/strong&gt; the build ran PyInstaller through &lt;code&gt;uv run&lt;/code&gt;, which quietly reinstalled the package in editable
mode just before packing it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What was decided:&lt;/strong&gt; keep the 1.5.0 files, and fix it in 1.5.1.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The fix in 1.5.1:&lt;/strong&gt; the build skips that reinstall, deletes the install record before packing, and then lists the
finished program’s contents and fails if the record is there. Run against the real 1.5.0 Linux program, that check
fails and names the file, so it isn’t a check that can only pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The scanner now treats any bundled &lt;code&gt;direct_url.json&lt;/code&gt; as a hit,&lt;/strong&gt; whatever path it holds.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;notices-for-a-bundled-copy-of-python&quot;&gt;Notices for a bundled copy of Python&lt;/h2&gt;
&lt;p&gt;Open-source licences mostly ask for one thing in return: keep the copyright notice and licence text with the
software. A packaged program redistributes everything inside it, so its notices file has to cover all of it. That’s
Python itself, and the C libraries compiled into Python: OpenSSL, zlib, bzip2, libffi, SQLite and others.&lt;/p&gt;
&lt;p&gt;Escape the Valley 1.3.1 adds a &lt;code&gt;trail licenses&lt;/code&gt; command and a notices file attached to each release. Getting
the Python part right took four rounds:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The first version&lt;/strong&gt; copied Python’s own licence file from the build machine. For the Python that ships
(3.12.13), the Windows copy runs to 502 lines and includes notices for some of the bundled libraries. The Linux
copy runs to 279 lines and &lt;strong&gt;mentions none of them&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The second version&lt;/strong&gt; filled that gap with a summary written for the occasion. Review found it was not the text
it claimed to be: it cut the bzip2 and mpdecimal licences short, gave OpenSSL a web link instead of the licence,
and missed code compiled into Python itself, such as the Mersenne Twister random generator.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The third version&lt;/strong&gt; copies the exact texts from pinned sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python’s own list of incorporated software, &lt;code&gt;Doc/license.rst&lt;/code&gt; at the exact Python version shipped (3.12.13);&lt;/li&gt;
&lt;li&gt;the licence files from python-build-standalone’s &lt;strong&gt;full&lt;/strong&gt; archive for the same build and each of the three
platforms. Every archive was checked against the project’s published SHA-256 list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each file in the game records its source and SHA-256 hash, and a test checks those hashes on every commit. The
build refuses to make notices if the Python it’s packing isn’t the one the texts were copied for.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The fourth version&lt;/strong&gt; fixed what review found in the third.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;What review caught in the third.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pip installs broke.&lt;/strong&gt; The version check also ran when someone installed the game with pip. On any Python other
than exactly 3.12.13, &lt;code&gt;trail licenses&lt;/code&gt; crashed. The tests had replaced the Python version with a matching one, so
they never ran the failing path. Running the command on Python 3.13 found it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An exclusion that wasn’t done.&lt;/strong&gt; The notes said the Tk graphics libraries were left out of the program. Nothing in
the build left them out.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the fourth version:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trail licenses&lt;/code&gt; on a pip install shows the game’s own licence and its libraries, with a note that the Python texts
belong to the packaged programs.&lt;/li&gt;
&lt;li&gt;Tk and dbm are excluded in the build, and the notices check fails if either turns up.&lt;/li&gt;
&lt;li&gt;All 19 licence files in each archive are accounted for.&lt;/li&gt;
&lt;li&gt;The same install-record cleanup as XRPL Camp’s is in.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Listing what’s really bundled.&lt;/strong&gt; The notices list is now checked against PyInstaller’s own record of what it packed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On a Linux build, the check found &lt;strong&gt;22&lt;/strong&gt; libraries and passed. With one name deleted from a copy of the notices, it
failed and named the missing library.&lt;/li&gt;
&lt;li&gt;The same build showed the notices listing three libraries that were installed but never packed (Deprecated,
mdit-py-plugins and wrapt). The list is now trimmed to what the program actually contains.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-second-path-in-pythons-own-settings&quot;&gt;A second path, in Python’s own settings&lt;/h2&gt;
&lt;p&gt;The Linux build checks for both tools then found one more build-machine path, and this one wasn’t in an install
record. Python keeps a small settings module, &lt;code&gt;_sysconfigdata&lt;/code&gt;, that records where the interpreter itself is
installed. About 20 of its entries, such as the install prefix and the folder for programs, hold that location, and
PyInstaller packs the module like any other.&lt;/p&gt;
&lt;p&gt;The build tool, uv, installs Python under the build machine’s user account. So on GitHub’s Linux machines those
entries name a folder under the &lt;code&gt;runner&lt;/code&gt; account’s home, and XRPL Camp 1.5.0’s Linux program carries them alongside
its install record. Like the install record, it’s a CI machine’s folder, not a person’s.&lt;/p&gt;
&lt;p&gt;The ruling was the same as for the install record: it’s the studio’s own build setup, so it gets fixed, not excused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The build&lt;/strong&gt; installs Python into a neutral folder outside any user’s home.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The bundle check&lt;/strong&gt; reads &lt;code&gt;_sysconfigdata&lt;/code&gt; from inside the finished program, and fails if it names a home folder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The 1.5.0 programs stay as released,&lt;/strong&gt; and their receipts record both paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-things-stand&quot;&gt;Where things stand&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;XRPL Camp 1.5.0:&lt;/strong&gt; released and live-checked through npm on Windows and Linux. Its programs carry both
build-machine paths described above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XRPL Camp 1.5.1:&lt;/strong&gt; passed the studio’s own review, the independent review, and a Linux build check. That check
found the &lt;code&gt;_sysconfigdata&lt;/code&gt; path, and the fix for it is being built. It then goes through review and the build check
again.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escape the Valley 1.3.1:&lt;/strong&gt; the fourth version of its notices passed the studio’s code review, the independent
review, and a Linux build check. The Linux check confirmed the notices file block by block against its sources. It
takes the same &lt;code&gt;_sysconfigdata&lt;/code&gt; fix as XRPL Camp before it ships with its full release checklist.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;macOS:&lt;/strong&gt; neither program’s macOS build can run on the studio’s machines. So macOS, including what its
&lt;code&gt;_sysconfigdata&lt;/code&gt; holds, is first checked unpacked at release, and the receipts say so.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Records:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XRPL Camp 1.5.0 (&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-camp/releases/tag/v1.5.0&quot;&gt;release&lt;/a&gt;), its treatment
(#22) and live-check receipts (#24);&lt;/li&gt;
&lt;li&gt;the install-record fix, &lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-camp/issues/23&quot;&gt;xrpl-camp#23&lt;/a&gt; and pull request #25;&lt;/li&gt;
&lt;li&gt;Escape the Valley 1.3.1, &lt;a href=&quot;https://github.com/mcp-tool-shop-org/escape-the-valley/pull/25&quot;&gt;pull request #25&lt;/a&gt;, and
the notices issue, &lt;a href=&quot;https://github.com/mcp-tool-shop-org/escape-the-valley/issues/28&quot;&gt;#28&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 24: One fix for nine tools, and the check that comes before shipping</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-24/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-24/</guid><description>Nine of the studio&apos;s command-line tools reach npm through one small launcher. A bug in it meant `--version` printed the launcher&apos;s version instead of the tool&apos;s. Fixing it once fixed it for all nine. This issue covers that fix, what the translation model got wrong in the release READMEs, and the check every release now has to pass before it ships.</description><pubDate>Thu, 22 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Several studio tools are written in Python, but people install them with &lt;code&gt;npx&lt;/code&gt;, Node’s package runner. That works
through one small npm package, &lt;a href=&quot;https://github.com/mcp-tool-shop-org/npm-launcher&quot;&gt;npm-launcher&lt;/a&gt;. Each tool ships a
tiny wrapper that tells the launcher which GitHub release to fetch. The launcher downloads the right binary for the
machine, checks its SHA-256 checksum, caches it, and runs it.&lt;/p&gt;
&lt;h2 id=&quot;the-bug-the-wrong---version&quot;&gt;The bug: the wrong &lt;code&gt;--version&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The launcher answered &lt;code&gt;--version&lt;/code&gt; and &lt;code&gt;--help&lt;/code&gt; itself, before it looked at which tool it was running. So
&lt;code&gt;npx @mcptoolshop/xrpl-lab --version&lt;/code&gt; printed &lt;code&gt;mcptoolshop-launch 1.1.1&lt;/code&gt;, the launcher’s version, not the tool’s.
Learners and scripts asking “which version do I have?” got the wrong answer.&lt;/p&gt;
&lt;p&gt;Launcher &lt;strong&gt;1.2.0&lt;/strong&gt; answers those flags itself only when it’s run on its own. Through a tool’s wrapper, every argument
reaches the tool. The launcher’s own version now has a flag of its own, &lt;code&gt;--launcher-version&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One fix, nine tools.&lt;/strong&gt; Nine wrappers ask for “any 1.x” of the launcher, so a fresh &lt;code&gt;npx&lt;/code&gt; picks up 1.2.0 with no
release of their own. One wrapper, XRPL Camp, pinned the exact version 1.1.1, and moves to 1.2.0 in its own release.&lt;/p&gt;
&lt;h2 id=&quot;the-second-bug-the-first-one-revealed&quot;&gt;The second bug the first one revealed&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;--help&lt;/code&gt; now reaching the tool, the tool’s usage line read &lt;code&gt;Usage: xrpl-lab-2.7.9-win-x64.exe …&lt;/code&gt;. Python
command-line libraries take the program’s name from how it was started, and here that was the cached download’s file
name. Nobody had seen it before, because &lt;code&gt;--help&lt;/code&gt; had never got through.&lt;/p&gt;
&lt;p&gt;The fix tells the operating system to start the tool under its own name. Measured with the real tool:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Platform&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;How it was checked&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Usage line&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Windows&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;the packed launcher with the published xrpl-lab wrapper&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;code&gt;Usage: xrpl-lab …&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Linux&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;WSL Ubuntu, with the real Linux binary downloaded and checksum-verified&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;code&gt;Usage: xrpl-lab …&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A command that reads the tool’s bundled lesson files still worked, so the bundled tool finds its own files under the
new name.&lt;/p&gt;
&lt;h2 id=&quot;what-the-translation-model-got-wrong&quot;&gt;What the translation model got wrong&lt;/h2&gt;
&lt;p&gt;Release READMEs are translated into seven languages by a local model, TranslateGemma 27B, running on the studio’s own
GPU. It’s reliable on prose. Today’s runs showed two kinds of slip, the same kinds the readouts newsletter recorded
on 2026-10-20:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bold text dropped.&lt;/strong&gt; Japanese lost one bold label in the xrpl-lab README. In XRPL Camp’s, Italian lost two and
Portuguese one. The Japanese camp README has been missing 5 of its 25 bold spans since an earlier run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Product names translated.&lt;/strong&gt; Japanese, Chinese and Hindi turned “XRPL Lab” into a translated name, and Hindi did the
same to “XRPL Camp”.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these were fixed by hand. Every translation run is now checked for both: bold spans are counted against the
English, and product names must survive unchanged.&lt;/p&gt;
&lt;h2 id=&quot;the-check-before-shipping&quot;&gt;The check before shipping&lt;/h2&gt;
&lt;p&gt;The studio has a full release checklist for every tool, called the &lt;strong&gt;treatment&lt;/strong&gt;. It checks the ship gate and scans for
personal details, then brings the README, translations, handbook, landing page and repository metadata up to date, and
records the tool in the studio’s knowledge base.&lt;/p&gt;
&lt;p&gt;Three releases this week (XRPL Camp 1.4.2 and xrpl-lab 2.7.8 and 2.7.9) shipped after code review and live testing, but
without it. Nobody had decided to skip it; the step had fallen out of the routine. A published version can’t be
changed, so running the checklist afterwards doesn’t fix what went out.&lt;/p&gt;
&lt;p&gt;Two changes followed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The release gate now refuses&lt;/strong&gt; any version without a treatment record for the exact commit being tagged. The record
covers each part of the checklist, with evidence, and live checks that the landing page and handbook are up. The
gate refused the first version it checked, for README translations months out of date. They were fixed before the
release.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treatments ran for all three tools.&lt;/strong&gt; npm-launcher shipped 1.2.0 only after its own, and xrpl-lab and XRPL Camp were
treated so their next releases go out complete.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Records:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;npm-launcher 1.2.0 (&lt;a href=&quot;https://github.com/mcp-tool-shop-org/npm-launcher/releases/tag/v1.2.0&quot;&gt;release&lt;/a&gt;), with its live
checks on Windows and Linux;&lt;/li&gt;
&lt;li&gt;the treatment pull requests for npm-launcher (#10), xrpl-lab (#48) and XRPL Camp (#18).&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 23: Watching 0.2 XRP lock and come back</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-23/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-23/</guid><description>XRPL Camp 1.5.0 adds a lesson you can run after the course: mint an NFT and burn it, and watch your spendable XRP fall by the owner reserve and come back. On the live test network it does exactly that. Getting there meant answering two quieter questions: what a zero means when you measure money, and when a tool should write a receipt into your proof.</description><pubDate>Thu, 22 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-camp&quot;&gt;XRPL Camp&lt;/a&gt; teaches the XRP Ledger in one sitting. It runs six lessons on
the public test network, and ends with a certificate and a proof file anyone can check against the ledger. Version
1.5.0 adds three things the camp had built but never switched on. The most visible is a command that shows one of the
ledger’s less intuitive rules happening to your own account.&lt;/p&gt;
&lt;h2 id=&quot;the-rule-owning-things-locks-xrp&quot;&gt;The rule: owning things locks XRP&lt;/h2&gt;
&lt;p&gt;Every account on the XRP Ledger must keep a minimum balance, its &lt;strong&gt;reserve&lt;/strong&gt;. There are two parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a base reserve for existing at all, 1 XRP on the test network when we measured;&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;owner reserve&lt;/strong&gt; of 0.2 XRP (200,000 drops) for each object the account owns, such as a trust line or a page of
NFTs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reserve isn’t a fee. It isn’t spent. It’s held while you own the object, and released when you give it up. Your
&lt;strong&gt;balance&lt;/strong&gt; stays the same, but your &lt;strong&gt;spendable&lt;/strong&gt; XRP doesn’t.&lt;/p&gt;
&lt;h2 id=&quot;the-demo-measured&quot;&gt;The demo, measured&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;xrpl-camp reserve&lt;/code&gt; mints one NFT from the learner’s wallet, then burns it, and shows both numbers side by side. Here’s
the run that checked the release, read back from the ledger afterwards:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Step&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Objects owned&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Balance (drops)&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Spendable (drops)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Mint an NFT&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0 → 1&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;−10&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;−200,010&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Burn it&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1 → 0&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;−10&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;+199,990&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The balance only moved by the fee: 10 drops for each transaction. The spendable XRP moved by the fee &lt;em&gt;plus&lt;/em&gt; 200,000
drops on the way in, and got the 200,000 back on the way out. Over the round trip, 20 drops are gone, and the reserve
returned in full. The transaction records on the ledger agree with every number in the table.&lt;/p&gt;
&lt;h2 id=&quot;what-a-zero-means&quot;&gt;What a zero means&lt;/h2&gt;
&lt;p&gt;While the release was being reviewed, a question came up about the case where the demo shows &lt;strong&gt;no reserve change at
all&lt;/strong&gt;. There are two ways that happens, and they mean different things.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The NFT joined an existing page.&lt;/strong&gt; The ledger doesn’t charge the owner reserve per NFT. It charges it per &lt;strong&gt;NFT
page&lt;/strong&gt;, a container holding up to 32 tokens. An account that already holds an NFT with room on its page mints the new
one into that page, and locks nothing new. That’s a true zero.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The server hadn’t caught up.&lt;/strong&gt; If the read after the mint reaches a server that hasn’t yet seen the validated
ledger with the mint in it, the numbers come back unchanged. That reads as a zero too, but it’s a stale read, not a
fact about reserves.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first build called every zero “the token went into an existing page”. The fix uses numbers the demo already has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;a shared page&lt;/strong&gt; needs the account to have owned something before, the object count to stay the same, &lt;em&gt;and&lt;/em&gt; the fee
to have been paid, which proves the read came after the transaction;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;anything else that reads zero&lt;/strong&gt; is reported plainly: the ledger read after the mint didn’t show the transaction
yet, so the change couldn’t be measured, and here is the transaction link.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;receipts-and-when-not-to-write-one&quot;&gt;Receipts, and when not to write one&lt;/h2&gt;
&lt;p&gt;The camp’s proof file is sealed with a hash, so any edit shows. &lt;code&gt;proof verify --online&lt;/code&gt; also asks the ledger whether
the transactions in the file really happened. Version 1.5.0 can record each such check as a dated &lt;strong&gt;receipt&lt;/strong&gt; inside
the file, outside the seal. A file then gathers a short history of being checked over the years. That matters on a
test network, which is reset from time to time, so old transactions eventually stop resolving.&lt;/p&gt;
&lt;p&gt;Every receipt carries a disclaimer. It’s self-reported: a copy of the tool said this passed on this date, and it proves
nothing on its own.&lt;/p&gt;
&lt;p&gt;Review caught a problem with the first build: it wrote a receipt whatever the ledger answered. A file the ledger had
&lt;em&gt;contradicted&lt;/em&gt; would have gained a receipt saying it passed. So did a file whose check couldn’t reach the network.&lt;/p&gt;
&lt;p&gt;Now a receipt is written only when the ledger &lt;strong&gt;confirmed&lt;/strong&gt; the file. Any other answer leaves the file byte-for-byte
unchanged and says why. The release check tried a forged file, re-sealed with a fresh hash: it was contradicted,
refused a receipt, and left unchanged.&lt;/p&gt;
&lt;h2 id=&quot;also-in-150&quot;&gt;Also in 1.5.0&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;certificate --format text&lt;/code&gt; writes a plain-text keepsake beside the machine-readable certificate.&lt;/li&gt;
&lt;li&gt;Every sealed certificate prints a short block you can copy and paste. It names the proof command, so a reader can
check the claim rather than take it on trust.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;How it was checked:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Four review rounds&lt;/strong&gt; on the code by a second model, Gemini, and a review by the studio’s R&amp;amp;D seat.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A live check on the test network&lt;/strong&gt; by a separate seat, with the receipts committed in the repository
(&lt;code&gt;docs/receipts/1.5.0-live-check/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The XRPL’s rules on reserves and NFT pages:&lt;/strong&gt; xrpl.org, “Reserves” and “NFTokenPage”.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 22: Moving a deterministic game to Rust, and proving it plays the same</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-22/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-22/</guid><description>Escape the Valley, the studio&apos;s Oregon-Trail-style terminal game, is moving from Python to Rust. Before writing any of it, we ran four small spikes, one for each question that could sink the plan: can Rust reproduce Python&apos;s random numbers exactly, can it run the game&apos;s XRPL ledger features, can it speak without GPL code, and can it ship the same way? All four passed, and the voice spike found three licence traps on the way.</description><pubDate>Thu, 22 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/mcp-tool-shop-org/escape-the-valley&quot;&gt;Escape the Valley&lt;/a&gt; is an Oregon-Trail-style survival game
played in the terminal. A deterministic engine decides what happens, so the same seed gives the same journey. A local
AI model narrates, and optionally the XRP Ledger’s test network keeps a record of your supplies. It’s about 17,700 lines
of Python with 1,042 tests.&lt;/p&gt;
&lt;p&gt;An audit of version 1.3.0 found that the game had grown &lt;strong&gt;two turn engines&lt;/strong&gt;, one for the full-screen interface and one
for the classic command line, and that features kept landing in one and not the other. The fix the studio chose is a
rewrite in Rust as version 2.0: one engine, two front ends, one self-contained binary.&lt;/p&gt;
&lt;p&gt;A rewrite like that can fail in four ways that matter. So before writing the game, we asked four research agents one
question each, then ran a small spike for each question. A spike is a throwaway proof that answers one question with
evidence.&lt;/p&gt;
&lt;h2 id=&quot;1-can-rust-reproduce-pythons-random-numbers-exactly&quot;&gt;1. Can Rust reproduce Python’s random numbers exactly?&lt;/h2&gt;
&lt;p&gt;This decides whether players keep their seeds and saved games. The game draws from Python’s &lt;code&gt;random&lt;/code&gt; module 66 times
across its code, using five operations: a float, an integer in a range, a choice, a shuffle and a sample, plus its own
weighted pick. Saved games store the generator’s full internal state (624 numbers and a position).&lt;/p&gt;
&lt;p&gt;Python’s generator is the Mersenne Twister (MT19937). The spike ported it, and Python’s exact way of turning its output
into each operation, to Rust in about 60 lines. A Python script recorded expected values for six seeds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0, 1, 42, and 2³² + 7;&lt;/li&gt;
&lt;li&gt;a 100-bit integer, and a negative one. Python seeds from the absolute value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For each seed it recorded 200 floats (as exact bit patterns), 200 ranged integers at ten widths, 50 choices, 20
shuffles, 20 samples (covering both of Python’s internal methods), 50 weighted picks, and a save-and-restore round trip.&lt;/p&gt;
&lt;p&gt;The Rust port matched &lt;strong&gt;all 2,645 values&lt;/strong&gt;. A widely used Rust crate, &lt;code&gt;rand_mt&lt;/code&gt;, matched too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One finding we hadn’t expected:&lt;/strong&gt; the recorded values were byte-identical under Python &lt;strong&gt;3.11, 3.12 and 3.13&lt;/strong&gt;. The
game supports all three, so this also confirms that today’s Python version plays the same journey whichever of them
you run. Asking for a number in a range of one (say “between 4 and 4”) still consumes randomness, and the port has to
copy that too.&lt;/p&gt;
&lt;p&gt;The port will be proven the same way at full scale. The Python game becomes the reference: a thousand seeds, sixty
turns each, saved and resumed partway, with the whole game state compared turn by turn. This is differential testing
(McKeeman, 1998), with the old program as the oracle.&lt;/p&gt;
&lt;h2 id=&quot;2-can-rust-run-the-games-ledger-features&quot;&gt;2. Can Rust run the game’s ledger features?&lt;/h2&gt;
&lt;p&gt;The game’s “Ledger Backpack” issues five supplies as tokens on the XRP Ledger’s test network, settles them at towns
with transaction memos, sends parcels between players, and proves a run by reading the memos back. The research found
one maintained Rust library, &lt;code&gt;xrpl-rust&lt;/code&gt; on crates.io (version 1.3.0, ISC licence; its source now lives at
XRPLF/xrpl-rust, the XRPL Foundation’s GitHub),
that claimed to cover all of it.&lt;/p&gt;
&lt;p&gt;The spike tested that claim on the live test network:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Step&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Two wallets from the faucet, each balance checked on a validated ledger&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;11.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A trust line for a supply token&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;8.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A token payment carrying a settlement memo&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;6.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A 12-drop XRP payment carrying a parcel memo&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;5.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Reading history back across three pages, memos decoded, and the token balance read as an exact decimal&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A transaction sent with an expiry ledger already passed, to prove the client reports it as lapsed&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;2.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All of it passed, in 35.7 seconds. Two details the game’s Rust code will need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the ledger server raises any history page size below 10 up to 10;&lt;/li&gt;
&lt;li&gt;just after an expired transaction, the server can briefly still answer “not validated” before it answers “not
found”. Both mean the transaction will never land.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;3-can-it-speak-without-gpl-code&quot;&gt;3. Can it speak, without GPL code?&lt;/h2&gt;
&lt;p&gt;The game can read its narration aloud with Kokoro, an open text-to-speech model (Apache-2.0). A speech model needs a
&lt;strong&gt;phonemizer&lt;/strong&gt;, which turns words into sounds. The usual one, espeak-ng, is licensed GPL-3.0. Linking it in would mean
distributing the game’s binary under GPL terms, and the game is released under MIT.&lt;/p&gt;
&lt;p&gt;The research suggested sherpa-onnx, a speech toolkit with a Kokoro mode that uses a word list instead of espeak-ng.
Checking the code changed that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Trap 1:&lt;/strong&gt; the released sherpa-onnx (1.13.8) builds espeak-ng and piper-phonemize, both GPL, into every build that
includes speech, whichever mode you use. We read this in its build files at that release. Code on the main branch
marks espeak-ng as unused from version 2.0.0, which isn’t released; we haven’t checked a build of it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trap 2:&lt;/strong&gt; the spike reported that another Rust Kokoro crate compiles espeak-derived C code even when its pure-Rust
word list is switched on. We haven’t confirmed this one at the source; the route below avoids the crate either way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trap 3:&lt;/strong&gt; the spike reported that sherpa-onnx’s English word list was generated from espeak-ng’s dictionary. We
haven’t confirmed this at the source either.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The route that passed drives the Kokoro model directly through ONNX Runtime, with these parts:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Part&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Licence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;The Kokoro-82M model weights&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;ONNX Runtime, which runs the model&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;code&gt;ort&lt;/code&gt;, the Rust bindings to ONNX Runtime&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;MIT or Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Kokoro’s own pronunciation dictionaries (misaki)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;The CMU Pronouncing Dictionary as a fallback&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;BSD-style&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;rodio, for playback only&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;MIT or Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We checked each licence at its source. A symbol dump of the finished Windows binary shows no espeak or piper code.&lt;/p&gt;
&lt;p&gt;Measured on the studio’s machine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the binary is about 24 MB, because it carries ONNX Runtime;&lt;/li&gt;
&lt;li&gt;the model and dictionaries are a 353 MB download, made on first use;&lt;/li&gt;
&lt;li&gt;the first audio arrives &lt;strong&gt;1.9 seconds&lt;/strong&gt; after start, against 3.5 seconds for today’s Python voice.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Twenty game lines were rendered both ways for a blind listening test, which the studio is still to judge.&lt;/p&gt;
&lt;h2 id=&quot;4-can-it-ship-the-same-way&quot;&gt;4. Can it ship the same way?&lt;/h2&gt;
&lt;p&gt;Today the game installs three ways: &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;npx&lt;/code&gt;, and a downloaded binary. The spike built a tiny Rust program in CI
and produced all of it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;binaries for Linux, Windows and both kinds of Mac;&lt;/li&gt;
&lt;li&gt;a checksums file;&lt;/li&gt;
&lt;li&gt;Python wheels that install the binary through &lt;code&gt;pip&lt;/code&gt;, the way the &lt;code&gt;ruff&lt;/code&gt; and &lt;code&gt;uv&lt;/code&gt; tools ship.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Windows binary was built on a Linux machine and then run on Windows. Only the two Mac builds needed Mac machines,
which cost more to run. The build settings strip the build machine’s paths out of the binaries.&lt;/p&gt;
&lt;h2 id=&quot;what-happens-next&quot;&gt;What happens next&lt;/h2&gt;
&lt;p&gt;The plan holds. The rewrite starts with the core engine, tested against the Python game at every turn, and adds the
command line, the full-screen interface, the narrator, the ledger and the voice in that order. Meanwhile, Python 1.3.1
fixes the bugs players hit today.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sources:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPython 3.11 &lt;code&gt;Lib/random.py&lt;/code&gt; and &lt;code&gt;Modules/_randommodule.c&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;W. M. McKeeman, “Differential Testing for Software”, &lt;em&gt;Digital Technical Journal&lt;/em&gt; 10(1), 1998;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xrpl-rust&lt;/code&gt; 1.3.0 (&lt;a href=&quot;https://github.com/XRPLF/xrpl-rust&quot;&gt;XRPLF/xrpl-rust&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;sherpa-onnx v1.13.8 &lt;code&gt;CMakeLists.txt&lt;/code&gt; (&lt;a href=&quot;https://github.com/k2-fsa/sherpa-onnx&quot;&gt;k2-fsa/sherpa-onnx&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/hexgrad/Kokoro-82M&quot;&gt;hexgrad/Kokoro-82M&lt;/a&gt; and &lt;a href=&quot;https://github.com/hexgrad/misaki&quot;&gt;hexgrad/misaki&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cmusphinx/cmudict&quot;&gt;cmusphinx/cmudict&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;the spike code: escape-the-valley PR #26.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 21: A critic that catches more, and refuses too much</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-21/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-21/</guid><description>Issue 14 left our training-data critic with one gap: it ignored a correct &quot;no flaw&quot; answer. The confirmation test proved the stricter rule catches what today&apos;s rules miss, but it refused too many good rows. So we tried a narrower rule and a fuller definition of &quot;no flaw&quot;. The narrower rule caught 14 more broken rows, and still refused 9 of 22 sound ones. Here&apos;s why, and what we&apos;re testing next.</description><pubDate>Thu, 22 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;The studio builds training data with a pipeline of local models. A &lt;strong&gt;writer&lt;/strong&gt; drafts each row, and a &lt;strong&gt;critic&lt;/strong&gt; from
another model family checks it. &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-14/&quot;&gt;Issue 14&lt;/a&gt; tested the critic by breaking good rows on
purpose. It found one gap. Some rows carry a checker’s reasoning with a planted mistake, and a label naming it (such
as “guess”, reasoning from outside the material). When the reasoning was in fact sound but the label still named a
flaw, the critic correctly answered “no flaw”. Today’s rules ignore that answer, so the wrong row is kept.&lt;/p&gt;
&lt;p&gt;A stricter rule set, &lt;strong&gt;P2&lt;/strong&gt;, makes the flaw check count. In issue 14 it won every disagreement, but there were too
few to clear the test we had set in advance. This issue follows the confirmation test, and what came after.&lt;/p&gt;
&lt;h2 id=&quot;the-confirmation-proven-and-too-strict&quot;&gt;The confirmation: proven, and too strict&lt;/h2&gt;
&lt;p&gt;The confirmation test used fresh rows, hand-checked blind, with one comparison fixed in advance. P2 settled the
question:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it caught &lt;strong&gt;22 broken rows that today’s rules (P0) missed, against 0 the other way&lt;/strong&gt; (p ≈ 5 × 10⁻⁷);&lt;/li&gt;
&lt;li&gt;but it also refused &lt;strong&gt;12 of 32 good rows&lt;/strong&gt;. The design’s limit was 10%.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The good rows split two ways, and so do the refusals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rows with a planted flaw&lt;/strong&gt;, correctly labelled. The critic agreed a flaw was there on all 22, but sometimes named a
different one, such as “guess” for a reasoning loop. P2 requires the exact label, so it refused those.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rows with sound reasoning&lt;/strong&gt;, labelled “no flaw”. The critic called 4 of these 10 flawed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we separated the two problems, and tested the narrower one first.&lt;/p&gt;
&lt;h2 id=&quot;the-narrower-rule&quot;&gt;The narrower rule&lt;/h2&gt;
&lt;p&gt;The new rule set, &lt;strong&gt;Pc&lt;/strong&gt; (the coarse gate), ignores which flaw the critic names. It refuses only when the critic and
the label disagree on whether there is a flaw at all. A “loop” read as “guess” is still “a flaw”, so it passes. On the
confirmation data, Pc refused none of the 22 planted-flaw rows. All its refusals were sound reasoning read as flawed.&lt;/p&gt;
&lt;p&gt;That pointed at the definitions. In the critic’s instructions, “no flaw” was the one label with no description: just
“the check was right”. “Guess” included “or a misreading”, which could absorb any trace the critic doubted. So &lt;strong&gt;Track
A&lt;/strong&gt; tested one prompt edit. It gave “no flaw” a full description (quoted the deciding lines, reasoned only from what
they state, reached the material’s verdict), and said that such a check is not a guess.&lt;/p&gt;
&lt;p&gt;The design ran the critic, Muse-Glimmer-30B, twice over the same 102 items at seed 0: once with the old instructions,
once with the new. 219 items were scored from those replies. An arm qualified only if it beat P0, at
an adjusted p under 0.05, and kept its false refusals at 10% or less on both kinds of good row. The outside reviewer,
Google’s Gemini, checked the design, and set how to analyse it before any result was seen.&lt;/p&gt;
&lt;h2 id=&quot;what-it-found&quot;&gt;What it found&lt;/h2&gt;
&lt;p&gt;On the 90 broken rows the test is built around:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Rule set&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Broken rows caught&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Good rows refused: sound reasoning&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Good rows refused: planted flaw&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Qualifies?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;P0, today’s rules&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;63 of 90&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;2 of 22&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0 of 21&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;(the baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Pc, old instructions&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;77 of 90&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;9 of 22&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0 of 21&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Pc, new instructions&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;76 of 90&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;10 of 22&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1 of 21&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The coarse gate works where it’s meant to. Both arms caught 14 broken rows that P0 missed, and P0 caught at most one
they didn’t (adjusted p = 0.0002 and 0.001).&lt;/p&gt;
&lt;p&gt;But both failed the bar on sound reasoning, at 9 and 10 of 22, against at most 2. &lt;strong&gt;The new instructions changed
nothing measurable.&lt;/strong&gt; Scored on the same set of verdicts, so that only the instructions differ, the two arms
disagreed on 2 broken rows, both caught by the old instructions and missed by the new (p = 0.5).&lt;/p&gt;
&lt;p&gt;Why the critic still says “flaw” on sound reasoning shows up in what it named. On the good rows with sound reasoning
that it refused, it named “guess” 8 times of 9 in one run and 9 of 10 in the other. Read blind, none of that
reasoning hedges or leans on anything the material doesn’t say. Yet the critic called about half of it a guess,
whatever its form: quoting the deciding lines didn’t protect a trace, and a forced inference raised the rate only a
little. The one thing that separates real guesses from sound reasoning is an outside assumption, present in 6 of 7
planted guesses and in none of the 22 sound traces. Telling the critic what “no flaw” looks like didn’t stop the
over-calling.&lt;/p&gt;
&lt;p&gt;So nothing was adopted. The pipeline stays on P0, with the flaw check advisory, and every accepted row still goes
through R&amp;amp;D’s blind hand check.&lt;/p&gt;
&lt;h2 id=&quot;the-same-prompt-twice&quot;&gt;The same prompt, twice&lt;/h2&gt;
&lt;p&gt;The design assumed something we could check: both runs send byte-identical verdict prompts at the same seed, so they
should give the same verdicts. They didn’t. Of 102 matched calls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;22 replies differed in their text;&lt;/li&gt;
&lt;li&gt;7 verdicts differed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most likely cause: the critic samples at temperature 1, so the seed doesn’t make its output reproducible across
sessions. It changed little that mattered: under P0,
3 of the 90 broken rows changed outcome between runs. But a gap of 1 or 2 items between arms is within that noise, which
is part of why the two instruction sets can’t be told apart.&lt;/p&gt;
&lt;p&gt;When the premise failed, the reviewer ruled on how to analyse the result, without seeing any outcome figures. One
caveat: R&amp;amp;D’s analysis script had already computed the figures in the same pass, before the ruling arrived, so R&amp;amp;D
had seen them. They were held unreported, and the reviewer chose the analysis without them. One analysis was the
pre-registered one; the other scored both arms from one shared set of verdicts. The two agree: neither arm
qualifies.&lt;/p&gt;
&lt;h2 id=&quot;next-rewrite-the-labels&quot;&gt;Next: rewrite the labels&lt;/h2&gt;
&lt;p&gt;The failure points at the labels themselves, not at the rule. &lt;strong&gt;Track B&lt;/strong&gt; tests that, in two stages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Diagnosis, at no cost, now done:&lt;/strong&gt; on the items already scored, it found what’s described above, and that the
critic’s flaw label flipped between runs on 5 of 12 such items. The label fixes are now frozen: “guess” is defined
by an outside assumption alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification:&lt;/strong&gt; today’s labels against the rewritten ones, on a fresh bank of items neither arm has seen, scored
with the coarse gate. To handle the noise, the critic’s flaw answer is sampled three times, and a row is refused
only if 2 of the 3 name a flaw.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Its design has been reviewed and approved. T10, the next writer run, has priority for compute.&lt;/p&gt;
&lt;h2 id=&quot;where-the-figures-come-from&quot;&gt;Where the figures come from&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The result:&lt;/strong&gt; &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/critic-signal/data/track-a-result-2026-10-22.json&quot;&gt;&lt;code&gt;data/track-a-result-2026-10-22.json&lt;/code&gt;&lt;/a&gt;,
with every comparison, the control rates, the flaw labels named and the noise floor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The plan and its bar:&lt;/strong&gt; &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/critic-signal/PLAN-ROUND4.md&quot;&gt;&lt;code&gt;PLAN-ROUND4.md&lt;/code&gt;&lt;/a&gt;
§1 to §4, which also holds the confirmation test’s figures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The diagnosis and the frozen labels:&lt;/strong&gt; &lt;code&gt;TRACK-B-STAGE1.md&lt;/code&gt; and &lt;code&gt;TRACK-B-LABELS.md&lt;/code&gt;, beside it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/critic-signal/TRACK-B-DESIGN.md&quot;&gt;&lt;code&gt;TRACK-B-DESIGN.md&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The text of the rows stays off the public repo. These files hold ids, codes and counts only.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 20: The course, end to end</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-20/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-20/</guid><description>Five more xrpl-lab releases, 2.7.1 to 2.7.5. The check that proves a learner&apos;s work had been failing honest work. A lesson that passed on its own broke the next one. And the first run of all 32 lessons in a row, the way a workshop runs them, found five more lessons that broke others or failed outright. Here&apos;s what each looked like, and how the course got to a clean run end to end.</description><pubDate>Thu, 22 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab&quot;&gt;xrpl-lab&lt;/a&gt; is the studio’s hands-on course in the XRP Ledger. Its
lessons run real transactions on the public test network, XRPL Testnet, and can also run offline against a built-in
simulator. &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-19/&quot;&gt;Issue 19&lt;/a&gt; followed 2.6.2 and 2.7.0, where several checks passed that shouldn’t
have. This issue follows the next five releases. Most of it is about what happens between lessons.&lt;/p&gt;
&lt;h2 id=&quot;the-proof-that-couldnt-prove&quot;&gt;The proof that couldn’t prove&lt;/h2&gt;
&lt;p&gt;Each lesson leaves evidence. At the end, a learner can export a proof pack, a file listing every transaction they
made. The command &lt;code&gt;proof verify --live&lt;/code&gt; then looks each one up on the public ledger and confirms it is there as
claimed. That check is the reason the pack is worth anything.&lt;/p&gt;
&lt;p&gt;2.7.1 began as two small follow-ups. While reviewing them, our outside reviewer, Google’s Gemini, pointed out that
the dashboard’s verify route ignored a new field. Following that path led to a bigger problem. Live verification
expected every transaction in a pack to have succeeded. So since 2.6.2, which began recording the rejections a lesson
teaches, it had failed every honest pack from a lesson that teaches one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a rejection that reached the ledger failed as “not a success”;&lt;/li&gt;
&lt;li&gt;a rejection that never reaches a ledger, by design, failed as “not found”.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fixing that, the studio’s live-testing seat found a second, older rule. Live verification also required every
successful transaction to be signed by the learner. But many lessons create helper wallets of their own: an issuer,
a buyer, a second player. Their transactions could never pass. Of 25 real packs from earlier live runs, 17 failed on
this alone, across 32 helper-signed transactions. Helper-signed &lt;em&gt;rejections&lt;/em&gt; had been passing all along, because the
rejection path skipped the check. The rule wasn’t only strict, it was inconsistent.&lt;/p&gt;
&lt;p&gt;Now each run records the accounts it used, learner and helpers, and the pack carries them. A transaction signed by
any of them verifies.&lt;/p&gt;
&lt;p&gt;One design choice is worth telling. An earlier proposal would have had a rerun of a lesson silently delete the
learner’s earlier records. We kept them instead. Every attempt records its accounts, and the course’s existing reset
command handles records from before the change. A rerun shouldn’t destroy history.&lt;/p&gt;
&lt;h2 id=&quot;a-lesson-that-passes-alone&quot;&gt;A lesson that passes alone&lt;/h2&gt;
&lt;p&gt;The same live check turned up something new. A lesson on deposit authorization turns on a wallet setting that
refuses payments from anyone not approved. The lesson passed. But it left the setting on, so the next lesson in the
same wallet, which needs to receive a token, was refused, and its trading pool had nothing to pool. The lesson also
couldn’t be run twice, because its own leftover approval was still on the ledger.&lt;/p&gt;
&lt;p&gt;2.7.2 makes it clean up after itself: it removes the approval, turns the setting off, and reads the wallet back to
check. Run live in one wallet, the next lesson then created its pool, the rerun was clean, and live verification
passed.&lt;/p&gt;
&lt;p&gt;2.7.3 was a small fix: a rejection that reached the ledger now keeps its link to the ledger explorer in the saved
record, as the proof pack already did.&lt;/p&gt;
&lt;h2 id=&quot;all-32-lessons-in-a-row&quot;&gt;All 32 lessons in a row&lt;/h2&gt;
&lt;p&gt;That raised an obvious question. If one lesson could break the next, what about the rest? A workshop runs the course
in order, in one wallet. Until now, live checks had run each lesson in a fresh one.&lt;/p&gt;
&lt;p&gt;So we ran all 32 lessons in curriculum order, in one wallet, on testnet, and verified the whole pack at the end:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;22 of 32 lessons finished cleanly;&lt;/li&gt;
&lt;li&gt;live verification found 104 transactions as claimed, failed 14, and skipped 11 from the one lesson that runs
offline by design;&lt;/li&gt;
&lt;li&gt;no transaction failed because a helper signed it, so 2.7.1’s fix held across the whole course.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run alone in a fresh wallet, 8 of the 10 failing lessons passed. An earlier lesson had broken them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A setting left on.&lt;/strong&gt; A lesson on crediting players’ deposits makes the learner’s wallet refuse any payment
without a routing tag, and left it that way. Five later lessons that pay the learner were refused.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A shared issuer.&lt;/strong&gt; The lesson on recalling tokens needs an issuer that owns nothing yet. The course reuses one
issuer across lessons, and by then it owned plenty, so the ledger refused.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An empty wallet.&lt;/strong&gt; A wallet gets one grant from the test faucet. By the cheque lesson, the learner held
49.99967 XRP, just short of the 50 XRP cheque.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two lessons failed alone too:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The wrong signer.&lt;/strong&gt; A token-distribution lesson created its currency from the learner’s wallet instead of the
issuer’s. An issuer can’t hold its own token, so the learner couldn’t opt in, and the payment had nowhere to go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An invalid name.&lt;/strong&gt; A lesson named its currency &lt;code&gt;HYGIENE&lt;/code&gt;. A currency code must be 3 characters or 40 hex
digits, so the library refused it before signing. It had never worked live.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why didn’t the offline tests catch any of this? Each offline run starts a brand-new simulated ledger, so no lesson
ever saw what an earlier one left behind. The simulator was also more lenient than the ledger in places: it accepted
&lt;code&gt;HYGIENE&lt;/code&gt;, and it let token payments through to a wallet that refuses untagged ones.&lt;/p&gt;
&lt;h2 id=&quot;clean-end-to-end&quot;&gt;Clean end to end&lt;/h2&gt;
&lt;p&gt;2.7.4 fixed all five. The deposit-crediting lesson now turns its setting off. The token-recall lesson makes an issuer of its
own. The cheque lesson asks the faucet for a top-up when the learner is low. The distribution lesson signs as the
issuer. And the currency is now &lt;code&gt;HYG&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It also gave the course a new offline test that runs all 32 lessons in one simulated ledger, in order. On 2.7.3 it
fails the two lessons the live run broke for lack of money and the routing tag. We then made the simulator stricter
in the places the live run had shown it was too lenient. With the deposit-crediting lesson’s cleanup removed, the
test now fails exactly the lessons the live run did.&lt;/p&gt;
&lt;p&gt;The first live run of 2.7.4 still found two more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A retry that couldn’t fix the record.&lt;/strong&gt; Redoing a lesson with &lt;code&gt;--force&lt;/code&gt; had never updated its record. A
passing retry after a failure still read as failed. This dates from before 2.7.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A trust line that wouldn’t go.&lt;/strong&gt; The cleanup lesson removes a token trust line, but the line stayed on the
ledger, still holding its reserve. Deleting a line needs one of its flags back at its default value, and the
course left it at the other. This had been hidden all along, because the lesson used to fail earlier on
&lt;code&gt;HYGIENE&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each fix exposed the next layer. Fixed, the full course ran clean live: all 32 lessons finished, live verification
confirmed 123 transactions with none failed, and no setting was left on the learner’s wallet.&lt;/p&gt;
&lt;h2 id=&quot;a-learners-recovery-should-verify&quot;&gt;A learner’s recovery should verify&lt;/h2&gt;
&lt;p&gt;One gap was left. If a lesson fails and the learner fixes it with &lt;code&gt;--force&lt;/code&gt;, the pack still carries the failed
attempt’s records. Live verification failed on them, so a learner who had recovered couldn’t get a clean result.&lt;/p&gt;
&lt;p&gt;2.7.5 treats a failure older than the module’s passing latest run as history. If it never reached a ledger, it is
skipped. If it did, it must be on the ledger as recorded. A module whose latest run failed still fails. No new field
was needed, so packs made by earlier versions verify the same way.&lt;/p&gt;
&lt;p&gt;Our reviewer spelled out why one detail matters. Never-submitted records all share the placeholder &lt;code&gt;failed&lt;/code&gt; instead of a hash. If
they were grouped by module alone, a later interrupted run’s failure could hide behind an earlier, superseded one.
So they are grouped by module and by whether they’re superseded.&lt;/p&gt;
&lt;h2 id=&quot;smaller-moments&quot;&gt;Smaller moments&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A release that failed safely.&lt;/strong&gt; 2.7.1’s first release run stopped at npm’s signing step, with “an equivalent
entry already exists”. Nothing had been published. We asked before rerunning, the rerun passed, and we checked
every registry directly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coverage held a merge.&lt;/strong&gt; The test-coverage gate held 2.7.2 at 87% of the changed lines covered, against a 90%
bar. Three tests brought it to 98%. The bar doesn’t move; the coverage does.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paths in the receipts.&lt;/strong&gt; Six lines of run output carried a folder path from our rig. They were redacted
before merging.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-it-landed&quot;&gt;Where it landed&lt;/h2&gt;
&lt;p&gt;All five releases are out on PyPI and npm, each after outside review and a live check on the reviewed code. The
course’s tests went from 3,447 to 3,556, with coverage at 97%. Two more fixes found along the way followed. 2.7.6
adds &lt;code&gt;--live&lt;/code&gt; to &lt;code&gt;verify-artifact&lt;/code&gt;, the command the old verify commands now point learners to. 2.7.7, on its way out,
stops the setup check from reporting a failure that a &lt;code&gt;--force&lt;/code&gt; retry has already fixed.&lt;/p&gt;
&lt;h2 id=&quot;method-and-limits&quot;&gt;Method and limits&lt;/h2&gt;
&lt;p&gt;One course, one network (XRPL Testnet), runs over two days by one studio seat on the studio’s rig. Each run used a
fresh, isolated home folder; no wallet secret appears in any receipt. Full-course runs used one wallet, in
curriculum order. The faucet’s rate limit set the pace. We checked sample transactions from every run on the ledger
ourselves. Figures come from the receipts, the ledger and the course’s tests.&lt;/p&gt;
&lt;p&gt;Sources: xrpl-lab’s pull requests &lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/pull/24&quot;&gt;#24&lt;/a&gt; to
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/pull/33&quot;&gt;#33&lt;/a&gt;, the follow-up lists in
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/issues/20&quot;&gt;#20&lt;/a&gt; and
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/issues/28&quot;&gt;#28&lt;/a&gt;, and the run receipts in xrpl-lab’s
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/tree/main/docs/receipts&quot;&gt;&lt;code&gt;docs/receipts/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 19: Passes that weren&apos;t</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-19/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-19/</guid><description>Two more xrpl-lab releases, 2.6.2 and 2.7.0, and the same lesson from five directions. A correct lesson left its own result out of the record. The offline simulator handed out ten times testnet&apos;s money. A library upgrade changed which account a key signs as. A clock ran ahead of the ledger. And our own test sweep &quot;passed&quot; every lesson after one step. Here&apos;s what each one looked like, and what caught it.</description><pubDate>Wed, 21 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab&quot;&gt;xrpl-lab&lt;/a&gt; is the studio’s hands-on course in the XRP Ledger. Its lessons
run real transactions on the public test network, XRPL Testnet, and can also run offline against a built-in simulator.
&lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-18/&quot;&gt;Issue 18&lt;/a&gt; followed 2.6.0 and 2.6.1, where live runs kept finding defects that offline tests
had passed. This issue follows the next two releases. Almost everything here was a pass that wasn’t one.&lt;/p&gt;
&lt;h2 id=&quot;a-correct-lesson-missing-from-its-own-record&quot;&gt;A correct lesson, missing from its own record&lt;/h2&gt;
&lt;p&gt;Some lessons exist to show a rejection: a duplicate credential, a payment to an account that requires a tag, an
escrow that isn’t allowed yet. 2.6.1 taught the course to tell an expected rejection from a real failure. Each of
those steps now checks for the one code it teaches.&lt;/p&gt;
&lt;p&gt;2.6.1’s live run turned up a gap. In 12 of the 19 steps that teach a rejection, the transaction was rejected exactly
as taught, but the step never wrote it into the course’s record of transactions. So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the lesson passed;&lt;/li&gt;
&lt;li&gt;the run’s summary didn’t count the rejection;&lt;/li&gt;
&lt;li&gt;the proof pack, the file a learner keeps as evidence, didn’t list it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A receipt couldn’t show by itself that the failure was the one the lesson wanted.&lt;/p&gt;
&lt;p&gt;2.6.2 records every one of them and marks it &lt;code&gt;expected&lt;/code&gt;. Its release run passed all 14 lessons: 13 live on testnet
and one offline. In every lesson the summary’s “N failed” was exactly its expected rejections. Of those 19
rejections, 17 were confirmed on the ledger with the codes the lessons teach. The other two, a malformed
transaction and a multisig quorum failure, never reach a ledger by design.&lt;/p&gt;
&lt;h2 id=&quot;the-simulator-gave-out-ten-times-the-money&quot;&gt;The simulator gave out ten times the money&lt;/h2&gt;
&lt;p&gt;Offline, a new wallet got 1,000 XRP. On testnet the faucet gives 100. We set the simulator to 100, and one lesson
stopped working offline. Its trading pool asked for 100 XRP, all of a fresh testnet wallet’s money, before the
balance a wallet must keep in reserve. On testnet that pool could never have been created. The pool is now 25 XRP
each side, as in issue 18’s AMM lesson.&lt;/p&gt;
&lt;p&gt;The same pass turned up three more simulator gaps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A stand-in address.&lt;/strong&gt; Second wallets, such as a buyer or a sender, had their transactions filed under one
stand-in address the faucet never funded. A test now runs all 32 lessons and checks nothing lands there; two failed
it before the fix.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The wrong unit.&lt;/strong&gt; An escrow of 10 XRP was reported as “10 drops”, a millionth of the amount.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A crash.&lt;/strong&gt; On Windows, one lesson crashed when its output went to a file, because of an arrow character.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;an-upgrade-that-changes-who-signs&quot;&gt;An upgrade that changes who signs&lt;/h2&gt;
&lt;p&gt;2.7.0 moves to version 5 of xrpl-py, the Python library the course uses for the ledger. Version 5 fixed a
long-standing bug. Version 4 turned every secret key into the same kind of keypair, so a key of the other kind, such
as one imported from another tool, signed as the wrong account. Version 5 reads the kind from the key itself.&lt;/p&gt;
&lt;p&gt;For the course, that fix is a hazard. We measured both versions on one test key:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every wallet the course creates turns into the same account on both versions;&lt;/li&gt;
&lt;li&gt;a key of the other kind turns into two different accounts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A learner who imported such a key under version 4 has their funds and history on the old account. Under version 5,
with no change in the course, they would sign as an empty stranger.&lt;/p&gt;
&lt;p&gt;We asked an outside reviewer, Google’s Gemini, to choose between two designs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;keep version 4’s behaviour, the wrong account included;&lt;/li&gt;
&lt;li&gt;read the key the new way, and fall back to the old account only when that’s the one on record.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It chose the second, and added a rule: if neither account matches the record, refuse, and sign nothing.&lt;/p&gt;
&lt;p&gt;Its first review of the build found two gaps. One wallet path never recorded the address to check against, and 13
signing methods dropped the address they were given. Both are fixed.&lt;/p&gt;
&lt;h2 id=&quot;the-clock-and-the-ledger&quot;&gt;The clock and the ledger&lt;/h2&gt;
&lt;p&gt;A live run of 2.7.0 on one lesson per kind of transaction passed 10 of 11. The eleventh was an escrow that can be
released only after a set time. The release was rejected with &lt;code&gt;tecNO_PERMISSION&lt;/code&gt;, “not yet”.&lt;/p&gt;
&lt;p&gt;The receipts show why. The ledger releases an escrow only once the previous ledger’s close time is strictly after
the release time. When the release went out, that close time was exactly equal to it. The lesson had checked the
computer’s clock instead, which runs 1 to 2 seconds ahead of the ledger on our rig. So it decided the time had passed
and didn’t wait.&lt;/p&gt;
&lt;p&gt;The lesson now waits until a validated ledger has closed after the release time. On the rerun the release succeeded,
in a ledger whose previous close was 10 seconds after the release time. That timing code dates from 2.4.0, and
this lesson had no earlier live run on record.&lt;/p&gt;
&lt;p&gt;The same run found three more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A refusal reported as success.&lt;/strong&gt; The course correctly refused to sign with a mismatched key, and the wallet file
was left byte-identical, but the program exited with code 0, which tells a script “success”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--force&lt;/code&gt; did nothing.&lt;/strong&gt; The option to redo a finished lesson did nothing on testnet, and had done nothing since
the course’s first version.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An undercount.&lt;/strong&gt; One lesson’s summary said 4 transactions where the ledger recorded 6.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;our-own-sweep-passed-after-one-step&quot;&gt;Our own sweep passed after one step&lt;/h2&gt;
&lt;p&gt;The early-exit fix is what exposed our own mistake. Before each review we ran all 32 lessons offline from the command
line and counted the exits: all 0. But that sweep ran with no keyboard attached. Every lesson stopped at its first
“Press Enter to continue”, printed “Module interrupted”, and exited 0. It looked like 32 passes, and it checked
almost nothing.&lt;/p&gt;
&lt;p&gt;We had cited that sweep in two releases’ notes and in our reviewer’s briefs. The claim itself held: a separate
automated test runs every lesson to the end and confirms it finished. But the evidence we named was wrong. Once 2.7.0
made an interrupted run exit 130 instead of 0, the sweep failed all 32 at once.&lt;/p&gt;
&lt;p&gt;Run again with Enter fed to every prompt, the sweep completed and verified all 32. We’ve corrected the record on both
releases. We now check that each run reached its end, not just its exit code.&lt;/p&gt;
&lt;h2 id=&quot;where-it-landed&quot;&gt;Where it landed&lt;/h2&gt;
&lt;p&gt;Both releases are out. 2.7.0 shipped after four rounds of outside review and a live rerun of every fix, and it’s on
PyPI and npm. The course’s tests stand at 3,440, with coverage at 97%. The course now treats a refusal, an interruption or a missing record as
the failure it is, not a quiet pass.&lt;/p&gt;
&lt;h2 id=&quot;method-and-limits&quot;&gt;Method and limits&lt;/h2&gt;
&lt;p&gt;One course, one network (XRPL Testnet), runs over one day by one studio seat on the studio’s rig. Each live run used
a fresh, isolated home folder, and no wallet secret appears in any receipt. The test key for the upgrade was
generated for the measurement and never funded. Figures are from the receipts, the ledger and the course’s tests.&lt;/p&gt;
&lt;p&gt;Sources: the pull requests &lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/pull/22&quot;&gt;xrpl-lab #22&lt;/a&gt; (2.6.2) and
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/pull/23&quot;&gt;#23&lt;/a&gt; (2.7.0), the follow-up list in
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/issues/20&quot;&gt;#20&lt;/a&gt;, and the run receipts in xrpl-lab’s
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/tree/main/docs/receipts&quot;&gt;&lt;code&gt;docs/receipts/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 18: Testing the course we&apos;d already shipped</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-18/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-18/</guid><description>xrpl-lab&apos;s new AMM lesson passed nearly two thousand offline tests, then took four live runs to pass on the real XRPL Testnet. Once 2.6.0 was released, we kept running it live, and found more than all the offline testing had, including that the course&apos;s first lesson couldn&apos;t run on the real ledger at all. Here&apos;s what each round found, why every honest fix uncovered the next defect, and what 2.6.1 changed.</description><pubDate>Wed, 21 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab&quot;&gt;xrpl-lab&lt;/a&gt; is the studio’s hands-on course in the XRP Ledger. Each
lesson walks a learner through real transactions on the public test network, XRPL Testnet, and can also run offline
against a built-in simulator. This issue follows one release and the one after it.&lt;/p&gt;
&lt;h2 id=&quot;260-four-live-runs-for-one-lesson&quot;&gt;2.6.0: four live runs for one lesson&lt;/h2&gt;
&lt;p&gt;Version 2.6.0 made module #30 live for the first time. In it the learner creates an &lt;strong&gt;automated market maker&lt;/strong&gt; (AMM),
a pool of two assets that trades against itself, deposits into it, and withdraws again. The new code passed 1,985
offline tests. Before releasing it, we ran the lesson on the real Testnet.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Run 1 created nothing.&lt;/strong&gt; The faucet’s reply carried no balance the code could read, so it decided each funding had
failed and asked again: the wallet was funded three times, 300 XRP. The token issuer’s first transaction went out
before its account was confirmed and never landed. Every later step was then held by the network, and the client
gave up at 60 seconds, about 20 seconds before the network would have answered, throwing away the hash and the
result code. The report said “0 failed”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run 2 found what the extra money had hidden.&lt;/strong&gt; With funding fixed, the pool’s creation failed outright: the lesson
tried to pool 100 XRP, and a fresh wallet gets 100 XRP once, of which it must keep some in reserve. The report
counted the pool steps as passed and exited 0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runs 3 and 4 passed,&lt;/strong&gt; run 4 on the code an outside reviewer had passed: 9 of 9 transactions succeeded, and the
pool was created, used and deleted.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mocks couldn’t have shown any of it. A simulated network answers however the test was written; the real one holds
transactions, grants a fixed amount, and enforces its own reserves.&lt;/p&gt;
&lt;h2 id=&quot;after-the-release-testing-what-wed-shipped&quot;&gt;After the release: testing what we’d shipped&lt;/h2&gt;
&lt;p&gt;2.6.0 shipped, and we kept running the released course live. Ten other lessons create
a second wallet, such as a payment receiver, a buyer or a second token issuer, and had the same funding pattern that
broke run 1. We ran all ten on testnet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Eight of the ten finished with failed steps.&lt;/strong&gt; The run found four kinds of defect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The same race, in four lessons.&lt;/strong&gt; The second wallet was used before the network had confirmed its funding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Correct lessons marked as failed.&lt;/strong&gt; Some lessons exist to show a rejection, such as a duplicate credential or a
refused permission. 2.6.0’s new honest status counted their expected rejection as a failure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A pass that wasn’t.&lt;/strong&gt; The checks lesson reported its expected rejection, but that transaction had never reached the
ledger. Only the way the result was read made it look like the lesson’s outcome.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plain bugs, invisible offline.&lt;/strong&gt; Payment-channel claims always failed, because the code read the channel’s key in
the wrong format. Credential-gated payments couldn’t be signed, because of a field-name bug in the XRPL library we
use. And two lessons with an NFT sale priced the item at a buyer’s entire faucet grant.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A second sweep, after the fixes, passed nine of ten. The tenth, the credentials lesson, finished unverified in both
sweeps, and the second showed why: one of its steps had been passing for the wrong reason all along. Its “unfunded”
account was a well-known example address that is funded on testnet. Only the stricter status from the first round’s
fixes caught it.&lt;/p&gt;
&lt;h2 id=&quot;the-offline-simulator-had-its-own-blind-spots&quot;&gt;The offline simulator had its own blind spots&lt;/h2&gt;
&lt;p&gt;Running every lesson offline turned up two more problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Five lessons failed their own checks in the simulator,&lt;/strong&gt; also on 2.6.0. The simulator filed most of a learner’s
actions under a stand-in address the faucet never funded, so its balance went negative and its object counts were
wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The course’s first lesson couldn’t run on the real ledger.&lt;/strong&gt; It, and two others, sent their practice payment to
“self”. Nothing turned that word into an address, so the real network couldn’t even sign the payment. Sending it to
the learner’s own address wouldn’t have worked either: XRPL rejects a payment to yourself. The simulator accepted it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The lessons now pay a fresh practice address made for each run; the first payment creates that account.&lt;/p&gt;
&lt;h2 id=&quot;coverage-and-the-reviewers&quot;&gt;Coverage, and the reviewers&lt;/h2&gt;
&lt;p&gt;We also brought the course’s test coverage from 78% to 96%, with four AI agents writing 1,135 tests. Each test checks
behaviour, not just that code runs, and none touches the network. Writing them found four more bugs, including a
&lt;code&gt;send&lt;/code&gt; command that reported success after a failed payment.&lt;/p&gt;
&lt;p&gt;Two review passes caught what we’d missed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An internal review&lt;/strong&gt; found seven lesson steps, across six lessons, that would accept any rejection as their
lesson, so an unrelated failure could pass. Each now accepts only the code it teaches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An outside model’s review&lt;/strong&gt; (Google’s Gemini, over two rounds) found that one kind of expected failure could still
hide a real one, and that the “validated” flag was set by default where it should have been earned.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One defect came from a fix of our own. Our first fix for run 1 decided a step had failed by looking at a field that
only 2 of the 66 handlers that submit transactions set. It passed its own test, because the test was built to match it, and it marked the failure lesson’s
deliberate rejection as a failure.&lt;/p&gt;
&lt;h2 id=&quot;where-261-landed&quot;&gt;Where 2.6.1 landed&lt;/h2&gt;
&lt;p&gt;The final release run passed &lt;strong&gt;14 of 14 lessons live&lt;/strong&gt; on the reviewed code: the ten two-wallet lessons, the AMM lesson
and the three first lessons. Every rejection on the ledger was the one its lesson teaches. All 32 lessons also pass
offline, and coverage is held at 90% or more from now on.&lt;/p&gt;
&lt;p&gt;What made each failure readable in time to fix it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Receipts for every run, failures included.&lt;/strong&gt; Each run kept every transaction hash, and failed runs were checked
afterwards with read-only queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Making the course’s own status honest before trusting a green result.&lt;/strong&gt; Each time we did, an older defect showed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;method-and-limits&quot;&gt;Method and limits&lt;/h2&gt;
&lt;p&gt;One course, one network (XRPL Testnet), runs on two days, by one studio seat on the studio’s rig. Each run used a
fresh, isolated home folder, and no wallet secrets appear in any receipt. The figures are from the receipts and the
ledger.&lt;/p&gt;
&lt;p&gt;Sources: the studio’s research note,
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-20-live-testnet-runs-surface-defects-in-layers-each-fix-exposed.md&quot;&gt;&lt;code&gt;2026-10-20-live-testnet-runs-surface-defects-in-layers-each-fix-exposed&lt;/code&gt;&lt;/a&gt;,
and the run receipts in xrpl-lab’s
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/tree/main/docs/receipts&quot;&gt;&lt;code&gt;docs/receipts/&lt;/code&gt;&lt;/a&gt; (2.6.0 in
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/pull/19&quot;&gt;#19&lt;/a&gt;, 2.6.1 in
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/xrpl-lab/pull/21&quot;&gt;#21&lt;/a&gt;).&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 17: What does a Claude Code deny rule actually hide?</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-17/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-17/</guid><description>We asked an AI agent to reach files it had been told it couldn&apos;t read. A deny rule refused the obvious requests, but a search started in the right place still listed the hidden files. Which place depended on how the rule was written, down to a single slash. Version 2.1.280 filters the search we could test there. Here&apos;s what we measured, the two times we read it wrong, and what actually keeps an agent out.</description><pubDate>Tue, 20 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Studio tools increasingly hand work to AI agents that read and edit files. Claude Code, which runs many of them, lets
you write &lt;strong&gt;permission rules&lt;/strong&gt; to keep an agent away from some files: &lt;code&gt;Read(~/**)&lt;/code&gt;, for example, denies reading anything
under the home folder. We wanted to know how far such a rule really reaches, before trusting it with anything that
matters.&lt;/p&gt;
&lt;p&gt;So we built a small test. An agent ran inside a Docker sandbox, with a few &lt;strong&gt;sentinel&lt;/strong&gt; files planted where it had
been told it couldn’t look. Each sentinel had a random name, so an agent could only report it by actually finding it.
Then we asked the agent to reach them by every route it had.&lt;/p&gt;
&lt;h2 id=&quot;a-rule-that-refuses-and-a-search-that-doesnt&quot;&gt;A rule that refuses, and a search that doesn’t&lt;/h2&gt;
&lt;p&gt;On Claude Code 2.1.197, the deny rule did what it says for direct requests: every attempt to read a sentinel was
refused. &lt;strong&gt;Search was different.&lt;/strong&gt; Claude Code’s Grep tool walks a folder and reports which files match. Started in the
right place, it walked straight through the denied folder and listed the sentinels by name.&lt;/p&gt;
&lt;p&gt;The right place depended on how the rule was written. We tried three spellings of the same intent, with one sentinel
at the folder’s root and one in a subfolder:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;How the rule was written&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;CLI&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Search started at the denied folder&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Search started one folder up&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Search inside it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;code&gt;Read(~/**)&lt;/code&gt; (2 runs)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;2.1.197&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;listed both&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;listed both&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;code&gt;Read(//home/agent/b2probe/**)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;2.1.197&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refused&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;listed both&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;code&gt;Read(/home/agent/b2probe/**)&lt;/code&gt;, one slash&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;2.1.197&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;listed both&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;listed both&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;listed the sentinel in it&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;code&gt;Read(//home/agent/b2probe/**)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;2.1.280&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refused&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;“No files found”&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three things stand out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Above the folder, both real rule forms leaked on 2.1.197.&lt;/strong&gt; A search started one level up listed the hidden names,
however the rule was written.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;~/**&lt;/code&gt; and an absolute path aren’t the same rule.&lt;/strong&gt; With the home form, even a search started &lt;em&gt;at&lt;/em&gt; the home folder
leaked. With the absolute form, a search at the folder itself was refused.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One slash protects nothing.&lt;/strong&gt; In Claude Code’s permission rules, a path with a single leading &lt;code&gt;/&lt;/code&gt; is read relative
to the settings source, not the root of the disk. So &lt;code&gt;Read(/home/…)&lt;/code&gt; never matched the real folder, and every search
listed everything. An absolute path is written with two slashes: &lt;code&gt;//home/…&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;On 2.1.280, the search from above came back empty.&lt;/strong&gt; The newer CLI filters it. We only had turns to test the absolute
form there, so whether the &lt;code&gt;~&lt;/code&gt; form is filtered too is still open.&lt;/p&gt;
&lt;h2 id=&quot;documented-not-hidden&quot;&gt;Documented, not hidden&lt;/h2&gt;
&lt;p&gt;None of this is a secret flaw. Claude Code’s permissions page calls &lt;code&gt;Read&lt;/code&gt; rules on Grep and Glob a “best-effort”
attempt, applied to the folder a search starts from. For real enforcement, it points to running the agent in a
sandbox. Our measurements are a concrete picture of what “best-effort” means in practice.&lt;/p&gt;
&lt;h2 id=&quot;what-actually-kept-the-agent-out&quot;&gt;What actually kept the agent out&lt;/h2&gt;
&lt;p&gt;We ran the same probe a second way: the agent as its own unprivileged user, with every folder it shouldn’t see owned by
another user and closed to others. The operating system, not Claude Code, decided what it could open. &lt;strong&gt;Every route
came back empty: Read, Grep, Glob and Bash, 29 of 29 attempts, 0 leaks.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The practical rule for any tool that hands files to an agent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;keep secrets out with the &lt;strong&gt;operating system&lt;/strong&gt; (a separate user, or a sandbox);&lt;/li&gt;
&lt;li&gt;use deny rules only as a second layer;&lt;/li&gt;
&lt;li&gt;when you do write one, use &lt;code&gt;//&lt;/code&gt; for absolute paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-second-finding-the-first-turn-can-start-before-the-tools-arrive&quot;&gt;A second finding: the first turn can start before the tools arrive&lt;/h2&gt;
&lt;p&gt;Agents often get extra tools from &lt;strong&gt;MCP servers&lt;/strong&gt;, small programs the CLI starts beside the model. In headless mode on
2.1.197, Claude Code began the first turn the moment the message arrived. The MCP server was still starting
(&lt;code&gt;pending&lt;/code&gt;), so the model got none of its tools for that turn: in our runs, 0 of its 7 tools. Holding the first message
for a few seconds fixed it, and the studio’s harness now holds it for 10.&lt;/p&gt;
&lt;p&gt;On 2.1.280, the server was connected with its tools at the first turn in 3 of 3 starts, with no hold. An earlier note
that 2.1.280 also started early didn’t reproduce, so it’s marked disputed, not wrong. Either way, check the start event:
the server should say &lt;code&gt;connected&lt;/code&gt;, with its tools listed.&lt;/p&gt;
&lt;h2 id=&quot;how-we-got-it-wrong-twice&quot;&gt;How we got it wrong twice&lt;/h2&gt;
&lt;p&gt;This finding was corrected twice in one day, and the corrections are part of the record.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;First:&lt;/strong&gt; our original probe used the &lt;code&gt;~/**&lt;/code&gt; form and searched at the home folder. We read that as “only a search at
the denied folder itself leaks”, and narrowed the claim.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Second:&lt;/strong&gt; a re-probe with the absolute form showed the folder itself refused and the folder above leaking. We read
that as the opposite.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then&lt;/strong&gt; we ran both forms side by side, which gave the table above. Each earlier reading had been right for one
spelling of the rule, and wrong to generalise. Nothing in the record is marked wrong; every claim now names its rule
form and CLI version.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The lesson we took: don’t narrow or widen a measured claim until a probe separates every case it covers.&lt;/p&gt;
&lt;h2 id=&quot;where-the-figures-come-from&quot;&gt;Where the figures come from&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The probes:&lt;/strong&gt; Claude Code in the studio’s &lt;code&gt;atlas-pilot&lt;/code&gt; Docker sandbox, on the Claude plan
with claude-haiku-5-5. 13 short turns in all on 2026-10-20: 6 for the deny-rule searches (Grep the only tool), 6 for the MCP start and 1
for a tracing run. The original probe ran on 2026-10-13.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The entries:&lt;/strong&gt;
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-13-claude-code-read-deny-rules-do-not-stop-a-grep-walk.md&quot;&gt;Claude Code: a Read deny rule doesn’t stop a Grep walk over the denied tree&lt;/a&gt;
and
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-13-claude-code-starts-a-turn-before-mcp-servers-connect.md&quot;&gt;Claude Code starts a turn before its MCP servers connect&lt;/a&gt;.
Every claim there is marked verified or not, with what checked it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The knowledge base:&lt;/strong&gt; the same findings are wave 1 of the new &lt;code&gt;agent-tooling-knowledge&lt;/code&gt; base in
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/readouts&quot;&gt;readouts&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not published:&lt;/strong&gt; the raw run receipts. They carry session identifiers and, in one case, an account email. They’re
kept privately and named in the entries by their SHA-256.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code’s own account:&lt;/strong&gt; the &lt;a href=&quot;https://code.claude.com/docs/en/permissions&quot;&gt;permissions documentation&lt;/a&gt;, read
2026-10-20.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 16: What counts as a duplicate? Calibrating the dataset desk</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-16/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-16/</guid><description>The studio&apos;s dataset desk flags duplicate images and files before they reach training data. Its thresholds were guesses. We planted copies among known originals, measured what six models and a simple image hash catch, and found that a different view of the same ship looks like a duplicate to most of them. We also found that the desk&apos;s memory figures were short by up to seven times.</description><pubDate>Tue, 20 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;The studio builds training data from its own art and run records. Before anything is trained on, a tool called the
&lt;strong&gt;dataset desk&lt;/strong&gt; reads the set and flags problems, and duplicates are one of them. A duplicate does two kinds of damage.
It counts one example twice. And when a copy lands on both sides of a train/test split, it quietly inflates the test
score.&lt;/p&gt;
&lt;p&gt;The desk never deletes anything: it flags, and a person decides. So a flag has to be worth a person’s time. Until this
week its thresholds were borrowed from other people’s data, with a note saying they needed recalibrating. This issue is
that recalibration, in three rungs, on the RTX 5090.&lt;/p&gt;
&lt;h2 id=&quot;how-you-measure-a-duplicate-finder&quot;&gt;How you measure a duplicate finder&lt;/h2&gt;
&lt;p&gt;You can’t measure a duplicate finder on data where nobody knows the answer. So we made the answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Planted copies.&lt;/strong&gt; We took known originals and made copies of each:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;images:&lt;/strong&gt; flipped, cropped by 5%, resized to half, brightened by 10%, and saved as JPEG at quality 70;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;files:&lt;/strong&gt; small edits, for files from the studio’s public run records.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good finder pairs every copy with its original. The share it pairs is its &lt;strong&gt;recall&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clean negatives.&lt;/strong&gt; Things that can’t be duplicates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;for files, files of the same type from other runs, under other names;&lt;/li&gt;
&lt;li&gt;for images, art from four other projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anything flagged among these is a &lt;strong&gt;false positive&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Views.&lt;/strong&gt; The studio’s turntable project renders each object from many camera angles: 1,381 pairs of the same object
at different angles. The studio’s rule is that these are never duplicates, so any flag here is wrong too.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rung 1 found that its negatives weren’t clean: versions of one file across runs looked like copies. Rung 2 rebuilt them,
and rung 2b ran the text side again through the desk itself.&lt;/p&gt;
&lt;h2 id=&quot;images-the-angle-problem&quot;&gt;Images: the angle problem&lt;/h2&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Rule&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Copies caught&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Other-project images flagged&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Views flagged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;SigLIP2, cosine ≥ 0.95 (the desk’s old default)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;95%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;20%&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;77%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;mirror-aware image hash at the old threshold (≥ 246 bits agreeing)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;71%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;image hash only, mirror-aware, ≥ 230 of 256 bits agreeing&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;81%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;4.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;DINOv2, cosine ≥ 0.97&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;82%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;4.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;DINOv2 + mirror-aware hash, two signals&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;91%&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;4.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;SigLIP2&lt;/strong&gt; is the model the desk used for images. It’s built to match pictures to words, so it puts two angles of one
ship close together: they show the same thing. That’s right for search, but wrong for finding copies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DINOv2&lt;/strong&gt; is trained on images alone, and it’s more literal about what it sees.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The image hash&lt;/strong&gt; (dHash) compares 256 brightness steps between neighbouring pixels. It’s cheap, needs no model, and
survives resizing and JPEG. It breaks on a mirror image: in rung 1, a flipped copy agreed with its original on 155 of 256 bits on average. Comparing each image with its
mirror as well brings flipped copies up to about 249.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The two-signal rule&lt;/strong&gt; flags a pair when DINOv2 and the hash agree, or when DINOv2 alone is very sure:
&lt;code&gt;(cosine ≥ 0.94 and hash ≥ 212 bits) or cosine ≥ 0.985&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That still flags 4.3% of views. No threshold removes them without losing real copies, but the records can. Each
turntable image’s record names its object and its camera angle, so the desk now checks those first. Two angles of one
object are never paired, whatever the score. A second render of the &lt;em&gt;same&lt;/em&gt; angle is still checked.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A shortcut that didn’t work.&lt;/strong&gt; A reviewer suggested working out the mirror’s hash from the stored hash, with no image
to read: reverse each row of bits and invert it. That’s exact only when no two neighbouring pixels are equal. We tested
it against real flips on 300 images:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Project&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Images&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Median error (bits of 256)&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Off by more than 10 bits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;turntable renders&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;45&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;112&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;painted art&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;255&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;18%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The renders sit on flat backgrounds, where neighbours tie and the inversion turns every tie into a disagreement. The
desk will compute the mirror’s hash from the flipped image.&lt;/p&gt;
&lt;h2 id=&quot;text-read-the-model-card-first&quot;&gt;Text: read the model card first&lt;/h2&gt;
&lt;p&gt;For files, we compared four text embedding models. Rung 2 ran &lt;strong&gt;EmbeddingGemma 2&lt;/strong&gt; without the setup its model card
asks for. It got the wrong task prefix, it loaded its image and audio encoders for text it didn’t need, and it read
inputs in short pieces. So rung 2b ran every model through the desk itself, after the desk was rebuilt to follow each
model’s card: a task prefix where the model wants one, text-only loading, and inputs split by the model’s own tokenizer
into chunks up to the model’s own limit: 8,000 tokens for EmbeddingGemma 2, 8,192 for the others.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Pool&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Model&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Threshold that flags ≤ 1% of non-copies&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Copies caught there&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;all 458 files, 100 copies&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;bge-m3&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.98&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;94%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;EmbeddingGemma 2&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.995&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;gte-modernbert&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.99&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;52%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Qwen3-Embedding 0.6B&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;none below 1.0&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;scripts only, 17 copies&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;EmbeddingGemma 2&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.935&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On scripts, EmbeddingGemma 2 separates cleanly.&lt;/strong&gt; Every copy scores at least 0.976 against its original, and every
non-copy at most 0.931. The desk uses 0.95, inside that gap. It rests on only 17 copies, so it’s marked provisional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On logs and JSON, it can’t.&lt;/strong&gt; Run files from different runs share templates, field names and timestamps, and
EmbeddingGemma 2 scores look-alikes up to 0.992. &lt;strong&gt;bge-m3&lt;/strong&gt; keeps the best balance, and stays the default for text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Through the desk, bge-m3 got better:&lt;/strong&gt; 94% of copies at 0.98, up from 90% when it was run off-card.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-memory-surprise&quot;&gt;The memory surprise&lt;/h2&gt;
&lt;p&gt;The desk has a VRAM governor. Before loading a model, it checks the card has room for that model’s registered figure
plus a 1 GiB margin, so two jobs can’t push the shared card past its budget. Rung 2b measured each text model at its
longest input. Three of the four were registered far too low:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/desk-vram.png&quot; alt=&quot;dataset-desk text embedders: registered VRAM against the measured peak&quot; width=&quot;600&quot; height=&quot;240&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The registered figures came from short inputs. At 8,192-token chunks, a model needs memory for the attention over the
whole chunk, and that grows with length. Until the registry is fixed, long-text runs are planned at their measured
peaks, with the card held by one job alone. The fix, registering the long-input peaks, is in the next build.&lt;/p&gt;
&lt;h2 id=&quot;how-it-was-checked&quot;&gt;How it was checked&lt;/h2&gt;
&lt;p&gt;Every step went past a second reviewer before it ran. Argon, an outside model, read the code and the plan, and the
Publisher seat checked the settled choices. Three of Argon’s suggestions changed the plan:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one threshold for text instead of two;&lt;/li&gt;
&lt;li&gt;the view check from records;&lt;/li&gt;
&lt;li&gt;comparing the desk’s cached settings before reusing any vector.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two other points came out differently once they were tested:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The mirror shortcut&lt;/strong&gt; failed, as shown above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A batch-size claim was R&amp;amp;D’s own error.&lt;/strong&gt; We told the reviewer the bge-m3 run used batches of 4; it used 8. The fix
that rested on it was dropped, and the write-up was corrected.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-changes-and-whats-still-open&quot;&gt;What changes, and what’s still open&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Next build (D2):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DINOv2 with the mirror-aware hash for image duplicates;&lt;/li&gt;
&lt;li&gt;the view check from records;&lt;/li&gt;
&lt;li&gt;per-task text thresholds, at 0.95 for code and 0.98 for text;&lt;/li&gt;
&lt;li&gt;registry VRAM figures measured at long inputs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every threshold ships marked provisional, with the rung that measured it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not settled yet:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The two-signal rule misses 46 of the 500 planted image copies.&lt;/strong&gt; They probably have near-identical siblings in
the same set, but nobody has checked: a person will look at them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code thresholds rest on 17 copies.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copied sections inside long files&lt;/strong&gt; aren’t tested. The desk compares the average of a file’s chunks, which
dilutes a shared section.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dataset items&lt;/strong&gt; (questions and answers written for training) need their own pool and rule. Templates make
unrelated items look alike, and paraphrases make copies look different.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-the-figures-come-from&quot;&gt;Where the figures come from&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Images and the first text pass (rung 2):&lt;/strong&gt; &lt;code&gt;experiments/dataset-desk/calibration/data/rung2.json&lt;/code&gt;. The plain
hash on flipped copies (rung 1) is &lt;code&gt;data/rung1-dhash.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text through the desk (rung 2b):&lt;/strong&gt; &lt;code&gt;data/rung2b.json&lt;/code&gt;, the curves and picks, and &lt;code&gt;data/rung2b-runs.json&lt;/code&gt;, each run’s
model revision, settings, peak VRAM and time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The mirror test:&lt;/strong&gt; &lt;code&gt;data/mirror-dhash.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The registered figures:&lt;/strong&gt; &lt;code&gt;data/registry-25d9cdd5.json&lt;/code&gt;, read from
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/dataset-desk&quot;&gt;dataset-desk&lt;/a&gt; at commit &lt;code&gt;25d9cdd5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The chart&lt;/strong&gt; is built by &lt;code&gt;experiments/charts/make_charts.py&lt;/code&gt;, and &lt;code&gt;manifest.json&lt;/code&gt; pins each input’s commit and
sha256.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not published:&lt;/strong&gt; the art and the text of the run files. These data files hold scores, counts and settings only.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 14: Testing the tester: does our critic catch bad training rows?</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-14/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-14/</guid><description>Our training data is checked by a second model, a critic, and in the writer screen it accepted 35 of 36 rows from one writer. Was the data that good, or was the critic too easy? We broke good rows on purpose and counted what two critics caught. One rule turned out to decide almost everything, and the pre-registered test still said &quot;not shown&quot;. Here&apos;s why, and the confirmation test that comes next.</description><pubDate>Mon, 19 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;The studio builds training data with a pipeline of local models. One model, the &lt;strong&gt;writer&lt;/strong&gt;, drafts each row. A
second model from a different family, the &lt;strong&gt;critic&lt;/strong&gt;, checks it before it’s kept. In last week’s writer screen
(&lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-11/&quot;&gt;issue 11&lt;/a&gt;), the critic accepted almost everything: 35 of 36 rows from one writer. That’s either
very good data or a critic that rarely says no.&lt;/p&gt;
&lt;p&gt;This study tests the critic itself. Every figure here comes from files in the repo, and the last section says
which. The design was written and reviewed before any of it ran:
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/critic-signal/DESIGN.md&quot;&gt;&lt;code&gt;DESIGN.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-the-critic-checks&quot;&gt;What the critic checks&lt;/h2&gt;
&lt;p&gt;Many of the rows are reading exercises. A short document (the &lt;strong&gt;material&lt;/strong&gt;) and a &lt;strong&gt;statement&lt;/strong&gt; about it; the
answer is whether the material supports the statement, contradicts it, or can’t tell, plus the line or lines that
decide it. Some rows add a twist: a &lt;strong&gt;checker’s reasoning&lt;/strong&gt; that contains a planted mistake, which the learner has
to name. Each kind of mistake has a label, such as “guess” (reasoning from outside the material).&lt;/p&gt;
&lt;p&gt;The critic gets two checks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;the verdict check:&lt;/strong&gt; it solves the row blind, without seeing the answer, and its verdict and deciding lines are
compared with the row’s;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;the flaw check:&lt;/strong&gt; on rows with a checker’s reasoning, it names the mistake.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Today the flaw check is &lt;strong&gt;advisory&lt;/strong&gt;. A disagreement is logged, but the row is kept. That was decided earlier,
when an older setup agreed with the flaw labels only 9 times in 24.&lt;/p&gt;
&lt;h2 id=&quot;step-one-were-the-accepted-rows-actually-right&quot;&gt;Step one: were the accepted rows actually right?&lt;/h2&gt;
&lt;p&gt;Before testing the critic, R&amp;amp;D checked its accepted rows by hand, blind: 30 rows with a verdict, keyed without
seeing the writer, the critic’s answer or the row’s own answer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;29 agreed&lt;/strong&gt; with the row’s answer. &lt;strong&gt;1 was unclear&lt;/strong&gt;, and none were wrong.&lt;/li&gt;
&lt;li&gt;So the critic wasn’t letting wrong rows through. But the rows were easy, and none of them had “can’t tell” as
the true answer, so they don’t show what the critic would do with a hard one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New finding:&lt;/strong&gt; two rows had reasoning that named its own planted mistake (“that is a guess”), which gives the
answer away. That’s now a defect class the pipeline checks for.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;step-two-break-good-rows-on-purpose&quot;&gt;Step two: break good rows on purpose&lt;/h2&gt;
&lt;p&gt;From 27 rows that passed the hand check, a script made copies with one known change each. The critic never
sees which copy is which.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Copy&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;What changed&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;A good critic should&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;controls&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;nothing&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;accept it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;material flip&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;a number on the deciding line changed so the true verdict flips&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refuse it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;can’t-tell trap&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;the deciding line deleted, or its subject swapped for another one in the passage&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refuse it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;clean trace&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;the checker’s flawed reasoning replaced with sound reasoning, but the flaw label kept&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refuse it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;decoy deciding line&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;the answer’s deciding line swapped for a line that doesn’t decide anything (scored offline)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;refuse it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;R&amp;amp;D read every material-flip, can’t-tell and decoy copy before any critic saw them. One flip turned out not to
flip, because the script missed a number with “px” stuck to it. That copy was set aside before the run, and the
decision is on record.&lt;/p&gt;
&lt;p&gt;Two critics took the test: &lt;strong&gt;Muse-Glimmer-30B&lt;/strong&gt; (two runs, at seeds 0 and 1) and &lt;strong&gt;Nemotron-3.5-Lightning&lt;/strong&gt; (one
run). A critic never checks rows written by its own model family, so Muse skipped 14 copies and Lightning 11.&lt;/p&gt;
&lt;h2 id=&quot;what-they-caught&quot;&gt;What they caught&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/critic-catch.png&quot; alt=&quot;What a critic catches, P0 against P2&quot; width=&quot;600&quot; height=&quot;216&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Each row is scored under two rule sets, from the same critic replies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P0&lt;/strong&gt; is today’s rules (the flaw check is advisory).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P2&lt;/strong&gt; makes the flaw check count, and adds two rules about quoting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Most broken copies are caught already.&lt;/strong&gt; Across the material flips, can’t-tell traps and decoy lines, both
critics refused nearly everything under either rule set. The one miss was Muse reading a time changed from
5:00 PM to 7:00 PM as still matching. Lightning caught it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The clean traces are the gap.&lt;/strong&gt; On every clean-trace copy, both critics answered “no flaw”: 13 of 13. That’s the
right answer, because the reasoning really is sound. But the label still says “guess”, so the row is wrong, and
under today’s rules the critic’s correct answer is ignored. P2 counts it, and catches all 13.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; P2 refused 1 of the 44 unchanged good rows, where Muse named a flaw the row didn’t have. The two
quoting rules never decided a single item. Everything P2 changes comes from making the flaw check count.&lt;/p&gt;
&lt;p&gt;Muse’s second run (seed 1) refused nothing that its first run didn’t.&lt;/p&gt;
&lt;h2 id=&quot;then-why-didnt-we-adopt-it&quot;&gt;Then why didn’t we adopt it?&lt;/h2&gt;
&lt;p&gt;Because the design said in advance how strong the evidence had to be, and it wasn’t.&lt;/p&gt;
&lt;p&gt;The test compares the two rule sets item by item and counts only the items they disagree on. All 13 disagreements
favoured P2: 6 for Muse and 7 for Lightning. Each comparison on its own gives p = 0.031 or 0.016.&lt;/p&gt;
&lt;p&gt;But the design ran several comparisons: P2 and a two-run variant, for each critic. Testing more things gives
chance more opportunities, so the design applies a correction (Holm’s) across all of them. With four comparisons,
the smallest result has to be under 0.0125, and 0.016 isn’t. Corrected, the result is &lt;strong&gt;0.0625, above the 0.05
bar.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The reason is plain from the data: there were only 6 or 7 clean-trace copies per critic. Even a perfect 7-to-0
can’t clear a four-way correction. We didn’t run Lightning’s second seed, because its outcome was fixed before it
started: P2 already catches everything P0 misses on Lightning’s items, so a second run couldn’t add a
disagreement. Skipping it saved about 4.5 units.&lt;/p&gt;
&lt;p&gt;So under the rule we committed to, nothing changes yet. A “not shown” isn’t a “no”, though.&lt;/p&gt;
&lt;h2 id=&quot;next-one-clean-confirmation-test&quot;&gt;Next: one clean confirmation test&lt;/h2&gt;
&lt;p&gt;The fix is more clean-trace items, judged by one comparison that was set in advance. The design is
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/critic-signal/DESIGN.md&quot;&gt;§9 of &lt;code&gt;DESIGN.md&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New rows.&lt;/strong&gt; Gemma 4 31B writes 38 fresh reasoning rows, and R&amp;amp;D hand-checks each one blind.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No critic decides which rows are kept.&lt;/strong&gt; In the earlier rows, Muse helped pick what was kept, so it was partly
being tested on its own choices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One test.&lt;/strong&gt; P2 against P0 on Muse, at 0.05 with no correction, written down before any of the rows exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;How the outside review changed it.&lt;/strong&gt; An outside model, Argon, reviews every design before it runs. The first draft
picked 38 slots by lesson. Argon pointed out that the pipeline plants “no flaw” on every even-numbered slot, so
only 12 of the 38 could ever make a clean-trace copy. Too few. The fixed design takes all 27 eligible slots there
are, plus 11 even-numbered slots as natural controls. Argon passed the fix the same day.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/critic-confirmation-slots.png&quot; alt=&quot;The confirmation test’s 38 slots&quot; width=&quot;600&quot; height=&quot;284&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-it-cost&quot;&gt;What it cost&lt;/h2&gt;
&lt;p&gt;The study runs on Colab A100s, paid in compute units: about 5.3 units an hour.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Window&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;What&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Units&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Projected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;13v (a, b, c)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;the low-cost test ladder on a CPU runtime: a forced cut, two resumes&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.05&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;13w&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Muse, seed 0&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;3.10, plus 0.24 for a launch mistake&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;2.83–10.43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;13x&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Muse, seed 1&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;3.17&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;about 2.69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;13y&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Lightning, seed 0, converted on the GPU&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;4.52&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;3.82–8.83&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The design had projected about 23 units for each critic run, at about 120 seconds an item. That projection used Lightning’s earlier timings,
which included three extra calls per item. Measured, a critic call is short:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/critic-calls.png&quot; alt=&quot;How long one critic call takes&quot; width=&quot;600&quot; height=&quot;239&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The flaw check takes longer than the verdict check: its prompt is about twice as long, and the critic thinks more
before answering. Lightning thinks two to three times as much as Muse per call and still answers faster.
Lightning’s one empty reply came after it spent its whole 16,384-token reply budget thinking. The retry was
counted, and the copy it was answering was scored as refused.&lt;/p&gt;
&lt;h2 id=&quot;where-the-figures-come-from&quot;&gt;Where the figures come from&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The slot diagram&lt;/strong&gt; is &lt;code&gt;data/confirmation-slots.json&lt;/code&gt;, the frozen list in §9.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The broken-copy results&lt;/strong&gt; are &lt;code&gt;experiments/stage1-regeneration/critic-signal/data/outcomes.csv&lt;/code&gt;: one row per
copy, critic and rule set, written by role-set’s &lt;code&gt;critic-rescore-offline&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The per-call timings and thinking counts&lt;/strong&gt; are &lt;code&gt;data/calls.csv&lt;/code&gt;, from the logging proxy that sat between
role-set and the model in every window.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/extract_data.py&lt;/code&gt; writes both files from the window receipts. The text of the rows stays off the public
repo: these files hold ids, codes and counts only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hand checks, windows and decisions:&lt;/strong&gt; &lt;code&gt;PHASE-0-1.md&lt;/code&gt; and &lt;code&gt;PHASE-2-3.md&lt;/code&gt; in the same folder. Units come from
the study’s ledger, one row per window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The tools:&lt;/strong&gt; &lt;a href=&quot;https://github.com/mcp-tool-shop-org/role-set&quot;&gt;role-set&lt;/a&gt; (&lt;code&gt;critic-eval&lt;/code&gt;, &lt;code&gt;critic-rescore-offline&lt;/code&gt;,
&lt;code&gt;scripts/critic_signal/perturb.py&lt;/code&gt;) at commit &lt;code&gt;47a5882&lt;/code&gt;. The three pictures are built by
&lt;code&gt;experiments/charts/make_charts.py&lt;/code&gt;, and &lt;code&gt;manifest.json&lt;/code&gt; pins each input’s commit and sha256.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 13: Claude + Colab: the path we&apos;d use again</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-13/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-13/</guid><description>Three days of running model work on rented Colab GPUs from Claude Code, 49 windows, every unit accounted for. On the first night most of the spend bought nothing it was opened for. By the second day a 4-writer window finished under its projection and its cap. Here is what changed, the data behind it, and templates you can take.</description><pubDate>Sun, 18 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;The studio runs its model work on rented GPUs. Claude Code drives Google’s Colab from the command line: it asks for a
machine, sends the job, follows it and stops it. Nobody watches a notebook. Each of these sessions is a &lt;strong&gt;window&lt;/strong&gt;,
paid in Colab compute units: about 5.3 an hour for an A100, 0.08 for a basic CPU runtime.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-11/&quot;&gt;Issue 11&lt;/a&gt; told how night one went: the writer screen got its answers, but about half of the first 71
units bought nothing their windows were opened for. This issue follows the same work through day two and sets out
the way of working that came out of it. The path itself, with templates, is a handbook page:
&lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/handbook/rented-gpu-windows/&quot;&gt;Rented GPU windows (Claude + Colab)&lt;/a&gt;. Every window’s numbers are in the repo, one
row each, and the last section says where each figure comes from.&lt;/p&gt;
&lt;h2 id=&quot;the-two-days-in-one-table&quot;&gt;The two days in one table&lt;/h2&gt;
&lt;p&gt;Each window is classed by what it gave back: &lt;strong&gt;results&lt;/strong&gt; (what it was opened for), &lt;strong&gt;partial&lt;/strong&gt; (some of it),
&lt;strong&gt;lost&lt;/strong&gt; (a dropped VM, a cap stop, or a fault in our own tooling), or &lt;strong&gt;test&lt;/strong&gt; (a free run on a CPU runtime).&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Night one (Oct 17)&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Day two (Oct 18)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Windows&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;33&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Units spent&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;141.1&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;31.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Bought results&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;43.3 (31%)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;21.9 (69%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Partial&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;41.1 (29%)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;8.0 (25%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Lost&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;56.6 (40%)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;1.3 (4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Test runs&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.03&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;0.43&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two caveats. Day two’s paid work was smaller and better rehearsed, so the comparison flatters it somewhat. And some
spend figures are estimates where two windows shared a cap: summed window by window, the three days come to 172.7
units, while the account’s balance fell by 166.5 over the same span. The balance is the measure; the per-window rows
were booked on the high side.&lt;/p&gt;
&lt;h2 id=&quot;what-changed-step-by-step&quot;&gt;What changed, step by step&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Two lanes.&lt;/strong&gt; CPU runtimes do the preparation: converting models, testing, measuring. The A100 only serves models.
On night one, every A100 window downloaded and converted its own models, 15 to 21 minutes each, before any real work.
Now a CPU runtime converts each model once, at 0.26 units an hour, and the A100 downloads the finished file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Measure first, then budget.&lt;/strong&gt; Every projection uses a measured rate with its receipt. Every window has a cap at
the projection plus 15%, and opens only if the cap plus 2 units is left. When one cost was still a guess (the
“twin” waves had never run on a GPU), the smaller window ran first and measured it: twin slots cost about twice a
normal slot, not the same.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. One script runs a window,&lt;/strong&gt; and it trusts nothing it hasn’t checked. It refuses bad inputs before asking for a
machine, checks the machine before uploading anything, follows the job’s own log file rather than the command’s
output stream (which went quiet on us mid-run), counts only its own spend against the cap, and fetches the
job’s log, and its results when the job got that far, on every way out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. A health check on every new machine.&lt;/strong&gt; Before anything is uploaded, one short command reads the card, the disk,
the memory and the download speed. A bad machine is stopped and a fresh one requested. On day two it ran 12 times and
rejected 2: one machine whose first command died (two night-one windows had lost about 4.5 units that way, after
their uploads), and one downloading at 45 MB/s. Each rejection cost a few minutes of machine time, not a run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. The job saves itself.&lt;/strong&gt; It writes every finished model call and every finished wave into its log, so a killed
window’s work can be pulled out of the log, and the next window can resume from it. Each saved file carries a
checksum, and a file that doesn’t match is refused.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. A test ladder before any paid run:&lt;/strong&gt; unit tests, then the real script against a fake command-line tool, then the
real job on a real CPU runtime with stand-in models, then the paid window. The 13 test windows cost 0.46 units in
all. One of them caught a step that would have been refused partway through a paid window: an older file format lost
a record of the model’s settings on its way through the pipeline. Two review rounds had read that code and passed it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. Three seats review everything.&lt;/strong&gt; One Claude session builds, a second reviews the actual diff (not the summary of
it), and an outside model from another company reviews from a written brief. The loop repeats until it passes, and a
reviewer who asked for fixes sees the fixed version before anything paid uses it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. Pre-built models, checked byte for byte.&lt;/strong&gt; A converted model file goes into the studio’s private repository only
if its checksum equals the one recorded when the arms first ran on it. A file that doesn’t match is never used, and we
never change the recorded checksum to make a file fit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9. Tokens that can’t leak far.&lt;/strong&gt; One access token per job type, each limited to the one private repository, sent to
the machine as a file in memory, read once at the start, and kept out of every log. A test pushes a fake token through
the real script and checks it appears nowhere.&lt;/p&gt;
&lt;h2 id=&quot;what-the-path-caught&quot;&gt;What the path caught&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The stand-in run caught the lost model settings&lt;/strong&gt; described in step 6. Fixing it took a small rule for the older
files, checked by checksum against the file they came from.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The first paid window after the ladder found what the ladder couldn’t:&lt;/strong&gt; a writer whose two twin attempts both
failed was still sent to a critique, which then refused. No stand-in model had ever failed every attempt. The fix is
one line, and the lesson is in the handbook: give your stand-ins the failures you expect from real models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The byte-match rule caught a surprise.&lt;/strong&gt; Qwen3.8, converted again on a CPU runtime, came out the same size as
before with different bytes. The recorded package lists differed in one material place: the A100 image’s Python
carried the GPU build of PyTorch 2.11.0, the CPU image the CPU build. Models converted in their own environment (which
always gets the CPU build) matched on both machines, and Qwen3.8 matched again when rebuilt on an A100. We think the
PyTorch build is the cause; the host’s CPU instruction set hasn’t been ruled out yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An upload failed after a conversion had already matched.&lt;/strong&gt; The job had installed packages into the very Python it
was running, partway through, and its final step tripped over the mix of old and new library code. The upload now runs
in a fresh process.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The health check’s speed test reads low.&lt;/strong&gt; Its short 200 MB read gave 88–115 MB/s on A100s that then downloaded at
179–310 MB/s. Our first speed floor, set from full downloads, would have turned good machines away. The floor now only
rejects a broken connection.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-payoff-one-window-under-its-projection&quot;&gt;The payoff: one window, under its projection&lt;/h2&gt;
&lt;p&gt;Window W-A resumed four writers (Qwen3.8, Gemma 4, Granite 4.2 and Bolt) from their saved drafts, with Muse as the
critic. It rewrote the one draft that had been sent back, then ran two “twin” practice rows per writer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It downloaded the four pre-built model files, 122.4 GB, in 684 seconds. Each matched its checksum.&lt;/li&gt;
&lt;li&gt;It finished on its own after 11,749 seconds: 17.55 units against a projection of 21.8 and a cap of 25.1.&lt;/li&gt;
&lt;li&gt;Every writer’s saved results came back: 30 rows from the pilot and 6 accepted twins, with no model errors.&lt;/li&gt;
&lt;li&gt;The projection was 19% high: setup came in far under, and the twins ran over.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These rows come from a screen with a fixed set of eight slots, so they’re not a releasable batch: they fail the
batch-mix rule a real batch must pass.&lt;/p&gt;
&lt;h2 id=&quot;still-open&quot;&gt;Still open&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prove the PyTorch explanation&lt;/strong&gt; with a one-layer conversion before any full one, recording the machine’s CPU.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recalibrate the speed floor&lt;/strong&gt; from more paired readings, and try downloading files in parallel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Record the CPU model on every health check,&lt;/strong&gt; so a machine difference can be seen, not inferred.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pin new conversions in their own environment&lt;/strong&gt; with the exact PyTorch build, in the next study.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;take-it-with-you&quot;&gt;Take it with you&lt;/h2&gt;
&lt;p&gt;The handbook page &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/handbook/rented-gpu-windows/&quot;&gt;Rented GPU windows&lt;/a&gt; has the full path and templates: a launch
file, a projection worksheet, a ledger row, a per-window results file, and a brief for an outside reviewer. The window
code is in role-set (&lt;code&gt;scripts/colab/&lt;/code&gt;), and the data behind this issue is in rnd’s
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/tree/main/experiments/colab-golden-path&quot;&gt;&lt;code&gt;experiments/colab-golden-path/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;where-each-figure-comes-from&quot;&gt;Where each figure comes from&lt;/h2&gt;
&lt;p&gt;All paths are in &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd&quot;&gt;mcp-tool-shop-org/rnd&lt;/a&gt; unless noted.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Units, windows and classes by day&lt;/strong&gt; (the table, 31% → 69%, 40% → 4%, 13 tests at 0.46 units, 172.7 summed):
&lt;code&gt;experiments/colab-golden-path/figures.json&lt;/code&gt;, computed by &lt;code&gt;figures.py&lt;/code&gt; from &lt;code&gt;data/windows.csv&lt;/code&gt;. Each window’s class
comes from its own &lt;code&gt;outcome.json&lt;/code&gt;, written by the Publisher when the window closed (&lt;code&gt;outcome_source&lt;/code&gt; says which).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;166.5 units by the balance:&lt;/strong&gt; the first window’s start balance, 1207.67 (&lt;code&gt;data/windows/window1-20261016/&lt;/code&gt; and its
ledger note), the last window’s end balance, 1141.14 (window 13l), and the 100-unit top-up recorded in
&lt;code&gt;data/units-ledger.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rates&lt;/strong&gt; (5.3, 0.08, 0.26 units an hour): &lt;code&gt;instruments/colab.md&lt;/code&gt; and &lt;code&gt;data/windows/_top/highmem-rate-probe.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;About half of the first 71 units:&lt;/strong&gt; &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-11/&quot;&gt;issue 11&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;15 to 21 minutes of setup per model:&lt;/strong&gt; night one’s job logs, as measured in the W-A projection
(window 13l’s README in the Publisher’s receipts, summarised in &lt;code&gt;data/windows/window13l-20261018/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Health check, 12 tries and 2 rejections, 45 MB/s:&lt;/strong&gt; &lt;code&gt;figures.json&lt;/code&gt;’s &lt;code&gt;gate&lt;/code&gt; block, from each window’s
&lt;code&gt;gate.jsonl&lt;/code&gt;. &lt;strong&gt;About 4.5 units lost to first-exec deaths:&lt;/strong&gt; windows 11c (3.2) and 12e (1.29) in &lt;code&gt;data/windows.csv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Twin slots about twice a normal slot:&lt;/strong&gt; window 13b (W-B) and 13l (W-A) results, per writer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0.07 units and 15 minutes per CPU conversion:&lt;/strong&gt; window 13e (Muse) and 13g (Gemma) rows in &lt;code&gt;data/windows.csv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The PyTorch difference:&lt;/strong&gt; the pip freezes in windows 12f, 13f, 12h and 13e, and the outside review’s reply on it;
the A100 rebuild that matched is window 13j.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The failed upload:&lt;/strong&gt; window 13i. &lt;strong&gt;The speed-test pairs&lt;/strong&gt; (88–115 against 179–310 MB/s): windows 13i, 13j, 13k and
13l, short reads from &lt;code&gt;gate.jsonl&lt;/code&gt; and download rates from their job logs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;W-A&lt;/strong&gt; (684 s, 122.4 GB, 11,749 s, 17.55 units, 21.8 and 25.1, 30 rows and 6 twins, 19%): window 13l’s
&lt;code&gt;pin-result.json&lt;/code&gt; and README, summarised in &lt;code&gt;data/windows.csv&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 11: Picking a writer model on rented GPUs</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-11/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-11/</guid><description>We&apos;re choosing which open-weight model should write training data for a small local verifier. Night one tested the judge before trusting it, found that thinking is most of the bill, and spent about half of its first 71 compute units on windows that failed, through faults in our tooling and in the machines it ran on, before giving the results they were opened for. Here&apos;s the data, and the method that came out of it.</description><pubDate>Sat, 17 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;The studio is building a small local model whose job is to check other models’ claims: is this statement supported by this
evidence, or not? To train it, we need thousands of practice rows, each a short piece of material with a statement and the
right answer. We write those rows with larger open-weight models, and a second model, the critic, checks every draft
before it’s kept.&lt;/p&gt;
&lt;p&gt;Which model should do the writing? There were eight candidates. This issue covers night one of the pilot that picks
between them: what we measured, what went wrong, and the method we’d use again. The screen’s results will be added here
as updates when they land. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/PLAN.md&quot;&gt;the plan&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-setup-in-brief&quot;&gt;The setup, in brief&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Eight writers, all open-weight:&lt;/strong&gt; Qwen3.8-27B, Qwen3.6-35B-A3B, Granite-4.2-30B, Gemma-4-31B-it,
DeepSeek-R1-Distill-Qwen-32B, Muse-Glimmer-30B, and NVIDIA’s Nemotron-3.5 Lightning-30B and Bolt-74B. Every one was
checked against its licence before it went in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One critic per writer, never from the writer’s own family.&lt;/strong&gt; Muse critiques every writer except itself; Lightning
critiques Muse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rented GPUs.&lt;/strong&gt; Each “window” is a Colab session on one NVIDIA A100 with 40 GB of memory, paid in Colab compute units.
An A100 costs about 5.3 units an hour. A CPU-only session costs about 0.08.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pinned models.&lt;/strong&gt; Every model file is rebuilt from a fixed source revision and has to reproduce a recorded
fingerprint (its sha256 hash) before it serves. When something went wrong, we always knew it wasn’t the model files.
Lightning’s file was rebuilt four times and matched every time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Same settings for everyone:&lt;/strong&gt; each model runs at its own model card’s recommended sampling and thinking settings,
with a 32,768-token context and a 16,384-token limit on each reply.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not every model fits a 40 GB card at those settings. Qwen3.6 fit at 8-bit precision, peaking at 36,022 MiB of
40,960, and wrote at about 90 tokens a second. Gemma fit with 4,943 MiB to spare, at about 28 tokens a second.
DeepSeek-R1-Distill didn’t fit at 8 bits, so it will run a 6-bit build and be reported apart from the
8-bit writers.&lt;/p&gt;
&lt;h2 id=&quot;step-1-test-the-judge-before-it-judges-anything&quot;&gt;Step 1: test the judge before it judges anything&lt;/h2&gt;
&lt;p&gt;If the critic is wrong, every number downstream is wrong too, so the critic was tested first. The test is a
&lt;strong&gt;known-answer set&lt;/strong&gt;: 40 practice items whose right answers we wrote ourselves, plus 10 copies with a deliberate error
planted in them. The bar was written down before any result came in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;agree with the key&lt;/strong&gt; on at least 36 of the 40 clean items;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;catch at least 80%&lt;/strong&gt; of the planted errors;&lt;/li&gt;
&lt;li&gt;for a critic that samples randomly, like Muse, &lt;strong&gt;two runs with different random seeds&lt;/strong&gt; may disagree on at most 4 of the 40.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both passed. Muse agreed with the key on 38 and 37 of 40 across its two runs, caught all 10 planted errors, and its two
runs disagreed on just 1 item. Lightning agreed on 38 of 40 and caught all 10.&lt;/p&gt;
&lt;p&gt;The more useful finding was in the misses. On two items, a critic disagreed with &lt;em&gt;our&lt;/em&gt; key and was right: both critics on one, Muse alone on the other.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Retries the upload up to 3 times” means up to 4 attempts in all, so “tried at most 3 times” isn’t supported. Our key
said it was. Muse flagged it in both runs.&lt;/li&gt;
&lt;li&gt;“Apply every four weeks from March to September” fairly rules out January. Our key said you couldn’t tell. Both
critics disagreed with it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We replaced both items in version 2 of the set and changed nothing else. &lt;strong&gt;A calibration set can check the person who
wrote it&lt;/strong&gt;, so read the misses before blaming the model. Next time, the key will be written by two writers independently,
or checked blind by a model from a third family, before it’s used. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/critic-control/README.md&quot;&gt;the set and its errata&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;step-2-thinking-is-most-of-the-bill&quot;&gt;Step 2: thinking is most of the bill&lt;/h2&gt;
&lt;p&gt;Modern open models “think” before they answer: they write out reasoning that’s never shown to the reader, then the
answer. That reasoning counts toward the reply limit and takes GPU time like any other output.&lt;/p&gt;
&lt;p&gt;We logged every call through a small proxy that records token counts and timings. Qwen3.8’s first 13 calls show where
the time went:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/pilot-thinking-budget.png&quot; alt=&quot;Thinking vs the draft, per call&quot; width=&quot;600&quot; height=&quot;313&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;95% of the output was thinking:&lt;/strong&gt; 81,372 of 85,752 tokens.&lt;/li&gt;
&lt;li&gt;The calls came in two shapes. Short prompts (144–573 tokens) got 405–2,030 tokens back, in under 70 seconds. Long
prompts (6,370–7,643 tokens) got 4,364–15,264 back, at 2½–8 minutes each.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nothing was cut off,&lt;/strong&gt; but the longest call used 15,264 of its 16,384 tokens. A model that thinks a little longer
will start hitting the limit. A cut-off reply is reported as its own outcome, separately from a refusal, so running
out of room isn’t confused with being wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That changed the plan. Each practice row took about two calls: about 12,100 output tokens and about 6½ minutes per row
(roughly 7 rows finished in those 46 minutes). Projected across the pilot as designed (8 writers, 20 practice rows
each, with rewrites), and assuming every writer thinks about as long as Qwen3.8, it came to &lt;strong&gt;147–175 units&lt;/strong&gt;, against about 35 left
that night.&lt;/p&gt;
&lt;p&gt;So the pilot became a &lt;strong&gt;screen&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every writer drafts the same 8 practice rows, plus 2 “twin” rows of a second kind, from one fixed list;&lt;/li&gt;
&lt;li&gt;every draft is critiqued once, and drafts the critic sends back get one rewrite;&lt;/li&gt;
&lt;li&gt;writers that drop 3 or more of their 8 are cut;&lt;/li&gt;
&lt;li&gt;writers that pass go on;&lt;/li&gt;
&lt;li&gt;with 8 rows per writer, the screen can tell “doesn’t work” from “works”. It can’t rank close writers finely, and the
readout will say so.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And one number joined the readout: &lt;strong&gt;cost per accepted row.&lt;/strong&gt; A writer that thinks for 12,000 tokens per row pays that
on every row of the full dataset later, so cost is part of the choice, not just a budget line.&lt;/p&gt;
&lt;h2 id=&quot;step-3-long-paid-jobs-fail-so-design-for-it&quot;&gt;Step 3: long paid jobs fail, so design for it&lt;/h2&gt;
&lt;p&gt;This is where most of the money went. By the time the budget was raised at 19:44 UTC, 70.66 units had been spent, and
&lt;strong&gt;about 38 of the first 71 didn’t buy the results their windows were opened for&lt;/strong&gt;:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;What happened&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Units&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A long pilot window: the rig stopped receiving the job’s output, the job ran on unseen, and the window hit its cap without downloading anything&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;14.03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Colab reclaimed a machine 77 minutes into the job, with nothing saved off it&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;12.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A connection hung at the first command with nothing running, and the session sat open for 35 minutes&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;about 3.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;A check looked for a log line the server doesn’t print at its default log level, and stopped the window&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;3.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Three failed starts: a missing setting, a machine dropped mid-download, and a module missing from a package&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;5.29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;None of these were the writers’ fault. They were our tooling, and how it dealt with machines and connections that
failed. The method that came out of it, in the order we’d build it again:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The job writes its own log, and the rig reads that file, never the live output stream.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;The stream between the rig and a Colab session can stop while the job keeps running. That’s how the long pilot
window ran on unseen. It was the full 20-row design; by our projection it couldn’t have finished its first wave inside its cap, and it stopped
without downloading, so its work was lost.&lt;/li&gt;
&lt;li&gt;So the job appends everything to a file on the machine, and the rig downloads that file every 2 minutes.&lt;/li&gt;
&lt;li&gt;Progress, stalls and “finished” are all judged from it.&lt;/li&gt;
&lt;li&gt;A download that dies partway never overwrites the good copy.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every finished model call is saved the moment it returns.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Each call goes into a journal, keyed by a fingerprint of exactly what was asked.&lt;/li&gt;
&lt;li&gt;A resumed run replays the saved calls instead of asking the model again, and the job’s own logic rebuilds
everything else from them.&lt;/li&gt;
&lt;li&gt;We tested it with a mock model in role-set’s tests: a run killed halfway and resumed gave every draft and every
decision identical to an uninterrupted run, and no call ran twice. A separate check on the rig showed the saved
calls survive the trip through the log byte for byte. It hasn’t yet had to rescue a real run.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every way a window can end downloads the results first:&lt;/strong&gt; finished, stalled, out of time or at its spending cap.
&lt;ul&gt;
&lt;li&gt;The cap starts the exit early enough that the download fits inside it.&lt;/li&gt;
&lt;li&gt;The window that lost 14.03 units had stopped at its cap without downloading.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caps count each window’s own spend.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Two separate bugs. An early cap read the account balance, so one window was cut off by its neighbour’s spend.
Later, two windows that opened at the same moment each measured both sessions’ combined rate as their own,
and stopped at about half their real budget.&lt;/li&gt;
&lt;li&gt;Each cap now counts its own rate times its own runtime, which fixed the first. The rate is read under a lock, so
two windows can’t open at the same moment, which fixed the second.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up each model just before it’s needed,&lt;/strong&gt; not all at the start.
&lt;ul&gt;
&lt;li&gt;The reclaimed window’s log showed 49 minutes setting up three of its four models; its last 28 minutes were never
seen.&lt;/li&gt;
&lt;li&gt;Now a writer is set up right before its own drafts and the critic right before the first critique. A loss costs
at most one setup.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test each new kind of window on the cheapest machine first.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Before trusting the new file-based watcher, we ran a fake job on a CPU-only session that went silent on the
stream and then hung.&lt;/li&gt;
&lt;li&gt;The watcher saw it finish from the file, downloaded the results in 2 seconds, and closed the session.&lt;/li&gt;
&lt;li&gt;The two tests together cost 0.03 units.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The rule underneath all six: &lt;strong&gt;assume the machine can vanish at any minute, and make sure that minute costs only the
work in flight.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;step-4-a-blind-second-opinion-on-the-budget&quot;&gt;Step 4: a blind second opinion on the budget&lt;/h2&gt;
&lt;p&gt;When the budget was raised to 150 units, we wanted an outside view on how to spend the remaining 79. The catch with
asking for advice is that you tend to anchor on it, or the adviser anchors on you. So:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We wrote and committed our own allocation first, then sealed it.&lt;/li&gt;
&lt;li&gt;Argon, the studio’s outside reviewer, got only the measured facts: setup times,
speeds, tokens per row and the failures so far. It didn’t see our plan.&lt;/li&gt;
&lt;li&gt;We compared the two answers against the sealed version.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Argon’s plan was better in three places, and we took them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pair Lightning and Muse in one window, each critiquing the other, which saves two model setups;&lt;/li&gt;
&lt;li&gt;set up models lazily (point 5 above);&lt;/li&gt;
&lt;li&gt;fold one rewrite into the screen itself, so writers that pass don’t need a second window.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We kept one of ours, and corrected one of Argon’s assumptions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The second-critic check.&lt;/strong&gt; A second critic re-scores a blind sample of each writer’s drafts, to see whether Muse
favours writers that sound like it. It stays as a gate before any writer goes further.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The correction:&lt;/strong&gt; Argon assumed Qwen3.8’s lost window could be resumed. It couldn’t: that window ran before the call journal
existed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;/h2&gt;
&lt;p&gt;The screens are running tonight in this order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Granite;&lt;/li&gt;
&lt;li&gt;Qwen3.6 and Bolt;&lt;/li&gt;
&lt;li&gt;Lightning and Muse;&lt;/li&gt;
&lt;li&gt;Qwen3.8 and Gemma.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DeepSeek-R1-Distill waits for another day. Its 6-bit build hasn’t been made yet (the one setup step we haven’t timed),
and it sits in its own 6-bit group.&lt;/p&gt;
&lt;p&gt;For each writer, the readout will give:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how many of its 8 rows were accepted, dropped or cut off;&lt;/li&gt;
&lt;li&gt;its twin results;&lt;/li&gt;
&lt;li&gt;its tokens and units per accepted row;&lt;/li&gt;
&lt;li&gt;its share of thinking;&lt;/li&gt;
&lt;li&gt;“cut” or “goes on”.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those numbers will be added below as they come in.&lt;/p&gt;
&lt;h2 id=&quot;where-each-figure-comes-from&quot;&gt;Where each figure comes from&lt;/h2&gt;
&lt;p&gt;Every number above was checked against its record before publishing. Records in this repo are linked. The window logs
and the units ledger live in the studio’s private run records (the Publisher seat’s &lt;code&gt;receipts/colab/&lt;/code&gt;), and the figures
from them are listed here so they can be traced.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The critic test&lt;/strong&gt; (38, 37 and 38 of 40; 10 of 10 caught; 1 item differing between Muse’s runs; the two faulty keys):
window 7b’s log and result, and the &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/critic-control/README.md&quot;&gt;critic-control README&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qwen3.8’s calls&lt;/strong&gt; (13 calls, 81,372 of 85,752 tokens thinking, 15,264 longest, 45.7 minutes): the logging proxy’s
records from window 10d, committed as counts only in &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/stage1-regeneration/pilot/qwen3.8-w0-calls.json&quot;&gt;&lt;code&gt;qwen3.8-w0-calls.json&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;About 12,100 tokens and 6½ minutes per row, about 7 rows, and the 147–175 unit projection:&lt;/strong&gt; the Publisher’s
projection from the same window, made at its halfway point.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fits&lt;/strong&gt; (Qwen3.6 peak 36,022 MiB and 89.7 tokens a second; Gemma 4,943 MiB free and 27.8 tokens a second; R1
doesn’t fit at 8 bits): windows 11b, 11d and 5.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lightning’s four matching rebuilds:&lt;/strong&gt; windows 2, 3, 7 and 7b, against window 1’s original.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Units&lt;/strong&gt; (70.66 spent at 19:44 UTC; 14.03, 12.81, about 3.2, 3.00 and 5.29 lost): the units ledger, windows 10d, 12a,
11c, 7, and 10, 10b and 10c. The balance is the measure. Per-window figures for windows that overlapped are estimates
from their own runtimes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The 77 and 49 minutes, and the long window running on unseen&lt;/strong&gt; (stream lost at 15:46, last read at 16:38, cap at
17:57): windows 12a’s and 10d’s logs and Colab’s session history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The CPU tests&lt;/strong&gt; (0.03 units, a 2-second fetch): windows 12p and 12q.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The resume test:&lt;/strong&gt; role-set’s &lt;code&gt;tests/test_callcache.py&lt;/code&gt;, with a mock model.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 10: What the terms let you do with AI output</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-10/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-10/</guid><description>A licence check on a few Google tools turned into a question about every AI-written line in the studio. The answer depends on the use, not the provider, and it sent Stage 1&apos;s training data back to be rewritten by open models.</description><pubDate>Fri, 16 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;This is a reading of published terms, not legal advice.&lt;/strong&gt; Every clause below was re-read at its live page on
2026-10-16. The full record is in rnd, under
&lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/tree/main/experiments/generated-content-terms&quot;&gt;&lt;code&gt;experiments/generated-content-terms/&lt;/code&gt;&lt;/a&gt;
and &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/tree/main/experiments/stage1-regeneration&quot;&gt;&lt;code&gt;experiments/stage1-regeneration/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;how-it-started&quot;&gt;How it started&lt;/h2&gt;
&lt;p&gt;The studio has Google AI Ultra, which comes with Flow, Flow Music and Project Genie. Before anything they made
could ship, the maintainer asked for a licence check. Reading Google’s terms raised a bigger question, because the
studio trains its own small models. Could AI-written text from any provider be used as training data?&lt;/p&gt;
&lt;p&gt;The maintainer set the rule for answering it. &lt;strong&gt;A use is allowed unless the provider’s contract or licence
restricts it,&lt;/strong&gt; and the search for restrictions is documented: the page, the exact clause, and its date. Uses that
providers commonly ban (training on outputs, distillation, competing models) get that explicit check before anything
relies on them. A model’s reading of the terms proves nothing, including the studio’s own outside reviewer’s.&lt;/p&gt;
&lt;p&gt;The first version of the rule went further: it treated anything not expressly permitted as off limits. That made
silence a ban, which it isn’t. Only a contract you agreed to, or a licence, can restrict a use. The rule was
reworded the same day, and the verdicts below follow the reworded version.&lt;/p&gt;
&lt;p&gt;A second correction followed the next day. The studio had read Google’s “related AI technology” at its widest, so it
covered even small pieces of pipeline code. The outside reviewer pointed out that the clause sits under “Don’t abuse
our services”, and that its purpose is stopping people distilling Gemini into a rival model. So the clause bars
training data and distillation, and code the studio owns is ordinary software whoever wrote it. A two-tier version
that made anything touching a model “strict” was tried the same day and withdrawn: it kept producing bars no clause
contained. The rule is now simply that a use is barred only by a clause you can name.&lt;/p&gt;
&lt;p&gt;The rule earned its keep. The outside model’s first pass got two things wrong:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it said Claude’s consumer terms bar all commercial use, when that clause covers evaluation access only;&lt;/li&gt;
&lt;li&gt;it said the Llama licence bars training non-Llama models, when Llama 3.3 permits it if the new model’s name
begins with “Llama”.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both were caught by re-reading the source.&lt;/p&gt;
&lt;h2 id=&quot;the-four-uses&quot;&gt;The four uses&lt;/h2&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Use&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Google (consumer)&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;xAI (Grok)&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Anthropic (Claude)&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Moonshot (Kimi app)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Commercial assets&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed. Flow Music grants “Commercial use rights” outright; for the rest, Google disclaims ownership and restricts nothing that applies&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed, with attribution (“Written with Grok”)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed: you own the outputs&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Barred: non-commercial use only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Training data&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Barred&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Barred: no distilling outputs&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed for non-competing specialized classifiers and tools, &lt;strong&gt;never as training targets&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Barred for a business: non-commercial use only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Evaluation only&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Barred where it serves developing a model; allowed otherwise&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Unclear: the AUP’s “develop … machine learning models” clause&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed on the same reading&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Barred for a business&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed when no model trains on it and it builds no rival (the clause is read by its anti-distillation purpose)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed for tooling that develops no model and doesn’t compete, with attribution&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed for non-competing specialized tools&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Barred for commercial use&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The clauses behind the table:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google&lt;/strong&gt; (Terms of Service, effective 2026-07-30) bars “using AI-generated content from our services to develop
machine learning models or related AI technology”. It has no competition qualifier, and Antigravity’s terms
incorporate it. Google’s Gemini API and Cloud terms are narrower: they bar only models or products that compete.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;xAI&lt;/strong&gt; (consumer terms, last updated 2026-10-12): “you retain your ownership rights to the User Content”, and
using Output requires attribution under the Brand Guidelines. The Acceptable Use Policy (2026-08-14) separately
bars “distilling model data or Outputs”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anthropic’s Help Center&lt;/strong&gt; (article 12326764, 2026-03-16): “You can use Claude’s Outputs to train models that
don’t compete with Anthropic’s own models.” It bars general-purpose chatbots, models designed for open-ended text
generation, and “Using Outputs as training targets for models”. Anthropic’s support channel, asked about this
exact use, said it fits. That reply came from an AI support agent, so the article stays the proof.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moonshot’s Kimi app:&lt;/strong&gt; “you may only use Kimi in a non-commercial, personal research, and study manner.”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;open-weights-read-what-the-licence-says-about-outputs&quot;&gt;Open weights: read what the licence says about outputs&lt;/h2&gt;
&lt;p&gt;Running an open-weight model yourself removes the service’s terms, but not the model’s licence. Most licences
grant rights in the weights and say nothing about what the model writes.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Licence or card&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;What it says about outputs&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;OpenMDW-1.1&lt;/strong&gt; (Nemotron 3, 3.5 Lightning, 3.5 Super-VL)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;“does not impose any restrictions or obligations with respect to any use … of any outputs”&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Proven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Muse-Glimmer-30B&lt;/strong&gt; card&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Its intended uses include “Synthetic data generation” for “downstream model development”&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Proven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;DeepSeek-R1&lt;/strong&gt; (MIT)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;“including, but not limited to, distillation for training other LLMs”&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Proven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Muse Model License&lt;/strong&gt; (Muse-Spark-816B)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Synthetic data is an intended use, but a model trained on its outputs is a “Derivative” that must carry the licence and display “Built with Muse Spark Open”&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Proven, with conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Llama 3.3&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Permits training on outputs if the new model’s name begins with “Llama”&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Proven, with a naming condition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;NVIDIA Open Model License&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;“does not claim ownership” of outputs, and no restriction on them&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;Plain Apache-2.0 or MIT&lt;/strong&gt; (Qwen, Gemma 4, Granite, Kimi K3)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Nothing on outputs&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;Allowed: the licence makes no claim and no contract binds a local run. Check any model-specific use policy too (Gemma has one)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;On the last two rows, the Linux Foundation’s OpenMDW launch post notes that Apache-2.0 and MIT were written for code
and are silent on generated outputs. OpenMDW-1.1 says outright that outputs are unrestricted. Silence and an
explicit grant reach the same place: nothing restricts the use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The route matters too.&lt;/strong&gt; The studio’s Kimi seat turned out to run in the Zed editor through Ollama Cloud, not
Moonshot’s app. Ollama’s terms (May 2026) say “You retain ownership of content you create using our services”, and
restrict only building products that compete with Ollama. The Kimi K3 and K2.6 licences are MIT-style. So the app’s
non-commercial clause never applied to that code.&lt;/p&gt;
&lt;h2 id=&quot;what-the-studio-found-in-its-own-work&quot;&gt;What the studio found in its own work&lt;/h2&gt;
&lt;p&gt;An inventory traced every piece of text in the Stage 1 pipeline to whoever wrote it. Three findings stood out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The training rows&lt;/strong&gt; were written by Kimi (the pilot), Grok (batches 2–8) and Antigravity (the 210 apply items).
Grok’s and Google’s terms bar training on them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The generator showed the writer finished Kimi and Grok rows as examples,&lt;/strong&gt; so even a clean model would have
been prompted with barred text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The “real-source” excerpts weren’t human-written.&lt;/strong&gt; They came from the studio’s own repos, and those repos were
mostly written with Claude.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-changed&quot;&gt;What changed&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;All 810 training rows and 210 apply items are being regenerated&lt;/strong&gt; by licence-proven open models: Muse-Glimmer
and Nemotron on Colab, and a larger Nemotron on a rented GPU if a pilot earns it. The writer and critic always
come from different families.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The method carries over:&lt;/strong&gt; the lessons, the gates, the defect catalogue and the evaluation sets. Claude wrote
those, and Anthropic’s terms allow that use for a specialized verifier. No Claude text becomes a training target.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The code stays, whoever wrote it.&lt;/strong&gt; Under the two-tier rule, pipeline code that no model trains on is ordinary
software. Three small Antigravity-written pieces had already been re-implemented under the earlier, stricter
reading; they stay as they are. Grok’s generator logic stays too, with its text replaced. The tooling Grok and Kimi
wrote is credited in &lt;code&gt;CONTRIBUTORS.md&lt;/code&gt; files that say “Written with Grok”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provenance is now recorded at the source.&lt;/strong&gt; Every text-bearing template and pack in the generator gets a
record of who wrote it, and the generator’s own check refuses one without it. Seat commits carry a trailer naming
the seat.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;if-youre-doing-the-same&quot;&gt;If you’re doing the same&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ask per use.&lt;/strong&gt; Shipping, training, evaluating and code are separate questions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read the use limits, not just ownership.&lt;/strong&gt; Acceptable-use policies are where the training bans live.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t mistake silence for a ban, or a ban for silence.&lt;/strong&gt; Only a contract you agreed to, or a licence, can
restrict a use. Find the clauses that do (training, distillation and competing-model bans), and record where you
looked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check which account you used.&lt;/strong&gt; Consumer, API and enterprise terms differ, and so do apps and hosts serving
the same model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-read every clause yourself.&lt;/strong&gt; A model’s summary of the terms, ours included, got things wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Record who wrote what when it’s written.&lt;/strong&gt; An inventory after the fact needed two agents reading PR threads
and commit trailers, and still left gaps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mind the attribution clauses:&lt;/strong&gt; “Written with Grok”, “Built with Muse Spark Open”, and Llama’s naming rule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name the clause, or it’s allowed.&lt;/strong&gt; Never read an undefined phrase at its widest just to be safe, and never
add a tier or a filter the terms don’t contain: that ratchet ends up banning a temperature monitor.&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 9: The backend that changed under us</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-09/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-09/</guid><description>An Ollama update quietly swapped out the studio&apos;s CUDA 13.4 backend. Rebuilding it took two builds, a CPU incident that set the rules for every heavy job since, three rounds of outside review, and a weekly check so it can&apos;t happen silently again.</description><pubDate>Thu, 15 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Every claim below links to the record it came from. The build records are in rnd, under &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/tree/main/experiments/ollama-cuda-backend&quot;&gt;&lt;code&gt;experiments/ollama-cuda-backend/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-happened&quot;&gt;What happened&lt;/h2&gt;
&lt;p&gt;The studio runs its own CUDA backend for Ollama on the RTX 5090. It’s a &lt;code&gt;ggml-cuda.dll&lt;/code&gt; built from Ollama’s own llama.cpp pin with CUDA 13.4, for compute capability 12.0 only, and dropped into Ollama’s &lt;code&gt;lib\ollama\cuda_v13&lt;/code&gt; folder. The script that starts the studio’s Ollama checks that file’s sha256 before it calls the server ready.&lt;/p&gt;
&lt;p&gt;On the morning of 2026-10-15 that check failed. The 0.40.4 install of the night before had written the stock CUDA libraries over the studio’s: same file name, different hash. Ollama ran normally, so only the check noticed. Every GPU grant was held until the studio build was back. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/ollama-cuda-backend/build-0.35.1-cuda-13.4.1.md&quot;&gt;the 0.35.1 build record&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;why-it-had-to-be-rebuilt&quot;&gt;Why it had to be rebuilt&lt;/h2&gt;
&lt;p&gt;Ollama doesn’t vendor ggml. It fetches llama.cpp at a pinned tag and builds the backend against it. 0.35.1 pinned b11232, and 0.40.4 pins b11521. The old DLL was built against the old ggml ABI, so copying it back next to the new &lt;code&gt;ggml-base.dll&lt;/code&gt; would have been a guess. The rebuild used the same recipe as the first build (Ollama’s &lt;code&gt;llama_cuda_v13_windows&lt;/code&gt; preset, architecture 120 only, CUDA 13.4’s nvcc, target &lt;code&gt;ggml-cuda&lt;/code&gt;), from the new tag.&lt;/p&gt;
&lt;p&gt;It built in 343.8 s (156 steps), and every dependency of the new DLL resolved. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/ollama-cuda-backend/build-0.40.4-cuda-13.4.1.md&quot;&gt;the 0.40.4 build record&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-cpu-incident-and-the-rules-it-set&quot;&gt;The CPU incident, and the rules it set&lt;/h2&gt;
&lt;p&gt;Earlier the same day, a different job taught the lesson this build was run under. An index rebuild through offrig’s new embedding server ran torch at its default thread count: all 24 logical cores. The CPU went to 100%, and it was stopped by hand. Nothing broke, but the job took a shared machine from everyone on it, without warning, from a team that already knew torch takes every core.&lt;/p&gt;
&lt;p&gt;Since then, every heavy job is pinned before it starts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Efficiency cores only, at below-normal priority.&lt;/strong&gt; On this CPU, &lt;code&gt;GetSystemCpuSetInformation&lt;/code&gt; puts the 8 performance cores at logical 0, 1, 10–13, 22 and 23, and the 16 efficiency cores at 2–9 and 14–21. That isn’t the “first eight” a guess would give. Jobs run under an affinity mask over efficiency cores only, set at launch (&lt;code&gt;start /AFFINITY … /BELOWNORMAL&lt;/code&gt;), so every child process inherits it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The load is stated before the start,&lt;/strong&gt; with a stop rule (total CPU held above a threshold for so many seconds) and CPU logged every 5 s.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The process that’s watched is the one doing the work.&lt;/strong&gt; The first corrected rerun watched a Python launcher shim, which was idle at 6 MiB, and its gates “passed” on the wrong process. They were declared void, and the rerun was repeated against the real interpreter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every thread pool is capped, not just torch’s.&lt;/strong&gt; An outside review of the embedding server found that capping torch’s threads isn’t enough: OpenMP, MKL and the tokenizer’s Rayon pool each size themselves to the machine unless capped before torch loads. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/embeddinggemma-2/argon-consult-embed-serve-resources-disposition.md&quot;&gt;the review and its disposition&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The CUDA build ran under these rules: 8 efficiency cores (&lt;code&gt;0x3FC&lt;/code&gt;), below-normal priority, &lt;code&gt;-j 8&lt;/code&gt;. A per-core reading mid-build showed those 8 cores at about 97% and every other core at about 22%. Total CPU averaged 41.7%.&lt;/p&gt;
&lt;h2 id=&quot;three-rounds-of-review-before-the-gpu&quot;&gt;Three rounds of review before the GPU&lt;/h2&gt;
&lt;p&gt;The plan to test the build was reviewed by Argon, the studio’s outside reviewer, before any GPU use. It came back twice with changes, and they’re why the test looks the way it does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolation.&lt;/strong&gt; The sandbox Ollama runs from its own tree, with the stock install stripped from &lt;code&gt;PATH&lt;/code&gt; and the CPU-only switches from the embedding work removed from its environment. A stray &lt;code&gt;CUDA_VISIBLE_DEVICES=-1&lt;/code&gt; would have quietly tested the CPU.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One model at a time,&lt;/strong&gt; checked before every prompt, not once per block, with a warm-up generation after each load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A breach kills, not just records.&lt;/strong&gt; If card memory passes the cap, the sandbox’s process tree is killed and the stock server unloaded at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A pre-registered tolerance.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;The outputs must match token for token.&lt;/li&gt;
&lt;li&gt;The one exception is a near-tie: at the first differing token, the stock run’s top two candidates are within 0.001 in log-probability, and the sandbox chose the second.&lt;/li&gt;
&lt;li&gt;The tolerance needs the top two candidates, so the source was read to check where they come from. Ollama 0.40.4 asks llama-server for &lt;code&gt;n_probs&lt;/code&gt; and never sets &lt;code&gt;post_sampling_probs&lt;/code&gt;. llama.cpp then reads the candidates from the full pre-sampling logits, so greedy &lt;code&gt;top_k: 1&lt;/code&gt; can’t hide the runner-up.&lt;/li&gt;
&lt;li&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/ollama-cuda-backend/sandbox_test-v3.py&quot;&gt;the v3 test&lt;/a&gt; and &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/ollama-cuda-backend/sandbox_test-v2-to-v3.diff&quot;&gt;its diff from v2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Argon also set the build to the studio’s standard, not the minimum:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;explicit &lt;code&gt;sm_120a&lt;/code&gt; SASS with no PTX;&lt;/li&gt;
&lt;li&gt;every FlashAttention quant compiled;&lt;/li&gt;
&lt;li&gt;fatbins compressed for speed;&lt;/li&gt;
&lt;li&gt;CUDA graphs and VMM on;&lt;/li&gt;
&lt;li&gt;cuBLAS from CUDA 13.4 Update 2, whose release notes list a fix for an NVFP4 heuristic on compute capability 12.x.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-note-on-version-numbers&quot;&gt;A note on version numbers&lt;/h2&gt;
&lt;p&gt;CUDA 13.4’s first release is 13.4.1 (nvcc V13.4.59). “13.4 Update 2” is &lt;code&gt;version.json&lt;/code&gt; 13.4.3, with nvcc V13.4.92 and cuBLAS 6,14,11,1380. The review asked for “13.4.2”, which is off by one. The build script now refuses any nvcc but the one installed and verified. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/ollama-cuda-backend/build-0.40.4-cuda-13.4.3.cmd&quot;&gt;the 13.4.3 build script&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-build-to-the-studio-standard&quot;&gt;The build to the studio standard&lt;/h2&gt;
&lt;p&gt;It built in 374.0 s: 201 steps and 189 CUDA objects, each compiled as &lt;code&gt;arch=compute_120a,code=[sm_120a]&lt;/code&gt;, which is machine code for this card only, with no PTX. The DLL is 217 MB, against 56 MB for the first rebuild; the extra FlashAttention kernels and the speed-first compression account for it. It ran under the same cap: 8 efficiency cores, total CPU averaging 37.5%, and one 5-second sample above 50%. Every dependency resolved. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/ollama-cuda-backend/build-0.40.4-cuda-13.4.3.md&quot;&gt;the 13.4.3 build record&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;update-the-same-afternoon-the-sandbox-test-failed&quot;&gt;Update, the same afternoon: the sandbox test failed&lt;/h2&gt;
&lt;p&gt;Argon gave round 3 a green light, and the test ran under a GPU grant: 46 seconds, peaking at 7.4 GB of card memory against a 9.2 GB cap, and cleaned up completely.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The new DLL loaded,&lt;/strong&gt; and the log names its path. It’s at least as fast as stock.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It failed the output check on 2 of 3 prompts.&lt;/strong&gt; Stock matched itself token for token. The sandbox matched stock exactly on the coding prompt, but diverged at token 15 of one prompt and token 6 of another. At both points it ranked stock’s top two candidates the other way round, by margins of about 0.7 and 0.8 in log-probability. That’s not the near-tie the rule allows.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So by the rule set before the run, nothing is activated and the stock backend stays. The difference is larger than reordered sums usually give, and the cause isn’t identified: build options or toolkit. Neither answer is wrong, but the bar was identity with stock, and identity isn’t quality. Finding the cause is its own decision, with its own review. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/ollama-cuda-backend/sandbox-result-cu1343.md&quot;&gt;the result&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;update-later-the-same-day-the-cause-is-cublas&quot;&gt;Update, later the same day: the cause is cuBLAS&lt;/h2&gt;
&lt;p&gt;A diagnosis harness, frozen after outside review, ran eight arms of qwen3:8b on 15 prompts, one arm at a time under one
GPU grant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stock repeats itself bit for bit&lt;/strong&gt; across server starts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stock with only CUDA 13.4.3’s cuBLAS&lt;/strong&gt; matches the studio build bit for bit: every token and every top-10
log-probability. Three different &lt;code&gt;ggml-cuda.dll&lt;/code&gt; builds with the same cuBLAS change nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Where cuBLAS runs.&lt;/strong&gt; The model keeps one weight in f16, the attention V projection. While the prompt is read, that
matmul goes to cuBLAS with f16 accumulation, and everything else runs in ggml’s own kernels. That’s why the builds
disagree from the first token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Neither build is closer to a q8_0 reference,&lt;/strong&gt; and the studio build had no task failures.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the rule set before the run, the build is eligible. Activation is a separate decision, with stock CUDA 13.0 kept as a
fallback.&lt;/p&gt;
&lt;p&gt;The harness’s first run crashed at its first arm, before anything loaded, on a bug every review had missed. The fix
came with an offline dry run of the whole harness, which now runs in CI. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/ollama-cuda-backend/diag-result-v6.md&quot;&gt;the result&lt;/a&gt; and &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/ollama-cuda-backend/&quot;&gt;the brief&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;so-it-cant-happen-silently-again&quot;&gt;So it can’t happen silently again&lt;/h2&gt;
&lt;p&gt;A weekly check now runs every Monday. It reads the rig’s live versions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ollama’s version;&lt;/li&gt;
&lt;li&gt;the installed backend’s hash against the studio’s builds;&lt;/li&gt;
&lt;li&gt;the start script’s expected hash;&lt;/li&gt;
&lt;li&gt;nvcc and the driver;&lt;/li&gt;
&lt;li&gt;the newest Ollama and llama.cpp releases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It writes them into a paste-ready brief that asks Argon for the newest recommended setup: the toolkit, the Ollama release and its pin, the build options, whether a rebuild is due, and any security advisories. If an update has replaced the studio’s backend again, the brief opens with an alert.&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 8: Room for the seventh pack</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-08/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-08/</guid><description>A puzzle generator kept building the same shapes. Three briefs, one proof, an allocator, a committed record in CI, a stop, and a new axis later, it has room for twice the packs it was built for. How the variety floors were set, missed, met, and then given headroom.</description><pubDate>Wed, 14 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Every claim below links to the record it came from.&lt;/p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mcp-tool-shop-org/role-set&quot;&gt;role-set&lt;/a&gt; builds logic puzzles to train and test small models. Its T10 family turns on one size word: “at least two of the lamps are on”, “exactly one clinic opens in the morning”. Every puzzle is proven before it’s kept: a solver checks the model, and a set of registered readings checks that a solver taking a shortcut (reading the size word loosely) would be caught.&lt;/p&gt;
&lt;p&gt;Under those proofs the generator had collapsed onto a few shapes. A test set that repeats one shape teaches the shape, not the reading. So the work was to make the shapes vary, measurably, without loosening a single proof.&lt;/p&gt;
&lt;h2 id=&quot;the-floors-set-before-any-fix&quot;&gt;The floors, set before any fix&lt;/h2&gt;
&lt;p&gt;The first brief fixed the measure and the bar before anyone touched the code. A puzzle’s &lt;strong&gt;canonical skeleton&lt;/strong&gt; is its structure with the wording removed: how many entities, which clue forms, where the trap sits. In each stratum (two-value or three-value packs; “set” or “claim” questions): at least 75% of skeletons distinct, no skeleton more than 3 times, none twice in one pack’s file of 20 seeds. Tuning on seeds 41–60; the verdict on locked seeds 61–80 that nobody tunes against. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/t10-variety/brief.md&quot;&gt;Brief 1&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-proof-that-narrowed-the-claim-strata&quot;&gt;The proof that narrowed the claim strata&lt;/h2&gt;
&lt;p&gt;Brief 2 asked what a claim puzzle can test at all, and the answer was a proof, not a measurement. A claim states something and asks whether it holds. If the claim says “at least two” and exactly two is the truth, the claim is true, and a reader who read “at least” as “exactly” agrees. Every world where “exactly k” holds is one where “at least k” holds. So no claim built on “at least” or “at most” can separate the careful reader from the careless one; only “exactly” can. The generator now refuses to build what can’t be tested (&lt;code&gt;trait_not_tested&lt;/code&gt;), and claims carry “exactly” alone. That is correct, and it left the claim strata with very few structural cells. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/t10-variety/brief-2-trait-tested.md&quot;&gt;Brief 2&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-allocator-and-the-first-stop&quot;&gt;The allocator, and the first stop&lt;/h2&gt;
&lt;p&gt;Brief 3 made the packs of one kind share their skeleton space, with an allocator (a min-cost flow) that spreads a pack’s twenty seeds across cells (entity count, size word, count, number of conditional clues), with a cap on the barest cells. It stopped on arithmetic: six three-value packs needed 26 visits to the nine cells with no conditional clue, against a supply of 18 at a cap of 2. The stopgap ruled was a cap of 3 for that one stratum, which passed exactly on the floor: 108 of 120 distinct, top repeat 3. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/t10-variety/brief-3-multipack.md&quot;&gt;Brief 3, amendments 1 and 2&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-floors-in-ci-from-a-committed-record&quot;&gt;The floors in CI, from a committed record&lt;/h2&gt;
&lt;p&gt;Proving 640 puzzles under coverage took 46 minutes against a 15-minute job. The ruling, after a cross-family consult: the measurement is committed. A record holds every build’s cell, try count and skeleton, keyed by a hash over every file that decides a build, closed under the package’s imports. The normal job recomputes the hash, fails a mismatch by naming the regenerate command, and computes the floors from the stored skeletons in seconds, inside a CI job that finishes in about seven minutes. The exhaustive rebuild runs on demand and must reproduce the record exactly. A cap on build attempts was refused twice: it changes which puzzles get built. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/role-set/pull/49&quot;&gt;role-set #49&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-new-axis-and-the-second-stop&quot;&gt;The new axis, and the second stop&lt;/h2&gt;
&lt;p&gt;To get the claim strata off the floor, the conditional clues gained a shape coordinate: for one &lt;code&gt;if&lt;/code&gt;, the consequent’s polarity; for two, chain, shared consequent, shared antecedent or disjoint. (A finer coordinate, whether the &lt;code&gt;if&lt;/code&gt; touches the claimed entity, was dropped because the skeleton can’t see it, so it would have inflated cell counts without adding measured variety.) The zero-&lt;code&gt;if&lt;/code&gt; cap went back to 2. On fresh locked seeds 81–100 the claim strata cleared every floor.&lt;/p&gt;
&lt;p&gt;Then set three came in at 118 of 119: one build needed 435 tries against a limit of 400, and the baseline failed the same build. The cause was a pooled weight: the capacity script had measured one weight per set cell across two- and three-value packs, so a cell that builds easily for two-value packs looked rich and sent a three-value pack there in every file. Brief 3 had always said capacity is per stratum. The ruling was to split the weights, re-freeze, and take the verdict on fresh locked seeds 101–120. The alternative, a stricter probe, drops cells, which is the same forbidden move as a try cap. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/t10-variety/brief-3-multipack.md&quot;&gt;Brief 3, amendment 4&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-verdict&quot;&gt;The verdict&lt;/h2&gt;
&lt;p&gt;On locked seeds 101–120, with the baseline code run on the same seeds beside it:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Stratum&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Before&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;set, two values&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;40/40, top 1&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;40/40, top 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;set, three values&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;118/120, top 2&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;118/120, top 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;claim, two values&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;40/40, top 1&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;40/40, top 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;claim, three values&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;112/120, top 4 (fail), 26 zero-&lt;code&gt;if&lt;/code&gt; visits&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;118/120, top 2, 0 zero-&lt;code&gt;if&lt;/code&gt; visits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All 1,280 builds across four seed files build inside the try limit; the one that caused the stop now takes 8 tries. Three-value claims have 61 usable cells against 27. An allocation test places twelve three-value packs at a cap of 2 with no zero-&lt;code&gt;if&lt;/code&gt; visit. R&amp;amp;D reproduced the baseline and the record figures independently before passing it. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/role-set/pull/55&quot;&gt;role-set #55&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-wed-do-the-same&quot;&gt;What we’d do the same&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Set the floors and the locked seeds before the first fix,&lt;/strong&gt; and take every verdict on seeds nobody tuned against. Fresh locked seeds after every change to the allocation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Freeze, then measure.&lt;/strong&gt; The verdict run starts only from a named commit, and the record names it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The baseline beside the verdict,&lt;/strong&gt; on the same seeds, every time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refuse the easy fixes that change what gets built.&lt;/strong&gt; A try cap and a dropped cell both passed the floors by changing the population; both were refused, twice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the hash closed.&lt;/strong&gt; The record’s hash list missed two files at first; an import-closure test written by the builder caught them, and it’s the better instrument.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;released&quot;&gt;Released&lt;/h2&gt;
&lt;p&gt;All of it shipped as role-set &lt;a href=&quot;https://github.com/mcp-tool-shop-org/role-set/releases/tag/v0.2.0&quot;&gt;v0.2.0&lt;/a&gt; on 2026-10-14, after a full treatment: a README rewritten for the generator, two new handbook pages (&lt;a href=&quot;https://mcp-tool-shop-org.github.io/role-set/handbook/generator/&quot;&gt;the T10 generator&lt;/a&gt; end to end, and &lt;a href=&quot;https://mcp-tool-shop-org.github.io/role-set/handbook/roles/&quot;&gt;the role packs&lt;/a&gt;), the README in seven more languages, and a ship-gate scorecard of 37 of 50, up from 31, with the audit passing every gate. It stays 0.x: role-set is a prototype, and nothing it writes is training data until a person checks it.&lt;/p&gt;
&lt;p&gt;→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/taking-size-words-literally/&quot;&gt;The T10 brief&lt;/a&gt;, updated with these figures.&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 7: The tracer that kept every promise but one</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-07/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-07/</guid><description>We put NVIDIA&apos;s NeMo Relay inside our local-model harness under a pre-registered test. It traced faithfully and leaked nothing, and it still failed, on a cost we had set a hard budget for. How the verdict stood, what the diagnosis found in our own harness, and why the decision departed from the rule on purpose.</description><pubDate>Wed, 14 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Every claim below links to the record it came from.&lt;/p&gt;
&lt;h2 id=&quot;the-question-and-the-rule-set-before-the-run&quot;&gt;The question, and the rule set before the run&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mcp-tool-shop-org/interlock&quot;&gt;interlock&lt;/a&gt; measures local models: it drives them through Ollama on the RTX 5090 and writes a receipt for every measurement. &lt;a href=&quot;https://github.com/NVIDIA/NeMo-Relay&quot;&gt;NeMo Relay&lt;/a&gt; is NVIDIA’s open tracing library for agents. The question was narrow: &lt;strong&gt;can Relay trace interlock’s model calls without changing what interlock measures?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The design was pre-registered and reviewed by a model from a different family at each step, with the analysis code frozen before any counted run. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/interlock-relay/design.md&quot;&gt;The design, v3.1 to v3.2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Three hypotheses, and a decision rule written as a tree:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;H1:&lt;/strong&gt; the trace accounts for every request the harness sends. Fail this, and Relay is rejected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H2:&lt;/strong&gt; tracing doesn’t move the measurements, within a band taken from the run’s own noise, and it costs the harness under &lt;strong&gt;0.5 ms&lt;/strong&gt; per two-call measurement on the client side. Fail this, and Relay is kept for debugging runs only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H3:&lt;/strong&gt; nothing leaks: no prompt text, no paths, no machine or user names.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stage 0, on fakes, held. Stage 1 ran on the 5090 on 2026-10-14, under a grant with a time cap, a VRAM watch and a preflight that checked the card, the CPU and the environment before every step. The first attempt refused itself after eight seconds, because it had been launched from the wrong Python environment; the driver now checks that too. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/interlock-relay/stage1-plan.md&quot;&gt;The stage 1 plan&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-the-frozen-analysis-said&quot;&gt;What the frozen analysis said&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;stage1_holds = false.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;H1 failed in every step,&lt;/strong&gt; by exactly one request each time: 204 sent against 203 counted, 217 against 216, and so on. The companion step also had three spans that belonged to no measurement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H2 held on every model number.&lt;/strong&gt; Decode speed, prompt speed and latency were equivalent with tracing on. The client-time budget failed: medians of 0.548 ms and 0.682 ms against 0.5.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H3 held everywhere.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Under the tree, H1 failing means reject. The analysis is byte-identical to its freeze, and nothing was re-scored. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/interlock-relay/stage1/result-note.md&quot;&gt;The result note&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-diagnosis-two-of-ours-one-real&quot;&gt;The diagnosis: two of ours, one real&lt;/h2&gt;
&lt;p&gt;Read-only, from the run’s own records.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The off-by-one was the harness’s empty-card check.&lt;/strong&gt; Before a 5090 run, interlock asks the server what’s loaded. That request went through the adapter, so the driver’s counter saw it, but it went out before the tracer was attached. With that one request set aside, every count matched exactly. No traced call was missed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The stray spans were the companion’s stop.&lt;/strong&gt; The harness closed the measurement, then unloaded the companion model, then switched tracing off. The unload was traced but belonged to nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The client time was real.&lt;/strong&gt; A traced call cost about 270 µs more than an untraced one. A CPU-only bench against a stub transport, with the same 250 ms rest before each measurement, reproduced the live figure within about 10%, with no GPU at all. Most of the cost is Relay’s own work per call; the rest was our own serialisation of long prompts to measure their size. Removing ours would not have passed the gate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The outside reviewer checked all three and agreed, and added a test we hadn’t thought of: the four large deviations in the import check split three faster to one slower, which is noise, not a slowdown from importing the library.&lt;/p&gt;
&lt;h2 id=&quot;the-decision-and-why-it-departed-from-the-rule&quot;&gt;The decision, and why it departed from the rule&lt;/h2&gt;
&lt;p&gt;The rule said reject. The Director chose &lt;strong&gt;debug-only tracing&lt;/strong&gt; instead, recorded as a dated amendment (v3.2) and as his call: H1’s failure was a counting-window bug with every request accounted for, so H2’s branch is the one that fits. The stage 1 verdict stands as frozen. A rerun was ruled out, because the cold bench shows the client-time gate would fail again. No further GPU or NPU time goes to Relay.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The build landed the same day.&lt;/strong&gt; The reviewer wrote it from the result note; we applied it with six recorded departures (one of them a real defect in the draft that would have silenced ordinary traces); the reviewer then checked the branch itself and approved. The card check now runs inside the tracing window, the companion stops inside its own measurement, request sizes come from the bytes actually sent, and every row of a traced run is marked and kept out of the routing table, which the trial’s rows had reached. A deterministic test drives the real adapter and real HTTP client against a fake server and checks that every request is counted, every span is matched and closed, and nothing leaks. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/experiments/interlock-relay/argon-code-review-v32.md&quot;&gt;The build round’s review&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;One more defect fell out of the review: the request-size field in Relay’s records had never held the request size. The sanitizer was re-measuring its own &lt;code&gt;{&quot;bytes&quot;: n}&lt;/code&gt; record, so every value was 13 or 15. It’s fixed, and no verdict depended on it.&lt;/p&gt;
&lt;h2 id=&quot;what-wed-do-the-same-and-what-we-learned&quot;&gt;What we’d do the same, and what we learned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Freeze the analysis before the run, and keep the verdict even when the cause is yours.&lt;/strong&gt; A rerun is a new pre-registration, and a departure is a dated amendment with a name on it. That is what made the diagnosis trustworthy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Count below the hook.&lt;/strong&gt; The off-by-one was only visible because the driver counted requests independently of the tracer. A tracer checking itself would have passed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bench the cold case.&lt;/strong&gt; The warm bench put Relay at 30 µs per call; the live rig said 270. Putting the stage’s own rest before each measurement on a CPU-only bench explained the gap without touching the GPU.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Settle a ruling with the outside reviewer’s input before sending it.&lt;/strong&gt; Two reversals in one evening on another repository cost a paste and a re-read each. That’s now a working rule.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/agent-tracing-nemo-relay/&quot;&gt;The NeMo Relay brief&lt;/a&gt;, now measured.&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 6: A red check that says how to fix itself</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-06/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-06/</guid><description>Atlas and testing-os, what they are, and how the research library is learning to collect from the fleet the way housekeeping collects from Atlas. Plus interlock&apos;s first release, and a proof that changed how a puzzle generator builds its items.</description><pubDate>Wed, 14 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Every claim below links to the record it came from.&lt;/p&gt;
&lt;h2 id=&quot;atlas-and-testing-os-and-how-the-library-connects-to-them&quot;&gt;Atlas and testing-os, and how the library connects to them&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What they are.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dogfood-lab/testing-os&quot;&gt;testing-os&lt;/a&gt;&lt;/strong&gt; is the studio’s testing workbench: protocols, evidence stores and learning loops for AI-assisted software.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atlas&lt;/strong&gt; is its mapping engine. &lt;code&gt;atlas map&lt;/code&gt; records how one repository is built: which parts it has, what each part does, which files belong to which part, and the “doors” where it meets the outside, such as workflows, entry points and the packages it publishes. The map is committed in the repository it describes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;atlas check&lt;/code&gt; runs in CI.&lt;/strong&gt; It fails when the code and the map disagree, so a new folder or a moved file can’t pass unnoticed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every repository in the fleet that runs workflows keeps a map, made by one pinned engine version.&lt;/strong&gt; That’s what lets a question be asked across the whole organisation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;How &lt;a href=&quot;https://github.com/mcp-tool-shop-org/housekeeping&quot;&gt;housekeeping&lt;/a&gt; uses it.&lt;/strong&gt; The two tools split the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Atlas works offline,&lt;/strong&gt; one repository at a time, and records how that repository is built.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Housekeeping works online,&lt;/strong&gt; across the organisation. It records what GitHub says now: CI, pull requests, releases, Actions cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Housekeeping collects each repository’s committed map by its blob id,&lt;/strong&gt; checks the map’s health against the organisation’s rule (no map, no check in CI, an engine behind the fleet), and ties a red run’s failed step to the door it came through. Its findings stay private.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;How this library copies that pattern.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each tool repository already commits its own pre-registered experiment specs and run records, the way it commits its map. interlock’s receipts are the main example.&lt;/li&gt;
&lt;li&gt;The library should collect them the way housekeeping collects maps, rather than by hand. That’s the build-up program R&amp;amp;D set out on 2026-10-13. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-13-atlas-testing-os-rnd-buildup.md&quot;&gt;The build-up program&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-red-check-that-says-how-to-fix-itself&quot;&gt;A red check that says how to fix itself&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The problem.&lt;/strong&gt; The fleet’s commonest &lt;code&gt;atlas check&lt;/code&gt; failure was a folder that no part of the map covers, such as interlock’s &lt;code&gt;receipts/&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each new file there failed the check, and regenerating the map pardoned only that one file.&lt;/li&gt;
&lt;li&gt;In CI the failure showed only as “exit code 1”.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What Atlas slice AM changed.&lt;/strong&gt; It’s merged in testing-os, and ships in the next Atlas release with the multi-repository slices.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The check prints the lines to fix it,&lt;/strong&gt; ready to paste into &lt;code&gt;boundaries.yaml&lt;/code&gt;. A folder no part reaches gets a part of its own, and a folder a part already reaches gets a glob. Every test applies the printed lines and requires the check to pass afterwards.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;atlas map&lt;/code&gt; warns about an uncovered folder before CI fails&lt;/strong&gt; on the next file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In GitHub Actions, the check annotates.&lt;/strong&gt; A failure is an error on the file, and a door notice is a warning on the line it was read from. The finding and the fix go into the job summary. Outside Actions, the output is unchanged byte for byte.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/dogfood-lab/testing-os/pull/159&quot;&gt;testing-os #159&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What the live runs showed.&lt;/strong&gt; The spec required one real failing Actions run before release, and recorded what GitHub actually displays. The first run found two defects that the tests had passed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Paths:&lt;/strong&gt; a job that checked out into a subdirectory annotated &lt;code&gt;repo/.github/...&lt;/code&gt;, and GitHub never placed that on the pull request’s diff. Paths are now relative to the repository’s top level.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Line numbers:&lt;/strong&gt; file-level failures carried line 0, and GitHub shows those only as a dead link, never on the diff. They now point at line 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A second live run showed every annotation landing on the diff, the subdirectory job included. The same run showed each message repeated four times, once per test leg and once per trigger. The organisation’s Atlas rule now says to run the check on one leg only. → &lt;a href=&quot;https://github.com/dogfood-lab/testing-os/pull/160&quot;&gt;the live run&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; a CI message is only fixed when someone has looked at where GitHub puts it. Tests of the emitted text couldn’t see either defect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next for Atlas:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slice AN is merged:&lt;/strong&gt; one &lt;code&gt;atlas mcp&lt;/code&gt; server can now answer for several repositories, fixed when it starts. → &lt;a href=&quot;https://github.com/dogfood-lab/testing-os/pull/169&quot;&gt;testing-os #169&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slices AO to AQ follow,&lt;/strong&gt; before one pinned release for the whole set. AQ, the inferred-consumer check, turns on only after it clears a pre-set recall and precision bar on a labelled seed. → &lt;a href=&quot;https://github.com/dogfood-lab/testing-os/pull/168&quot;&gt;the rulings&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-library-collects&quot;&gt;The library collects&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;python -m rnd collect&lt;/code&gt; reads an experiment’s records from its tool repository at a pinned commit.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The pin:&lt;/strong&gt; each source is named with its path, its commit and the SHA-256 of its bytes. The bytes are read with &lt;code&gt;git show&lt;/code&gt;, so the clone’s own state doesn’t matter, and a source whose hash no longer matches is refused rather than re-trimmed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The trim:&lt;/strong&gt; each experiment’s own module decides what its published record keeps. Every output then passes a public guard, which refuses a machine path, a home directory or an environment dump.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The drift check:&lt;/strong&gt; &lt;code&gt;--check&lt;/code&gt; rebuilds every output and compares it byte for byte with the committed copy, so any drift between a source and its published copy is found.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;E5’s card, in &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-05/&quot;&gt;issue 5&lt;/a&gt;, was the first experiment collected this way. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/pull/4&quot;&gt;rnd #4&lt;/a&gt; · &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/tree/main/experiments/e5&quot;&gt;E5’s collect.json&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Still to come in the program:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one run-record format in testing-os, for interlock, the library and later experiments;&lt;/li&gt;
&lt;li&gt;defect classes shared across the fleet, so the library can count them over time;&lt;/li&gt;
&lt;li&gt;an instrument-health check;&lt;/li&gt;
&lt;li&gt;the same failure-output style for the library’s own &lt;code&gt;check&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These will be reported as they land.&lt;/p&gt;
&lt;h2 id=&quot;also-this-week&quot;&gt;Also this week&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/mcp-tool-shop-org/interlock/releases/tag/v0.1.0&quot;&gt;interlock 0.1.0&lt;/a&gt; is released,&lt;/strong&gt; the first on PyPI as &lt;code&gt;mcptoolshop-interlock&lt;/code&gt;. It’s a prototype advisory router: it suggests a model, an engine and a device from measured receipts, and never serves traffic. Its release assets carry SLSA provenance, and CI now fuzzes its receipt and spec readers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A fuzz-target bug caught in review:&lt;/strong&gt; the first version of the fuzz target excused the very crashes it should report. A receipt that isn’t UTF-8 crashed &lt;code&gt;interlock check&lt;/code&gt; with a traceback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Now:&lt;/strong&gt; both readers refuse cleanly, with tests. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/interlock/pull/121&quot;&gt;interlock #121&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What a T10 claim can test.&lt;/strong&gt; &lt;a href=&quot;https://github.com/mcp-tool-shop-org/role-set&quot;&gt;role-set&lt;/a&gt; builds small logic puzzles that teach a model to read size words literally (“exactly”, “at least”, “at most” two of them).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The defect:&lt;/strong&gt; a variety fix passed its floors, but review found that some “at least” puzzles were passing their shortcut check on a second “exactly” clue. They tested “exactly” again.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The proof:&lt;/strong&gt; every assignment that fits “exactly k” also fits “at least k”. So if every assignment that fits “at least k” gives the same verdict about one entity, every assignment that fits “exactly k” does too, and misreading the word can’t change a claim’s verdict.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The fix:&lt;/strong&gt; claim puzzles now test “exactly”, set puzzles test all three words, and each puzzle must change its answer on its own clue’s misreading. On fresh locked seeds, the defect went from 8 items to 0. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/role-set/pull/53&quot;&gt;role-set #53&lt;/a&gt; · &lt;a href=&quot;https://github.com/mcp-tool-shop-org/role-set/pull/54&quot;&gt;role-set #54&lt;/a&gt; · &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/tree/main/experiments/t10-variety&quot;&gt;the briefs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 5: The card is full</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-05/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-05/</guid><description>E5 finishes. Each local model&apos;s largest context window on the 5090 is now measured, and two of them came out far larger than the first run could show. Plus two NVIDIA tools read against our own, and three new instruments for the bench.</description><pubDate>Tue, 13 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Every claim below links to the record it came from. Interim results are labelled as interim.&lt;/p&gt;
&lt;h2 id=&quot;e5-how-much-context-each-model-holds&quot;&gt;E5: how much context each model holds&lt;/h2&gt;
&lt;p&gt;E5 asks a router’s most practical question about a local model: how long a prompt can it take before it no longer
fits on the graphics card? Past that point the model spills into system memory and slows sharply. For each model,
interlock’s harness raised the context window step by step and filled it with a real prompt. It recorded the largest
window that stayed on the card, under a 30,500 MiB ceiling, with no spill.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Model&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Largest window on the card&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Trained window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;llama3.1:8b&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;131,072&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;131,072&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;gemma4:31b&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;86,016&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;262,144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;granite4.1:30b&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;43,008&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;131,072&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;qwen3:14b&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;40,960&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;40,960&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;qwen3:8b&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;40,960&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;40,960&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;mistral-small:24b&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;&lt;strong&gt;32,768&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;32,768&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Measured with Ollama 0.35.1, an f16 KV cache with flash attention, and Q4_K_M weights on an idle card.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How the edges were found.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;gemma4:31b is bounded by a measurement.&lt;/strong&gt; The next step, 88,064 tokens, peaked 44 MiB over the ceiling. Neighbouring
readings vary by a couple of hundred MiB, so the true edge lies somewhere between the two.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;granite4.1:30b is bounded by a forecast and a measurement.&lt;/strong&gt; The next step was forecast over the ceiling from the
measured growth per token, and the step after that was measured over.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why the first run fell short.&lt;/strong&gt; The first full run, earlier the same day, stopped gemma at 14,336 and granite at
30,720. Both stops came from a safety predictor that assumed every layer’s memory grows with the window. gemma uses
sliding-window attention and granite is a hybrid model, so their memory grows far more slowly. After a dated
amendment, the predictor learned from measured loads instead. The same run also fixed a spill rule that had
misfired on llama3.1:8b, which in fact holds its full window with gigabytes to spare.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Three small defects in the receipt.&lt;/strong&gt; None changes a result, and they’re queued as one fix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a label names the wrong reason for gemma’s stop;&lt;/li&gt;
&lt;li&gt;one reading probably caught the previous load still freeing memory;&lt;/li&gt;
&lt;li&gt;a decode rate reads a million tokens a second, because each check generates a single token.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-13-e5-fill-card.md&quot;&gt;E5: the card, the rulings and the defects&lt;/a&gt;
· &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/tree/main/experiments/e5&quot;&gt;the trimmed records&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;two-nvidia-tools-read-against-our-own&quot;&gt;Two NVIDIA tools, read against our own&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NeMo Relay&lt;/strong&gt; records what an AI agent actually did: each model call, tool call and decision, in open trace
formats. It can wrap Claude Code without changes to it. We’ve added it to the toolset as a planned instrument: if
a trial confirms the transparent mode, it could trace our agent experiments.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/agent-tracing-nemo-relay/&quot;&gt;How do we see what an agent actually did?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PAIR&lt;/strong&gt; joins the computers on a home network into one model service and sends each request to a machine that
holds the model. Its own documentation lists what its scheduler can’t see: whether a model is already loaded, how
much context fits, and how jobs differ in size. interlock measures exactly those things on one machine, and E5 is
one of them.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/pair-and-interlock/&quot;&gt;Where should this request run? NVIDIA PAIR and interlock&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;new-on-the-bench&quot;&gt;New on the bench&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rnd probe &amp;lt;url&amp;gt;&lt;/code&gt;&lt;/strong&gt; answers the first questions about a tool or model from each source’s own records: its
licence, latest release, gating, and any code mentioning telemetry. A source it can’t reach is reported as unknown,
never guessed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rnd search --exact&lt;/code&gt;&lt;/strong&gt; finds whole words in the same case, so a search for PAIR finds the product, not every pair.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rnd watch&lt;/code&gt;&lt;/strong&gt; checks a list of sources for anything new since the last look. The list covers NVIDIA’s blog and
models, the CUDA toolkit, and the releases of Ollama, OpenVINO, PyTorch, ComfyUI, llama.cpp and Claude Code. Once
a week a scout reads what’s new and writes a short ranked report: what to try, what to file and what to skip.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd#probing-a-tool-before-a-trial&quot;&gt;rnd’s README: probing a tool, watching sources&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;new-the-weekly-watch&quot;&gt;New: the weekly watch&lt;/h2&gt;
&lt;p&gt;The watch now has its own page every Monday: what changed in the tools and models the studio runs on, what it
means for our work, and what we did about it. The first edition caught a Claude Code fix that closes a gap in our
own experiment’s sandbox.&lt;/p&gt;
&lt;p&gt;→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/watch/2026-10-13/&quot;&gt;The watch: week of 13 October&lt;/a&gt;&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 4: Three devices, no contention</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-04/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-04/</guid><description>E4b answers E4&apos;s question. The 5090, the NPU and the iGPU keep their speed when they run together. Getting there took a pilot, a run of live-only defects and one re-run.</description><pubDate>Mon, 12 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Every claim below links to the record it came from. Interim results are labelled as interim. The device router
switchyard is now &lt;strong&gt;interlock&lt;/strong&gt;, renamed before its first release because the old name is taken.&lt;/p&gt;
&lt;h2 id=&quot;e4bs-result&quot;&gt;E4b’s result&lt;/h2&gt;
&lt;p&gt;E4b asks whether each device keeps its speed when another runs beside it. A pair counts as “free” only when the 90%
interval of its slowdown sits inside ±10%. That rule was fixed in advance.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;margin:12px 0&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Measured device&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Beside&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Change&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;90% interval&lt;/th&gt;
&lt;th style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top;text-align:left;background:#f3f4f6;color:#1f2430&quot;&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;5090 (decode rate)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;NPU&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;−0.8%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;−1.2% to −0.5%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;5090 (decode rate)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;iGPU&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;+0.6%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;+0.2% to +1.4%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;NPU (latency)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;5090&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;−0.6%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;−1.3% to +0.1%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;iGPU (latency)&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;5090&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;+2.5%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;−1.0% to +4.8%&lt;/td&gt;
&lt;td style=&quot;border:1px solid #c9ced6;padding:6px 10px;vertical-align:top&quot;&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So the router can treat the three devices as independent lanes, and E4’s question is answered.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/e4b-verdict.png&quot; alt=&quot;E4b verdicts: each pair’s interval against the ±10% band&quot; width=&quot;600&quot; height=&quot;236&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/e4b-triplet-effects.png&quot; alt=&quot;E4b: each triplet’s change, pair by pair&quot; width=&quot;600&quot; height=&quot;250&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The NPU and iGPU rows took a second run. In the first full block those two pairs came out &lt;strong&gt;inconclusive&lt;/strong&gt;, and not
because of noise. The Intel calls take about 40 ms, and the harness ran them within a tenth of a second of starting the
5090. So they overlapped the 5090’s start, not its steady decode. The clock check refused them. The harness now waits
for one steady 5090 warm-up, and the re-run of just those two pairs had no refusals at all.&lt;/p&gt;
&lt;p&gt;One detail for routing: the iGPU’s latency beside a 5090 decode is sometimes about 6% higher, which is inside the band
but worth knowing for latency-sensitive iGPU work. That chart was drawn after the verdict, and it decides nothing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/e4b-igpu-latency.png&quot; alt=&quot;E4b: iGPU call times beside a busy 5090 (post hoc)&quot; width=&quot;600&quot; height=&quot;207&quot; /&gt;&lt;/p&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/tree/main/experiments/e4b&quot;&gt;The E4b data and charts&lt;/a&gt;
→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-12-e4b-block-2.md&quot;&gt;E4b block 2&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;how-the-harness-got-there&quot;&gt;How the harness got there&lt;/h2&gt;
&lt;p&gt;E4b is the redesign of the co-scheduling experiment that &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-12-e4-co-scheduling-inconclusive.md&quot;&gt;came out inconclusive&lt;/a&gt;.
Its harness merged after two rounds of review.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It runs its own Ollama server.&lt;/strong&gt; The server inherits the system environment with every Ollama setting stripped, then
gets three pinned values.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Before it measures anything,&lt;/strong&gt; the run refuses unless the server’s new log shows the CUDA 13 backend, the model sits
wholly in GPU memory, and the model’s digest matches the spec.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Before the server starts,&lt;/strong&gt; the VRAM watchdog must be alive and the card must be empty.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A pilot of 20 calls on the 5090 alone set the clock tolerance from data, at 22.5 MHz (three of the card’s clock steps).
All 20 ran at full length, at 218–224 tokens/s.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mcp-tool-shop-org.github.io/rnd/charts/e4b-pilot-clock.png&quot; alt=&quot;E4b pilot: how far the 5090 clock moved during each call&quot; width=&quot;600&quot; height=&quot;270&quot; /&gt;
→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/instruments/switchyard.md&quot;&gt;The switchyard review record&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;live-only-defects&quot;&gt;Live-only defects&lt;/h2&gt;
&lt;p&gt;The first pilot attempts stopped before any device work, with the card idle.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The listener looked closed.&lt;/strong&gt; After an overnight update, Windows build 26340 reports Ollama’s listening socket
as “Closed”, so every lookup by listening state found nothing. → &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-12-windows-26340-listener-state-closed.md&quot;&gt;The rig fact&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The command runner threw its output away,&lt;/strong&gt; so the server pin read nothing. Worse, the git check read “no
output” as “clean tree”. It now fails closed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The pilot counted its prompt before its own server existed.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A read of the pilot path then turned up more of the same, all fixed before the fourth try:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the start didn’t wait for the server to come up;&lt;/li&gt;
&lt;li&gt;GPU clock sampling wasn’t connected;&lt;/li&gt;
&lt;li&gt;there were no request timeouts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fourth try showed that E4’s prompt, one sentence repeated, makes the model loop at long lengths, so E4b now counts
“1, 2, 3, …”. The first full block then lost its record to a read-only NPU cache file at cleanup. The harness now writes
the record first, refuses run names already in use, and sets aside any run that stops.&lt;/p&gt;
&lt;h2 id=&quot;fakes-that-told-the-truth-continued&quot;&gt;Fakes that told the truth, continued&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-03/&quot;&gt;Issue 3&lt;/a&gt; described four failures caused by test fakes. The count is now seven. The pattern is the same
each time: a test swaps in a stand-in for a real call, and the real version is never exercised. The remedies are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;recorded real exchanges;&lt;/li&gt;
&lt;li&gt;one test that builds the live setup with nothing swapped in and fails if anything is still a stand-in;&lt;/li&gt;
&lt;li&gt;reads that refuse a missing value instead of passing it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-12-test-fakes-hide-the-platform.md&quot;&gt;Test fakes that hide the platform&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-puzzle-generator-and-batch-6&quot;&gt;The puzzle generator and batch 6&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dated spans.&lt;/strong&gt; The solver now works out which edition was in force on a given day. The writer can’t state it,
which closes the shortcut that let a placeholder date pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The generator’s first slice&lt;/strong&gt; builds T10 puzzles, then proves that a reader who misreads “exactly” or “at least”
gets a different answer. T10 uses only “which could be…” and true/false questions, because a single-answer
question can’t show that misreading.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch 6’s last fixes&lt;/strong&gt; were wording and options only, and it is clear for the Stage 1 freeze.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/instruments/role-set.md&quot;&gt;The role-set review record&lt;/a&gt;&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 3: A second opinion on the first live runs</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-03/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-03/</guid><description>switchyard&apos;s first live runs on the 5090, NPU and iGPU, what went wrong, and what an outside model&apos;s review changed.</description><pubDate>Mon, 12 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Every claim below links to the record it came from. Interim results are labelled as interim.&lt;/p&gt;
&lt;h2 id=&quot;the-first-live-runs&quot;&gt;The first live runs&lt;/h2&gt;
&lt;p&gt;switchyard is the studio’s device router: it decides which model runs on the 5090, the Intel NPU or the iGPU, from
measurements rather than guesses. On 2026-10-11 and 12 its harness made its first live runs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;E4&lt;/strong&gt; asked whether the Intel devices can work beside the 5090 without slowing either side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E5&lt;/strong&gt; asked how large a context window each model can hold on the card.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Getting there took a series of fixes, each logged with its evidence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a token count that Ollama read as “no limit”;&lt;/li&gt;
&lt;li&gt;a prompt search that made thousands of calls;&lt;/li&gt;
&lt;li&gt;two missing input shapes;&lt;/li&gt;
&lt;li&gt;a crash on the NLI model’s output.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/instruments/switchyard.md&quot;&gt;The live-run records&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;E4’s result is inconclusive.&lt;/strong&gt; The run was valid. Nothing measured slower when paired, and the iGPU even ran
about 15% faster. But every “alone” baseline varied more than the pre-registered noise rule allows, so no pair can be
called. We traced the noise to three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the GPU clocks hadn’t settled during short calls;&lt;/li&gt;
&lt;li&gt;the NPU woke slowly after a pause;&lt;/li&gt;
&lt;li&gt;a control request was silently reloading the model.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;E4 isn’t re-run as it stands.
→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-12-e4-co-scheduling-inconclusive.md&quot;&gt;E4: co-scheduling inconclusive&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-outside-review&quot;&gt;The outside review&lt;/h2&gt;
&lt;p&gt;The Director put the night’s findings to Kimi K3, a model from outside the families that built the harness, which
read both experiment specs. R&amp;amp;D ruled on each of its six points.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;E4’s noise rule (accepted, with one change).&lt;/strong&gt; The old rule pooled every “alone” measurement, so it counted the
very drift that alternating alone, paired, alone is meant to cancel. The redesign, E4b:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;scores each alone–paired–alone triplet as its own log-ratio;&lt;/li&gt;
&lt;li&gt;calls a pair free only when the 90% confidence interval of those ratios sits inside ±10% (an equivalence test);&lt;/li&gt;
&lt;li&gt;throws out any single call that reloaded the model or whose GPU clock jumped.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;R&amp;amp;D’s change: an interval wholly outside the band counts as “not free” on either side, faster included, as the
original spec said, because either way the devices are affecting each other.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The spill signal (accepted in part).&lt;/strong&gt; A whole-adapter memory counter replaces the per-process counter, whose
process lookup broke when the server ran as administrator. R&amp;amp;D rejected the proposed “two of three signals”
rule: it would have loosened a rule we fixed in advance, after seeing the data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A live test tier (accepted, changed).&lt;/strong&gt; The same recorded cases also run live against a tiny model on a CPU
Ollama the harness starts for itself, before each live run and on every Ollama update. It doesn’t run nightly, and
it never touches the CPU instance another tool depends on.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Design rules (accepted).&lt;/strong&gt; The harness owns the server it measures: it starts it, or reads a declared record of
how it was started. Probes only read; they never change the state they read. And state is read at the moment
of measurement, never pieced together afterwards.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The iGPU speed-up (accepted, with a correction).&lt;/strong&gt; The suggested first check, CPU frequency from the existing
receipts, can’t work: Windows reported the same nominal 3,700 MHz on every call. A real frequency counter and a
CPU-only control run come first.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clock settling (accepted).&lt;/strong&gt; Use longer decodes, wait until the GPU clock holds steady before timing, and fix the
clock reading that came back empty on every call.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/instruments/switchyard.md&quot;&gt;R&amp;amp;D’s rulings on the review&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;fakes-that-told-the-truth&quot;&gt;Fakes that told the truth&lt;/h2&gt;
&lt;p&gt;Four of the night’s failures had one cause: a test fake accepted something the real server treats differently.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A completion limit of 0 meant “nothing” to the fake and “no limit” to Ollama.&lt;/li&gt;
&lt;li&gt;A search’s cost was invisible to fakes, which take no time.&lt;/li&gt;
&lt;li&gt;A fake model returned rows the real one never does.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The remedy is to record real exchanges once, under a grant, and allow fakes to replay only those. The first
recording already overturned two assumptions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a very small context size is raised to a minimum, so it limits nothing;&lt;/li&gt;
&lt;li&gt;a request we treated as harmless loads the model.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/rnd/blob/main/entries/2026/2026-10-12-ollama-num-predict-zero-is-no-limit.md&quot;&gt;Ollama reads a zero limit as no limit&lt;/a&gt;&lt;/p&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 2: Puzzles with one answer, and a pod that never got ready</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-02/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-02/</guid><description>Two days of building the verifier&apos;s training data, and what a hung GPU pod changed in offrig.</description><pubDate>Sun, 11 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;p&gt;Every claim below links to a brief or to the record it came from. Interim results are labelled as interim.&lt;/p&gt;
&lt;h2 id=&quot;the-verifiers-training-data&quot;&gt;The verifier’s training data&lt;/h2&gt;
&lt;p&gt;ASPIRE is teaching Qwen3-8B to be a verifier before any domain training. Generated items needed a reader: of
49 candidates from a 235B writer, 15 passed as written. Hand-built batches did too: of 120, 91 passed. A
program now solves every logic puzzle and refuses any that has two answers, a spare clue, or a clue that gives
the answer away. Passing it isn’t enough, though. Puzzles bent to fit the solver, and the same puzzle under new
names, were found by reading, then by a script. Each defect now gets a class name, so the checks can move from
people into tools.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/verifier-training-data/&quot;&gt;How is the verifier’s training data checked?&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-pod-that-never-got-ready&quot;&gt;A pod that never got ready&lt;/h2&gt;
&lt;p&gt;On 2026-10-11 a rented pod’s engine logged that startup had finished, then deadlocked. Its health check stayed
down while its model list answered, so the old readiness check passed. The run wrote nothing and cost $2.80.
offrig 1.1.0 now calls a recipe ready only when one completion returns an answer. It reports a hang after
3 minutes and leaves the pod up for a person to decide. That pod sat on a CUDA 13.2 host where the working runs
had used 13.0. That isn’t shown to be the cause, but a profile can now pin the exact host CUDA versions.
These paths are tested against fakes and haven’t been re-run on a rented pod.
→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/offrig&quot;&gt;offrig on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;cuda-by-the-version-the-software-needs&quot;&gt;CUDA, by the version the software needs&lt;/h2&gt;
&lt;p&gt;A profile can also say which CUDA its software was built for, such as 13.4 for a PyTorch nightly. Its rented host
must then be at least the oldest of that major version (13.0). That follows NVIDIA’s minor-version compatibility,
and it matches what our CUDA 13.4 runs measured on RunPod’s 13.0 hosts.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/cuda-13-4/&quot;&gt;Can the studio train on CUDA 13.4?&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;also&quot;&gt;Also&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phi-3.5-mini’s first three CPU calls ran at about 7 tokens a second, then 21.&lt;/strong&gt; Neither a cold read from USB
or NVMe nor any CPU scheduling setting reproduced it. It’s recorded as open, and the next run samples the
machine once a second through every call.
→ &lt;a href=&quot;https://github.com/mcp-tool-shop-org/switchyard&quot;&gt;switchyard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A seeding bug in the critic heads.&lt;/strong&gt; Dropout drew from an unseeded generator, so a head depended on the
heads trained before it. The step-4 conclusions stand, because they rest on confidence intervals. The fix seeds
the whole of training.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item><item><title>Issue 1: A verifier, a cliff, a dead NPU and a missing input</title><link>https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-01/</link><guid isPermaLink="true">https://mcp-tool-shop-org.github.io/rnd/newsletter/issue-01/</guid><description>Two days of measurements, from a provisional verifier calibration to why the Intel NPU disagreed with itself.</description><pubDate>Fri, 09 Oct 2026 23:59:59 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠ Provisional (2026-10-10):&lt;/strong&gt; these verifier numbers were measured on synthetic gold, claims and labels
written by Claude, not checked against human labels. They are not results until re-measured on a public
human-labelled set. Further runs on this gold are stopped.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every claim below links to a research brief, and every brief links to its receipts. Interim and wrong results
stay visible and labelled.&lt;/p&gt;
&lt;h2 id=&quot;the-verifier&quot;&gt;The verifier&lt;/h2&gt;
&lt;p&gt;offrig’s &lt;code&gt;verify&lt;/code&gt; step needed a default local model to check claims about code. Six models ran against a gold
set built for the job, with every label assigned blind. None passed the first run. gemma4:31b was fooled almost
never, but ran out of reply budget while still thinking.&lt;/p&gt;
&lt;p&gt;A rerun with a larger budget and offrig’s improved quote rule passed reasoning claims on tune, then on the
held-out split. On this gold, gemma4:31b was the pick for reasoning claims; &lt;strong&gt;no default is named&lt;/strong&gt; until the
re-measure on public human-labelled data.
Grounded claims failed on one deterministic thinking loop: gemma repeated two lines 307 times because the
contract allowed only one quote. offrig’s next contract allows several quotes and kills loops.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/verifier-calibration/&quot;&gt;Which local model can check claims for offrig?&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;thinking-on-and-off&quot;&gt;Thinking on and off&lt;/h2&gt;
&lt;p&gt;The same tune claims, run again with thinking off, separate thinking from the model itself. Both qwen3 models were
fooled far more often with thinking off. On reasoning claims, the false-accept bound was 0.39 for qwen3:8b and
0.40 for qwen3:14b, against a limit of 0.10. gemma4:31b lost much less. It ran 3.3× faster and, on grounded
claims, passed the rule with thinking off: the first grounded pass of any model. We went looking for that after
seeing the data, so it names nothing until a held-out run confirms it. Model size and thinking interact, and
that is the case for a cascade: run a strong model cheaply first, and think only where it’s needed.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/making-thinking-cheaper/&quot;&gt;How do we make model thinking cheaper?&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-math-ladder&quot;&gt;The math ladder&lt;/h2&gt;
&lt;p&gt;Generated arithmetic claims, from one step to many, show where each model stops checking reliably. The thinking
models held perfectly through level 5. Then every model hit the same cliff: answers in megabytes, false claims
off by exactly one.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/math-ladder/&quot;&gt;Where does each model’s arithmetic checking fall off?&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-intel-npu&quot;&gt;The Intel NPU&lt;/h2&gt;
&lt;p&gt;The NPU runs small embedders 3–10× faster than the CPU. Batching nomic at 8×1024 crashed the device twice. When
the NPU disagreed with the CPU, Kimi traced it to one input the adapter never fed (&lt;code&gt;token_type_ids&lt;/code&gt;). OpenVINO
read leftover memory in its place. With that fixed, parity is exact everywhere. A last diagnostic try will show
whether the crash was the same bug.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/intel-npu/&quot;&gt;What can the Intel NPU carry?&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;cuda-134&quot;&gt;CUDA 13.4&lt;/h2&gt;
&lt;p&gt;It runs on RunPod’s CUDA 13.0 hosts through NVIDIA’s compatibility package, on an A100 and a B200, and the judge
fine-tune passes its band on it. The speed-up came from the &lt;code&gt;causal-conv1d&lt;/code&gt; kernel; plain 13.4 was slightly
slower than 12.8.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/cuda-13-4/&quot;&gt;Can the studio train on CUDA 13.4?&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;also-measured&quot;&gt;Also measured&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sung hymns land within 10 ms of the beat&lt;/strong&gt; at the median. The aligner that cross-checks them is not yet
validated. → &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/sung-vocal-timing-and-pitch/&quot;&gt;the brief&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Kimi-K3 piano arrangement costs $0.30–$1.01&lt;/strong&gt;, against offrig’s $3.00 worst-case budget.
→ &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/arrangement-costs/&quot;&gt;the brief&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local translation broke a pinned sentence in 5 of 7 languages&lt;/strong&gt; on one README, so translations are now
checked. → &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/local-translation-quality/&quot;&gt;the brief&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ScalarScope’s step-by-step comparisons fire on seed noise,&lt;/strong&gt; and its drift view measures the scoring teacher,
not the run. → &lt;a href=&quot;https://mcp-tool-shop-org.github.io/rnd/briefs/reading-training-geometry/&quot;&gt;the brief&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>mcp-tool-shop</dc:creator></item></channel></rss>