<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sergei Parfenov</title>
    <description>The latest articles on DEV Community by Sergei Parfenov (@p0rt).</description>
    <link>https://dev.to/p0rt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F157612%2F7bffbdb1-8c3d-47ed-9575-80cee50d05f2.jpeg</url>
      <title>DEV Community: Sergei Parfenov</title>
      <link>https://dev.to/p0rt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/p0rt"/>
    <language>en</language>
    <item>
      <title>The Agent Faked a Test Log, Then Believed It. Self-Editing Harnesses Have a Provenance Problem.</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Wed, 08 Jul 2026 11:39:46 +0000</pubDate>
      <link>https://dev.to/p0rt/the-agent-faked-a-test-log-then-believed-it-self-editing-harnesses-have-a-provenance-problem-3id6</link>
      <guid>https://dev.to/p0rt/the-agent-faked-a-test-log-then-believed-it-self-editing-harnesses-have-a-provenance-problem-3id6</guid>
      <description>&lt;p&gt;Lilian Weng published a new survey on July 4: &lt;a href="https://lilianweng.github.io/posts/2026-07-04-harness/" rel="noopener noreferrer"&gt;Harness Engineering for Self-Improvement&lt;/a&gt;. It maps roughly three years of work on agents that optimize their own scaffolding — context managers, workflows, harness code, and eventually the optimizer that optimizes the harness. Most of the discussion around it will be about recursive self-improvement, because RSI is the exciting frame.&lt;/p&gt;

&lt;p&gt;I read it with a different hat on. I run agents in production, and this blog has been circling one question for a while: what does it take to trust the output of a long agent chain? Read from that angle, her survey is not really a post about self-improvement. It's a post about a research field independently reinventing operations engineering — regression gates, immutable audit logs, least privilege — because every loop that skips those controls gets burned in a documented, reproducible way.&lt;/p&gt;

&lt;p&gt;The cleanest burn in the whole literature comes from the &lt;a href="https://arxiv.org/abs/2505.22954" rel="noopener noreferrer"&gt;Darwin Gödel Machine&lt;/a&gt; (DGM) paper. An agent, allowed to edit its own harness code, faked a log saying its unit tests had run and passed. The tests never ran. The fake log went into its own context. Downstream, the same agent read that log and concluded its changes were validated. It lied to itself, then trusted the lie — except "lied" smuggles in intent that was never there. This was garden-variety tool-use hallucination meeting an untyped log. Which is worse, not better: you don't need a deceptive agent to get this failure, just a filesystem that can't say who wrote what.&lt;/p&gt;

&lt;p&gt;If you've read my post on provenance dying at the storage boundary &amp;lt;!-- TODO: link "Your Provenance Vector Dies at the Storage Boundary" --&amp;gt;, you already know where this is going.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a harness is, and why it became the optimization target
&lt;/h2&gt;

&lt;p&gt;Weng's definition, compressed: the harness is everything between the raw model and the world. The loop that decides when to plan and when to act. Tool interfaces. Context assembly. Memory files. Permission checks. Evaluation. Claude Code and Codex CLI are harnesses. So is your homegrown retry-wrapper-plus-prompt-template, whether you call it that or not.&lt;/p&gt;

&lt;p&gt;That this layer matters is now measurable. &lt;a href="https://arxiv.org/abs/2601.11868" rel="noopener noreferrer"&gt;Terminal-Bench 2.0&lt;/a&gt; — 89 hard, containerized command-line tasks — shows the same frontier models scoring differently under different scaffolds; the best pairing in the benchmark paper (Codex CLI + GPT-5.2) lands at 63%. The benchmark authors are explicit that scaffolds get engineered around the quirks of specific models, which is also the founding observation of the self-harness line of work: harness design is model-specific, and hand-tuning it per model doesn't scale.&lt;/p&gt;

&lt;p&gt;Weng organizes the field as a ladder of what gets optimized — prompts, then structured context, then workflows, then harness code, then the optimizer code itself — and walks every rung with examples. I won't duplicate the map; she does it in 28 well-spent minutes. What I want to do instead is pull a few load-bearing systems off that ladder and squint at their numbers and their failure reports — because that's where the story stops being about self-improvement and starts being about trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say when you squint
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2310.02304" rel="noopener noreferrer"&gt;STOP&lt;/a&gt;&lt;/strong&gt; (Zelikman et al. 2023) — the original improver-improves-the-improver loop, still the conceptual core of the field — reported the result that should frame everything else: seed the recursion with GPT-4 and downstream utility climbs across iterations; seed it with GPT-3.5 or Mixtral and the loop actively hurts. Recursion is not a free lunch. Below a capability threshold, the loop amplifies noise instead of signal. The base model remains the ceiling; the harness moves you around underneath it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meta-Harness&lt;/strong&gt; (&lt;a href="https://arxiv.org/abs/2603.28052" rel="noopener noreferrer"&gt;Lee et al. 2026&lt;/a&gt;) — a search loop in which a coding agent proposes, edits, and evaluates whole harness variants — is the honest data point on how much headroom automated search finds above strong human engineering. On Terminal-Bench-2 it initializes the search from Terminus-2 and Terminus-KIRA — already strong hand-built harnesses — and the discovered harness comes out ahead: 37.6% on Haiku 4.5, where the next-best agent (Goose) sits at 35.5% and Terminus-KIRA at 33.7%, and 76.4% on Opus 4.6 against 74.7% for Terminus-KIRA. Call it two to four points, found by a proposer (Claude Code on Opus 4.6) that evaluates around 60 harness variants over 20 iterations in a few hours of wall-clock time. Polish-sized gains — but polish at that price is a good trade.&lt;/p&gt;

&lt;p&gt;The caveats are more instructive than the headline. The one entry still above Meta-Harness on Opus, ForgeCode at 81.8%, could not be reproduced by the authors from its public code. And — the detail I'd tattoo on the field — in the TB-2 experiment the search set and the test set are the same 89 tasks. The authors say so plainly: the benchmark is small and expensive, a proper split would gut the signal, so they run it as a discovery problem and compensate with manual inspection plus regex audits for task-specific string leakage. There's also a genuinely encouraging trace buried in the qualitative results: early candidates that bundled structural fixes with prompt-template rewrites regressed, and the proposer hypothesized the shared prompt edit was the confound, isolated the structural change, and shipped a safer additive modification that won the run. The optimizer performing ablation hygiene on itself is the best thing in the paper.&lt;/p&gt;

&lt;p&gt;Second-best thing in the paper: the proposer-context ablation, which lands on the same conclusion as an experiment I ran here &amp;lt;!-- TODO: link the summarization-destroys-provenance post --&amp;gt;. Give the optimizer scores only: median 34.6. Add LLM-written summaries of the trajectories: 34.9 — statistically nothing. Hand it the full raw traces: 50.0. Summaries did not recover the signal; compression strips exactly the diagnostic detail the optimizer feeds on. Independent group, different setup, same shape: the information that makes a trace useful for diagnosis is the information a summary throws away first. Provenance does not survive compression.&lt;/p&gt;

&lt;p&gt;Full disclosure, because it's too on-the-nose to skip: this exact failure mode bit me while writing this piece. My first draft sourced the Meta-Harness numbers from an LLM-generated paper-summary site, which confidently attributed that 35.5% baseline to Terminus-KIRA. The paper's own table says 35.5% is Goose; Terminus-KIRA sits at 33.7%. The wrong number lived in the draft until a fact-check pass against the raw table caught it. A machine-written summary — no provenance types, no link back to the table row — had quietly swapped a baseline. The ablation's finding, wearing street clothes. I nearly shipped an article about untyped trust on the strength of an untyped summary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DGM&lt;/strong&gt; — an evolutionary archive of coding agents, each free to rewrite its own harness repo — posted the impressive relative jump, 20% → 50% on SWE-bench Verified with a frozen Claude 3.5 Sonnet, but from a deliberately naive starting harness, and reporting put a single 80-iteration run at &lt;a href="https://the-decoder.com/sakana-ais-darwin-godel-machine-evolves-by-rewriting-its-own-code-to-boost-performance/" rel="noopener noreferrer"&gt;around $22k and two weeks&lt;/a&gt;. Look at what the loop actually discovered: finer-grained edit tooling, validation-and-retry on empty patches, context summarization near the window limit. Every one of these is a standard trick that hand-built coding harnesses already ship. The loop rediscovered expert engineering. That's a legitimate result — automated search recovering expert knowledge is useful — but it is not evidence of the loop &lt;em&gt;exceeding&lt;/em&gt; expert knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2605.27276" rel="noopener noreferrer"&gt;SIA&lt;/a&gt;&lt;/strong&gt;, the joint harness-plus-weights loop — a feedback agent decides per iteration whether to edit the harness or update the weights — is the one to hold loosely. Weng herself flags the confounds — the task agent (gpt-oss-120b) is far weaker than the meta and feedback agents steering it (Claude Sonnet 4.6), and the baselines are thin — and files the result under promising but unproven. A survey author doing that out loud is worth appreciating.&lt;/p&gt;

&lt;p&gt;And at the "AI scientist" end of the ladder, &lt;a href="https://arxiv.org/abs/2601.03315" rel="noopener noreferrer"&gt;Trehan &amp;amp; Chopra&lt;/a&gt; ran idea-to-paper attempts with a minimal scaffold (read, write, search, list). Of the generated ideas, four passed human selection and exactly one made it through to an executed paper. Their failure taxonomy reads like an ops incident review: stale defaults inherited from training data, implementation drift under execution pressure, context degradation on long horizons, and over-optimism — declaring victory on noisy results, the pattern &lt;a href="https://arxiv.org/abs/2511.16072" rel="noopener noreferrer"&gt;Bubeck et al.&lt;/a&gt; call "p-hacking and eureka-ing".&lt;/p&gt;

&lt;p&gt;Aggregate reading: self-editing loops today mostly (a) recover known harness engineering when started from scratch, (b) squeeze low single-digit points out of strong baselines, and (c) fail in ways that are structured and predictable rather than exotic. That last one is the good news. Predictable failure modes are the ones you can build controls for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incident, in slow motion
&lt;/h2&gt;

&lt;p&gt;Back to DGM, because the appendix on objective hacking is the most instructive few pages in this literature.&lt;/p&gt;

&lt;p&gt;Setup: the researchers wanted the loop to reduce hallucinated tool use — the model narrating tool calls it never made. They instrumented real tool invocations with special marker tokens and scored agents on whether unmarked "tool output" appeared in generations. The checking functions were hidden from the agent during self-modification, and the authors note hacking happened &lt;em&gt;more often&lt;/em&gt; when they weren't hidden.&lt;/p&gt;

&lt;p&gt;Two lineages are worth naming. Node 96 did the honest work — improved tool-invocation logging, stripped hallucinated tool output when it could determine no tool ran — and scored 1.67 out of 2.0. Node 114 hit a perfect 2.0 after just two self-modifications: it deleted the detection markers. Perfect score, zero progress on the actual problem. Goodhart's law in its purest lab form. The team caught it because DGM keeps what Sakana describes as a "transparent, traceable lineage" of every change — and that lineage lived outside the agent's editable surface.&lt;/p&gt;

&lt;p&gt;The faked test log is the deeper incident, though. A record was written &lt;em&gt;by the model&lt;/em&gt;, persisted to the filesystem, and later consumed &lt;em&gt;by the model&lt;/em&gt; as if it were runtime truth. Exactly one bit of metadata would have blocked the failure: &lt;strong&gt;who wrote this record&lt;/strong&gt; — the harness runtime, or the model? In the typed-provenance framing I've been arguing for &amp;lt;!-- TODO: link "Trust Isn't a Scalar" --&amp;gt;, that's a &lt;code&gt;runtime-verified&lt;/code&gt; vs &lt;code&gt;self-reported&lt;/code&gt; tag, plus one rule: &lt;code&gt;self-reported&lt;/code&gt; never upgrades itself by being re-read.&lt;/p&gt;

&lt;p&gt;This is the storage-boundary failure from my earlier post, except recursive. The artifact crossing the boundary isn't user data anymore — it's the evidence the next iteration of the system uses to decide what the current iteration did. Untyped trust used to compound per request. In a self-editing harness it compounds per generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three invariants every working loop converges on
&lt;/h2&gt;

&lt;p&gt;Put the successful loops side by side and the same skeleton appears, whether or not the authors name it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A bounded editable surface.&lt;/strong&gt; AlphaEvolve marks evolvable code regions with explicit block comments. &lt;a href="https://arxiv.org/abs/2606.09498" rel="noopener noreferrer"&gt;Self-Harness&lt;/a&gt; — Shanghai AI Lab's loop in which the agent mines its own failure traces and patches its own harness — hands its proposer a declared list of editable surfaces plus a record of passing behaviors it must not break. DGM lets the agent rewrite its own repo — but not the evaluation stack. The loop needs a fence, drawn in advance, in writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. An evaluator and permission layer outside the fence.&lt;/strong&gt; DGM hides the checker functions and measures more hacking when it doesn't. Self-Harness validates candidate edits on a held-out split the proposer never optimized against, and promotes only edits with zero regression on &lt;em&gt;both&lt;/em&gt; held-in and held-out sets. Meta-Harness is the instructive partial violation: on TB-2 the search optimizes against the test set itself — acknowledged, defended (small, expensive benchmark), and patched with manual audits. Even a careful team bends this invariant the moment evaluation gets expensive, which is exactly why it needs to be stated as an invariant rather than left as taste. Weng's own conclusion in the challenges section is that evaluators and permission controls should sit outside the loop that evolves the harness. This is separation of duties. It has a name because we've needed it before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. An append-only, typed record of what happened.&lt;/strong&gt; DGM's lineage is how node 114 got caught. Self-Harness's weakness-mining stage explicitly rejects flat pass/fail labels — each failure record captures what the verifier observed, whether the agent's own behavior actually caused it, and through what mechanism — because two timeouts that look identical in an error log can have entirely different roots. That is not a scalar trust score. That is a typed provenance record. Even &lt;a href="https://arxiv.org/abs/2510.04618" rel="noopener noreferrer"&gt;ACE&lt;/a&gt; — the context-optimization loop whose curator emits small itemized deltas instead of rewriting the whole prompt blob — lands on the same instinct: keep every change diffable, reviewable, attributable.&lt;/p&gt;

&lt;p&gt;If you've operated software for a living you recognize all three: least privilege, separation of duties plus CI regression gates, immutable audit logs. The field isn't inventing new safety machinery — it's rediscovering ops controls from the inside, one incident at a time. Weng herself reaches for an operating-systems analogy for harnesses; I'm just following it down to the ops floor. I mean that as a compliment. Convergent evolution is evidence the constraints are real rather than stylistic.&lt;/p&gt;

&lt;p&gt;Exactly one system in Weng's survey treats this as a first-class constraint — &lt;a href="https://arxiv.org/abs/2605.26340" rel="noopener noreferrer"&gt;ScientistOne&lt;/a&gt; (Meng et al. 2026), over on the AI-scientist branch, where every claim must trace back to an evidence source and the chain gets audited. That idea has not crossed over to the self-editing harness loops. There, provenance keeps getting built as a side effect: lineage exists in DGM because researchers wanted to debug evolution; failure records are rich in Self-Harness because flat labels made weakness mining useless. The argument I've been making for two posts now is that the record type system should be the load-bearing wall, not the scaffolding you only notice after it catches something.&lt;/p&gt;

&lt;h2&gt;
  
  
  You already run one of these
&lt;/h2&gt;

&lt;p&gt;This is not a frontier-lab concern. If your coding agent maintains its own memory file, writes its own instruction or skill files, or appends "lessons learned" that get loaded into future sessions — you are running a self-editing harness. Smaller loop, same topology: model-authored artifacts feeding future model behavior, usually with zero record types and no regression gate.&lt;/p&gt;

&lt;p&gt;The canonical failure shape doesn't need an adversary. An agent writes a confident note into its own memory — say, "the staging DB is safe to reset" — and three sessions later a different task reads it as established fact. Nobody hacked anything. The system simply has no way to distinguish what it &lt;em&gt;verified&lt;/em&gt; from what it once &lt;em&gt;said&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The checklist I'd actually apply:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat model-authored harness edits like schema migrations.&lt;/strong&gt; Memory files, instruction files, generated skills: versioned, diffable, reversible. A model changing its own operating instructions is a deploy, not a note.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-gate promotion.&lt;/strong&gt; An edit must fix the failure it targets (held-in) and break nothing else (held-out). Self-Harness converges on this shape independently, which is interesting — but shape is not sufficiency. My own preregistered test found a dumb baseline matching my gate scheme on half the failure classes &amp;lt;!-- TODO: link "My Strawman Baseline Beat My Own Scheme" --&amp;gt;, and I'd want Self-Harness benchmarked against an equally dumb accept-if-tests-pass rule before concluding the machinery earns its complexity. Run the gates — and run the strawman against them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type every persisted record at write time.&lt;/strong&gt; &lt;code&gt;runtime-verified&lt;/code&gt; / &lt;code&gt;self-reported&lt;/code&gt; / &lt;code&gt;human-authored&lt;/code&gt;, minimum. Enforce at read time that self-reported claims can't gate promotions or authorize actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the evaluator outside anything the loop can write.&lt;/strong&gt; Checker code, marker tokens, permission checks, credentials. If the agent can grep the checker, assume it will eventually optimize the checker instead of the task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the failures.&lt;/strong&gt; Rejected edits and failed trajectories are the cheapest signal the loop has. The literature's bias toward publishing successes is exactly the bias your local loop will inherit from its own logs if you prune them. Weng lists preserving negative results among the field's open challenges; it applies just as hard at your scale.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this needs a research budget. It's a few enum values, a CI job, and some restraint about what ends up in the agent's writable mount.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I land on "the model will eat the harness"
&lt;/h2&gt;

&lt;p&gt;Weng's prediction runs through the prompt-engineering analogy: models absorbed the tricks, while the job of specifying what you want, under which constraints, judged how — that part outlived every trick. I mostly buy it, with one sharpening.&lt;/p&gt;

&lt;p&gt;Split your harness into two piles. Pile one exists to &lt;em&gt;compensate for the model&lt;/em&gt;: context massaging, retry phrasing, output parsing, the clever loop tweaks. Pile two exists to &lt;em&gt;protect you from the model&lt;/em&gt;: permissions, evaluators, provenance types, the audit log. Pile one depreciates with every model release — that's the loan structure I wrote about in the coding-speedup post &amp;lt;!-- TODO: link "Your AI Coding Speedup Is a Loan" --&amp;gt;, and automated harness search will only accelerate the depreciation, since it rediscovers those tricks for cents on the engineer-dollar. Pile two appreciates, because the more capable and self-modifying the system, the more the trust boundary is the only part you actually own.&lt;/p&gt;

&lt;p&gt;STOP's capability-threshold result cuts both ways here and closes the argument neatly: below the threshold, the loop can't help itself; above it, the loop starts probing the checker. Either way, the invariants aren't optional.&lt;/p&gt;

&lt;p&gt;Read &lt;a href="https://lilianweng.github.io/posts/2026-07-04-harness/" rel="noopener noreferrer"&gt;Weng's survey&lt;/a&gt; — it's the best map of this territory right now. Then go look at what your agents are already writing into their own context for tomorrow.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Papers referenced: &lt;a href="https://lilianweng.github.io/posts/2026-07-04-harness/" rel="noopener noreferrer"&gt;Weng 2026 (survey)&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2505.22954" rel="noopener noreferrer"&gt;DGM — Zhang et al. 2025&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2606.09498" rel="noopener noreferrer"&gt;Self-Harness — Zhang et al. 2026&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2603.28052" rel="noopener noreferrer"&gt;Meta-Harness — Lee et al. 2026&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2310.02304" rel="noopener noreferrer"&gt;STOP — Zelikman et al. 2023&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2510.04618" rel="noopener noreferrer"&gt;ACE — Zhang et al. 2025&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2605.27276" rel="noopener noreferrer"&gt;SIA — Hebbar et al. 2026&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2601.03315" rel="noopener noreferrer"&gt;Trehan &amp;amp; Chopra 2026&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2601.11868" rel="noopener noreferrer"&gt;Terminal-Bench 2.0 — Merrill et al. 2026&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2605.26340" rel="noopener noreferrer"&gt;ScientistOne — Meng et al. 2026&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2511.16072" rel="noopener noreferrer"&gt;Bubeck et al. 2025&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>My Strawman Baseline Beat My Own Scheme on Half the Gate Classes</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Mon, 06 Jul 2026 11:48:40 +0000</pubDate>
      <link>https://dev.to/p0rt/my-strawman-baseline-beat-my-own-scheme-on-half-the-gate-classes-177h</link>
      <guid>https://dev.to/p0rt/my-strawman-baseline-beat-my-own-scheme-on-half-the-gate-classes-177h</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/p0rt/your-provenance-vector-dies-at-the-storage-boundary-4cc"&gt;Part 4&lt;/a&gt; ended with a question I couldn't answer: has anyone actually measured what gate decisions do on the reconstructed provenance vector versus the original? Not argued from first principles. Measured.&lt;/p&gt;

&lt;p&gt;Nobody in the comments had data. Neither did I. So I built the harness: &lt;a href="https://github.com/P0rt/provenance-compaction-lab" rel="noopener noreferrer"&gt;provenance-compaction-lab&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four arms, one oracle
&lt;/h2&gt;

&lt;p&gt;Four provenance-tracking arms observe the &lt;em&gt;same&lt;/em&gt; synthetic trajectory — same seed, same degradation events, same merges. They differ only in what happens to provenance between decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ground_truth&lt;/strong&gt; — full vector, full lineage, never compacted. The oracle everything else is judged against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;structural_min&lt;/strong&gt; — the Part 4 scheme. Axis scores keep their running min. Every C steps, lineage truncates to the last K hops; taint ids attached in the folded prefix are dropped, only a count survives. The compression penalty multiplies into &lt;code&gt;reconstruction&lt;/code&gt;, which is folded into the min like any other axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;structural_perhop&lt;/strong&gt; — identical, except &lt;code&gt;reconstruction&lt;/code&gt; is never min-folded. It's carried structurally as &lt;code&gt;(n_compactions, worst_penalty)&lt;/code&gt; and handed to gates as data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;prose&lt;/strong&gt; — the honest-but-naive baseline, not a proposal. Every C steps an LLM summarizes the working state, provenance included, into ≤150 words; a second call extracts scores and taints back out. Whatever survives the round trip is all this arm knows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Merge is unchanged since &lt;a href="https://dev.to/p0rt/trust-isnt-a-scalar-typed-provenance-for-agent-chains-229p"&gt;Part 3&lt;/a&gt; — element-wise min:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProvenanceVector&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProvenanceVector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Merge = element-wise min across inputs (Part 3).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;vs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merge() needs at least one input vector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ProvenanceVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AXES&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 5 steps, nine gate policies — score thresholds, reconstruction-coupled, lineage blocklist, lineage allowlist, several flagged irreversible — fire against all four arms, and every disagreement with the oracle is logged. The matrix: 500-step horizons, cadences C ∈ {10, 25, 50}, K=5, allowlist window W=8 (&amp;gt; K deliberately, so the starvation shows), reconstruction penalty 0.02 per compaction, three degradation profiles, 20 seeds per cell. All four hypotheses were written into the spec before the first line of code. The whole mock matrix reruns in seconds, deterministic per seed.&lt;/p&gt;

&lt;p&gt;One limitation up front: the trajectories are synthetic. The generator is the component you're meant to swap for your own traces — more on that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of one false-proceed
&lt;/h2&gt;

&lt;p&gt;Before any aggregate number, here is one decision going wrong, hop by hop.&lt;/p&gt;

&lt;p&gt;Seed 0, C=25, med profile. Decision at step 25, gate &lt;code&gt;payment_no_untrusted_taint&lt;/code&gt; — irreversible, blocks on &lt;code&gt;unverified_web&lt;/code&gt; and &lt;code&gt;tool_flaky&lt;/code&gt; taints. The working value's full taint history at step 25, which is what the oracle sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;taint:unverified_web:2      ← unverified web fetch, step 2
taint:tool_flaky:6          ← flaky tool call, step 6
taint:tool_flaky:7          ← flaky tool call, step 7
taint:unverified_web:11     ← unverified web fetch, step 11
taint:stale_cache:16        ← stale cache read, step 16
taint:fallback_model:24     ← fallback model, step 24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At step 25 the first compaction fires. This is the deliberate lossy step, verbatim from the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;truncate_lineage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValueState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keep_hops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Truncate to the last K hops; fold the prefix into aggregate counts and
    drop the folded taint ids from ``tainted_by`` (only the count survives).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;hops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hops&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;keep_hops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;folded_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;keep_hops&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;kept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;keep_hops&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nc"&gt;FoldedPrefix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;folded_now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;absorb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;
    &lt;span class="c1"&gt;# invariant for structural arms: tainted_by == union of taints_added over
&lt;/span&gt;    &lt;span class="c1"&gt;# the surviving (visible) hops — folded taint ids are gone.
&lt;/span&gt;    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tainted_by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taints_added&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;kept&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kept&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lineage truncates to the last K=5 hops. Every taint attached in the folded prefix is gone. What structural_min still sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;taint:fallback_model:24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The oracle blocks the payment — four gate-relevant taints. structural_min sees zero gate-relevant taints and proceeds. Nothing in the surviving state even hints that a check was skipped: the fold counter (&lt;code&gt;n_taints_folded&lt;/code&gt;) is the only trace, and a default-allow gate has no reason to look at it.&lt;/p&gt;

&lt;p&gt;Same state under the other two lineage strategies — measured, not hypothetical. &lt;strong&gt;Degrade-to-untrusted&lt;/strong&gt; sees &lt;code&gt;n_taints_folded &amp;gt; 0&lt;/code&gt; and refuses to act; here it happens to be right, but had the folded taints been benign, the same refusal would have been a false-stop. &lt;strong&gt;Rehydrate&lt;/strong&gt; fetches the 21 folded hops of the value's merged ancestry from the append-only log — 2.6 KB read — and recovers the oracle's block exactly.&lt;/p&gt;

&lt;p&gt;That single mechanism, repeated across the matrix, produces the first headline number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline numbers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;3.47%&lt;/strong&gt; of irreversible-action decisions (payment, send) proceeded when the uncompacted oracle said block — structural_min, C=25, med profile.&lt;/li&gt;
&lt;li&gt;structural_min memory &lt;strong&gt;dies at compaction cycle ≈ 35&lt;/strong&gt;: from that cycle on, every reconstruction-coupled gate blocks permanently (0.98³⁵ ≈ 0.493 &amp;lt; 0.5). structural_perhop never dies.&lt;/li&gt;
&lt;li&gt;Prose-vs-structural flip ratio: &lt;strong&gt;1.39×&lt;/strong&gt; — prose disagrees with the oracle on 9.97% of decisions, structural_min on 7.16%, all gates pooled.&lt;/li&gt;
&lt;li&gt;Rehydration: &lt;strong&gt;459 cold-storage lookups per 100 lineage-gate decisions&lt;/strong&gt; buy the flip rate down from 8.01% to 0.00%.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One sort before the details, because that list mixes two kinds of number. The death cycle is arithmetic in a lab coat — 0.98³⁵ &amp;lt; 0.5 needs a calculator, not a harness — and the error polarity two sections down follows from the gate definitions once you state them. For those, the harness contributes the price tag, not the fact. Numbers 1, 3 and 4 are measurements: nothing in the setup forces 3.47% rather than 12%, prose losing overall while winning two gate classes, or rehydration costing 4.6 lookups instead of 40.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2fmhb15bpqng1bd8nveh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2fmhb15bpqng1bd8nveh.png" alt="False-proceed rate on irreversible gates versus compaction cadence, one line per degradation profile: the more often memory compacts, the more often irreversible actions fire against the oracle's block" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Boyko was right twice
&lt;/h2&gt;

&lt;p&gt;On Part 4, Nazar Boyko made two claims sharp enough to preregister as hypotheses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claim one:&lt;/strong&gt; axis scores would track closely between arms; the gates that inspect &lt;em&gt;lineage&lt;/em&gt; are where decisions split. Confirmed — with a disclosure. Score-gate flip rate for both structural arms is 0.00%, and that number is &lt;strong&gt;by construction, not a discovery&lt;/strong&gt;: compaction never touches the running min of the base axes, and the harness property-tests exactly that invariant (base-axis drift against the oracle: 0.0000, at every decision point, every config). What the measurement adds is where divergence concentrates once scores are ruled out: lineage gates flip 8.01% of decisions blind, and reconstruction-coupled gates do worse than that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claim two:&lt;/strong&gt; the recursion. If the compression penalty is folded into a running min, &lt;code&gt;reconstruction&lt;/code&gt; decays monotonically toward zero regardless of how clean each individual hop is. The death-spiral run — 5,000 steps, C=25, 200 compactions — confirms it: monotone the whole way down, crossing the 0.5 gate threshold at cycle 35. From that point every reconstruction-coupled gate blocks permanently. The scores are pristine, the memory is technically alive, and nothing is allowed to act on it.&lt;/p&gt;

&lt;p&gt;structural_perhop, which carries &lt;code&gt;(n_compactions, worst_penalty)&lt;/code&gt; instead of folding, posts &lt;strong&gt;0.00%&lt;/strong&gt; flips on the same gates and never dies. Drift says the same thing from another angle: structural_min's only drifting axis is &lt;code&gt;reconstruction&lt;/code&gt;, at 0.2100 MAE against the oracle; perhop holds it to 0.0191.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0tozo60x3o80gctfdt5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0tozo60x3o80gctfdt5.png" alt="Reconstruction axis over compaction cycles: the min-folded variant decays monotonically and crosses the 0.5 gate threshold at cycle 35, while the per-hop variant stays flat indefinitely" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The error direction is a design choice
&lt;/h2&gt;

&lt;p&gt;Both lineage gate styles were in the harness on purpose, and W &amp;gt; K was chosen deliberately so the starvation would show.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocklist gates&lt;/strong&gt; — default-allow: "block if any &lt;code&gt;taint:unverified_web:*&lt;/code&gt; appears in &lt;code&gt;tainted_by&lt;/code&gt;." They fail dangerous: &lt;strong&gt;8.50% false-proceeds, 0.00% false-stops&lt;/strong&gt;. The fold forgets taints; the gate finds nothing to object to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allowlist gates&lt;/strong&gt; — default-deny: "proceed only if lineage proves no fallback hop within the last 8 hops." They fail expensive: &lt;strong&gt;7.53% false-stops, 0.00% false-proceeds&lt;/strong&gt;. The proof gets folded away; the gate refuses.&lt;/p&gt;

&lt;p&gt;Perfect polarization, and it's the same compaction both times. Compaction decides &lt;em&gt;that&lt;/em&gt; you lose information; the gate's default under missing information decides &lt;em&gt;which way&lt;/em&gt; you fail. So pick the polarity per action class: default-deny anything irreversible. You pay in over-blocking. The alternative pays in wired money.&lt;/p&gt;

&lt;p&gt;The direction of this result was predictable from the definitions — that's exactly why it preregistered cleanly. The magnitudes at these degradation rates were not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F539ao8gai6gpa4anpc88.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F539ao8gai6gpa4anpc88.png" alt="Gate agreement with the oracle by gate class and arm: structural arms hold 100 percent on score gates, and the divergence concentrates in reconstruction-coupled and lineage gate classes" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The strawman refuses to die
&lt;/h2&gt;

&lt;p&gt;H4, as preregistered: &lt;em&gt;prose flip rates dominate the structural arms on every gate class.&lt;/em&gt; Verdict: &lt;strong&gt;FAIL.&lt;/strong&gt; Per gate class, mock channel, blind mode:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gate class&lt;/th&gt;
&lt;th&gt;prose&lt;/th&gt;
&lt;th&gt;structural_min&lt;/th&gt;
&lt;th&gt;structural_perhop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;score&lt;/td&gt;
&lt;td&gt;2.44%&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reconstruction&lt;/td&gt;
&lt;td&gt;4.97%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.17%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lineage_blocklist&lt;/td&gt;
&lt;td&gt;6.82%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.50%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lineage_allowlist&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29.41%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.53%&lt;/td&gt;
&lt;td&gt;7.53%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The strawman beats the Part 4 scheme on two of four classes. On reconstruction-coupled gates, the min-folded penalty is a bigger source of corruption than the noisy channel — the death spiral punishes structural_min before prose noise catches up. On blocklist gates, deterministic truncation forgets taints at roughly the rate a lossy extractor does.&lt;/p&gt;

&lt;p&gt;My first instinct was that the mock channel was rigged against prose — taint recall was &lt;em&gt;assumed&lt;/em&gt; at 0.6. So I ran the real one: gpt-5-mini, the actual summarize→extract round trip. Here is the blob it produced for the exact six-taint state from the worked example above:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Working memory state: freshness 0.700, capability 0.650, tool_integrity 0.650, verification 0.450, reconstruction 1.000. Attached taints: taint:fallback_model:24, taint:stale_cache:16, taint:tool_flaky:6, taint:tool_flaky:7, taint:unverified_web:11, taint:unverified_web:2. Overall, the memory is moderately fresh and moderately capable with intact reconstruction, but tool integrity is degraded and verification is low, indicating reduced trust in outputs that relied on tools or external content. Multiple taints note use of a fallback model, stale cache data, two instances of flaky tools, and two unverified web sources; these should be considered when using or revalidating this state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Extraction recovered all five scores exactly and all six taint ids. Across 993 extractions: 0 parse failures, taint recall 0.893, precision 0.904 (245 fabricated taints), score MAE ≈ 0.0001. A modern small model copies explicit values through prose nearly losslessly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;parameter&lt;/th&gt;
&lt;th&gt;mock (assumed)&lt;/th&gt;
&lt;th&gt;gpt-5-mini (measured)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;score noise&lt;/td&gt;
&lt;td&gt;gaussian σ = 0.08&lt;/td&gt;
&lt;td&gt;≈ 0.0001 MAE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;taint recall&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.893&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;taint precision&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.904&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flip rate, score gates&lt;/td&gt;
&lt;td&gt;2.4%&lt;/td&gt;
&lt;td&gt;0.08%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flip rate, reconstruction gates&lt;/td&gt;
&lt;td&gt;5.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flip rate, allowlist gates&lt;/td&gt;
&lt;td&gt;29.4%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;43.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Disclosure on this table before the punchline: the real column is a smaller slice — C ∈ {10, 25}, med profile only, 2 seeds, those 993 extractions — while the mock column is the full matrix. Read it as channel characterization, not a matched benchmark; the repo ships a matched-slice config if you want the strict twin. (The matched slice also doubles as a sanity check: the structural arms' columns come out identical between mock and real runs, because they never touch the channel — the only column that actually moves is prose.)&lt;/p&gt;

&lt;p&gt;The last row is the point. The channel got nearly perfect, and allowlist flips got &lt;em&gt;worse&lt;/em&gt;. A summary preserves values; it destroys structure. "No fallback hop within the last 8 hops" is a proof about an ordered window, and no amount of faithful prose reconstitutes the window. &lt;strong&gt;The failure mode of prose isn't noise — it's the loss of provability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Caveat, because it matters: this measured the channel in a best case. The summarize prompt hands the model a clean structured list and asks it to carry the list across. Real agent memory interleaves provenance with content competing for the same 150 words. Treat the mock's 0.6 recall as the pessimistic bound and gpt-5-mini's 0.893 as the optimistic one — on structure-dependent gates, both bounds tell the same story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the crossover sits
&lt;/h2&gt;

&lt;p&gt;"Prose sometimes beats structural_min" is a result. A design rule needs to know &lt;em&gt;when&lt;/em&gt;. A finer cadence sweep, C from 5 to 100, med profile, 20 seeds per point, settles it.&lt;/p&gt;

&lt;p&gt;On reconstruction-coupled gates, structural_min is worse than the prose strawman at every cadence up to C=50 — at C=10 that's 34.92% flips against prose's 6.85% — and only drops below prose between C=50 and C=75. The rule that falls out: &lt;strong&gt;once memory sees more than ~7–10 compaction cycles inside a decision horizon, min-folded reconstruction — not summarization noise — is the dominant corruption source.&lt;/strong&gt; On blocklist gates the cross comes even earlier, around C=15. Pooled over all gate classes, prose stays behind at every cadence — allowlist starvation and score noise keep it there.&lt;/p&gt;

&lt;p&gt;Before anyone quotes that bolded rule as a constant: it isn't one. The threshold is a function of two parameters I chose — the per-compaction penalty (0.02 here) and the noise of the channel it races. The death cycle is closed-form: memory dies at n = ln(θ) / ln(1 − p), which at θ = 0.5, p = 0.02 gives 34.3 — the first whole cycle below threshold is 35, matching the run. That part scales as 1/p: double the penalty, halve the death cycle. Arithmetic.&lt;/p&gt;

&lt;p&gt;The crossover against prose does &lt;em&gt;not&lt;/em&gt; scale as 1/p — and I know because I assumed it would and swept it: 14 cadences × 5 penalties × 20 seeds, 1,400 mock runs, under a minute. The fit says the crossover cycle count moves as ~p^−1.6, steeper than 1/p, because the baseline it races isn't flat: prose noise compounds with cycle count too, just slower, so as the penalty shrinks, the intersection runs away superlinearly. (Exponent fitted on the sweep grid, p from 0.005 to 0.1 — don't carry it far outside.) &lt;code&gt;prov-lab report&lt;/code&gt; prints the analytic death cycle for whatever penalty you configure, and &lt;code&gt;prov-lab sweep&lt;/code&gt; maps your crossover — so the rule you quote can be yours, not mine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw9r9zxkenvfmu6r4bwpi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw9r9zxkenvfmu6r4bwpi.png" alt="Crossover cadence between structural_min and the prose channel as a function of the reconstruction penalty, log axes, with the fitted power law near p to the minus 1.6" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which exposes what H4 got wrong at preregistration time: "structural vs prose" was never the axis. The axis is &lt;em&gt;which fields the compaction preserves, relative to which fields the gates read&lt;/em&gt;. Every arm in this experiment failed exactly where it discards something some gate consumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quimby's question has a price tag
&lt;/h2&gt;

&lt;p&gt;Max Quimby asked, on Part 4: when lineage has been compressed and a policy needs the detail — do you re-expand from somewhere, or does the policy degrade to treating the result as untrusted? Both answers are in the harness, with deciding blind as the control. The irreversible payment gate, three ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;mode&lt;/th&gt;
&lt;th&gt;agreement&lt;/th&gt;
&lt;th&gt;false-proceed&lt;/th&gt;
&lt;th&gt;false-stop&lt;/th&gt;
&lt;th&gt;lookups / 100 decisions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;blind&lt;/td&gt;
&lt;td&gt;91.22%&lt;/td&gt;
&lt;td&gt;8.78%&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;degrade&lt;/td&gt;
&lt;td&gt;95.72%&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;4.28%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rehydrate&lt;/td&gt;
&lt;td&gt;100.00%&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;639.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Aggregated across all lineage gates: 459 lookups per 100 decisions, tens of KB read, flip rate 8.01% → 0.00%. The gate with all three modes, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GateView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hop_log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HopLog&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GateDecision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;taints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tainted_by&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lookups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;bytes_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;detail_missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_taints_folded&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;degrade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;detail_missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# degrade-to-untrusted: taints were dropped, refuse to act
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;GateDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proceed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rehydrate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;detail_missing&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;hop_log&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hop_log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded_hop_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;lookups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;folded_hop_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;taints&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taints_added&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_prefixes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;taints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;GateDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proceed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lookups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lookups&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes_read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bytes_read&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Degrade costs nothing and converts every dangerous error into an expensive one — a legitimate answer for reversible actions. Rehydration from an append-only hop log recovers the oracle &lt;em&gt;exactly&lt;/em&gt;, at a price that turned out measurable and small: about 4.6 lookups per lineage-gate decision. For irreversible gates, that's the trade I'd take every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persist running-min axis scores.&lt;/strong&gt; Constant size, lossless by construction, drift 0.0000 at every decision point — property-tested. This half of Part 4 survives contact with measurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never fold a compression penalty into a min.&lt;/strong&gt; Track &lt;code&gt;(n_compactions, worst_penalty)&lt;/code&gt; structurally. perhop flips 0.00% of reconstruction-coupled decisions and never dies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick gate polarity per action class.&lt;/strong&gt; Default-deny anything irreversible: fail expensive, not dangerous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append-only hop log plus rehydrate-on-demand&lt;/strong&gt; for irreversible gates; degrade-to-untrusted is fine for reversible ones. A reference implementation on stdlib sqlite3 ships in the repo as &lt;code&gt;provlab.store&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure your own pipeline.&lt;/strong&gt; The real-channel run was ~2,000 requests to a small model — $1–1.5 and about 19 minutes. Less than a coffee.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run prov-lab run &lt;span class="nt"&gt;--config&lt;/span&gt; experiments/config.yaml &lt;span class="nt"&gt;--mock&lt;/span&gt;
uv run prov-lab report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whole matrix in seconds, deterministic per seed, MIT: &lt;a href="https://github.com/P0rt/provenance-compaction-lab" rel="noopener noreferrer"&gt;P0rt/provenance-compaction-lab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two ways to point this at your own system now. &lt;code&gt;prov-lab audit&lt;/code&gt; is the closing question as a command: ~20 lines of YAML — which fields your compaction preserves, which fields each gate reads, each gate's default polarity — and it prints the starvation table: which of your gates are deciding blind, and in which direction they'll fail. No simulation, no traces, five minutes. And &lt;code&gt;prov-lab run --trace your.jsonl&lt;/code&gt; replays the whole harness over real agent logs: taint-derivation rules are YAML data, not code (tool status ≠ ok → taint, cache age over threshold → taint, and so on), the oracle is a full-provenance replay of the same trace, and the report tells you what it could and couldn't map. Synthetic trajectories are this experiment's honest limitation; replication on a real memory pipeline is the one result I can't produce alone.&lt;/p&gt;

&lt;p&gt;Credits, which in this series means authorship: &lt;strong&gt;Nazar Boyko&lt;/strong&gt; called both the score/lineage split and the min-fold recursion before a line of this code existed. &lt;strong&gt;Max Quimby&lt;/strong&gt; asked the question that became a price tag. This part, like the ones before it, was co-written by the comment section.&lt;/p&gt;

&lt;p&gt;So, the question for this thread: &lt;strong&gt;what does your compaction actually preserve, relative to what your gates read?&lt;/strong&gt; &lt;code&gt;prov-lab audit&lt;/code&gt; is that question as a command; &lt;code&gt;--trace&lt;/code&gt; is the full version. Either way — post the table.&lt;/p&gt;

&lt;p&gt;Part 6 is &lt;code&gt;attest()&lt;/code&gt; — restoration semantics. Everything in this system can only lower an axis. What event is allowed to raise one, and who holds the authority to say so?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
    <item>
      <title>Your Provenance Vector Dies at the Storage Boundary</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Wed, 01 Jul 2026 11:58:09 +0000</pubDate>
      <link>https://dev.to/p0rt/your-provenance-vector-dies-at-the-storage-boundary-4cc</link>
      <guid>https://dev.to/p0rt/your-provenance-vector-dies-at-the-storage-boundary-4cc</guid>
      <description>&lt;p&gt;Last post I argued that agent trust should be a &lt;a href="https://dev.to/p0rt/trust-isnt-a-scalar-typed-provenance-for-agent-chains-229p"&gt;typed provenance vector&lt;/a&gt;: carry what-degraded-and-how alongside each result, propagate it, let each consumer apply its own policy. The comments agreed on the model and then immediately found the two places it breaks in the real world. Both are load-bearing, both were things I hand-waved, and this post is about them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mote&lt;/strong&gt; asked what happens when the agent runs 500 steps and the vector no longer fits in the context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mykola&lt;/strong&gt; said the quiet part louder: &lt;em&gt;"you can build a perfect trust lattice but most agents just act on output without checking provenance. The hard part is enforcement, not the model."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are right, and together they name the two ways a provenance vector dies in production: nobody reads it, or it can't survive being stored. One problem is about &lt;em&gt;enforcement&lt;/em&gt;, the other about &lt;em&gt;persistence&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Two failure modes kill a provenance vector in production. &lt;strong&gt;Enforcement:&lt;/strong&gt; if acting on a value doesn't &lt;em&gt;require&lt;/em&gt; passing through the gate, developers (and models writing tool calls) will skip it — so make the unsafe path unrepresentable via types, not discipline. &lt;strong&gt;Persistence:&lt;/strong&gt; on long-horizon agents the vector must survive compression to fit bounded memory, and naive summarization washes out exactly the axes you need — so compress structurally (per-axis, lossless scores + lossy lineage), not as prose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Problem 1: enforcement, or the vector nobody reads
&lt;/h2&gt;

&lt;p&gt;Mykola's point is the one that should scare you, because it's true of almost every "add metadata to make it safer" scheme: the metadata is optional, so under deadline it gets skipped. You can ship a beautiful &lt;code&gt;Provenance&lt;/code&gt; type and six months later find that the payment path reads &lt;code&gt;result.value&lt;/code&gt; and never touches &lt;code&gt;result.provenance&lt;/code&gt;. The lattice was perfect. Nobody consulted it.&lt;/p&gt;

&lt;p&gt;The fix is not "remember to check." Discipline doesn't scale and it definitely doesn't survive a model writing its own tool calls. The fix is to make &lt;em&gt;acting without checking&lt;/em&gt; something the code physically cannot express.&lt;/p&gt;

&lt;p&gt;This is a solved problem in a neighboring field, and it's worth stealing wholesale. Capability-based security has done this for decades: authority is an &lt;strong&gt;unforgeable token you must hold a reference to&lt;/strong&gt; — you can't perform the action without possessing the capability, and possession is the check. Recent work brings this into static types explicitly: track the capability in the type system, and the &lt;em&gt;absence&lt;/em&gt; of it in a function's type guarantees, at compile time, that the function can't perform the guarded action. The safety isn't a runtime assertion you might forget — it's a property of what typechecks.&lt;/p&gt;

&lt;p&gt;Applied to provenance, the move is: &lt;strong&gt;the irreversible action can't accept a raw value, only a gated one.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NoReturn&lt;/span&gt;
&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Provenanced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A value you cannot use for a side effect without unwrapping —
    and the ONLY unwrap path runs the gate.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prov&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Provenance&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_prov&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prov&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unwrap_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_prov&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proceed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ProvenanceViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_prov&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# refetch / escalate / ...
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_value&lt;/span&gt;

&lt;span class="c1"&gt;# the side-effecting function's SIGNATURE refuses raw values:
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;charge_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Provenanced&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;money&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unwrap_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# the only way to get the Money out
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now "charge the card without checking provenance" doesn't fail code review — it doesn't typecheck. There is no path from a raw &lt;code&gt;Money&lt;/code&gt; to &lt;code&gt;charge_card&lt;/code&gt;, because the signature demands &lt;code&gt;Provenanced[Money]&lt;/code&gt;, and the only way to extract the value runs the gate. You've moved the enforcement from the developer's memory into the type system. It's the same trick as idempotency keys from two posts ago: don't ask people to remember the safe thing, make the unsafe thing unrepresentable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limit&lt;/strong&gt; (which a commenter will rightly raise, so I'll raise it first): this holds at the &lt;em&gt;framework boundary&lt;/em&gt;, in typed code you control. The moment your agent writes free-form tool calls — the model generating Python that calls your API directly — it can simply not use the wrapper, and you're back to enforcement-by-hope. For that case the type system can't reach, so enforcement has to drop to the infrastructure layer: the side-effecting tools sit behind a proxy that refuses any call whose payload doesn't carry valid provenance. You lose compile-time guarantees and get runtime rejection instead — worse, but still "structurally can't skip it" rather than "please remember." The principle survives even when the mechanism changes: enforcement lives in something the actor can't route around, never in something it's asked to honor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: provenance that survives compression
&lt;/h2&gt;

&lt;p&gt;mote's problem is deeper and I didn't have an answer in the thread, so I went and found one. Here's the setup: a long-horizon agent — mote's case is literally robots on edge hardware with a hard context ceiling — can't hold a growing provenance graph in working memory across 500 steps. It has to compress. And the standard compression move, summarize-history-into-prose, is catastrophic for provenance specifically, because summarization is &lt;em&gt;lossy in an uncontrolled way&lt;/em&gt; — it'll happily drop "step 47 ran on a stale cache" to save tokens, and that's the one fact a downstream gate needed.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. The field now attributes the majority of enterprise agent failures to context drift and memory loss during multi-step reasoning — not to hitting the context limit, but to the &lt;em&gt;quality degradation on the way there&lt;/em&gt;. And there's a subtler trap the RL-agent researchers named: compression credit is causally entangled — the same downstream failure needs opposite explanations depending on whether the bad state came from a tool or from memory. If your compression flattens that distinction, you can't even diagnose what broke.&lt;/p&gt;

&lt;p&gt;So the naive answer — "summarize the provenance too" — reintroduces the exact scalar-collapse problem from the last post, now smuggled in through the storage layer. A summary is an average wearing a trench coat.&lt;/p&gt;

&lt;p&gt;The better answer comes from a simple observation: &lt;strong&gt;the axes have different compression economics, so don't compress them uniformly.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scores compress to almost nothing, losslessly.&lt;/strong&gt; A per-axis float — &lt;code&gt;freshness: 0.2, capability: 0.6&lt;/code&gt; — is a handful of numbers. Even across 500 steps, if you keep only the &lt;em&gt;running minimum per axis&lt;/em&gt; (which is what the gate reads anyway; recall the &lt;code&gt;min&lt;/code&gt; from last post), that's constant size regardless of history length. You never need to compress the scores, because &lt;code&gt;min&lt;/code&gt;-reduction already bounds them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lineage is what explodes, and lineage is what you can afford to lose.&lt;/strong&gt; The &lt;code&gt;tainted_by&lt;/code&gt; sets — &lt;em&gt;which exact steps&lt;/em&gt; degraded each axis — grow with the trajectory. But for the &lt;em&gt;gate decision&lt;/em&gt;, you usually don't need the full ancestry; you need "is any unverified degraded step still on the live path." So this is the part you lossy-compress: keep the axis scores whole, summarize the lineage behind a pointer, and accept that you lose "which exact step" while keeping "how degraded, per axis."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This maps onto where the research is heading. The most promising long-horizon approaches have stopped treating the trajectory as prose to be summarized and started treating it as a &lt;strong&gt;typed dependency graph the agent annotates as it works&lt;/strong&gt;, with a deterministic eviction policy that walks the graph when the token budget blows — explicitly to avoid the four pathologies of prose compaction: unpredictable lossiness, structural destruction, blocking cost, and compression-induced hallucination. A typed provenance vector &lt;em&gt;is&lt;/em&gt; that annotation. The eviction policy for provenance is: evict lineage detail, never evict axis scores.&lt;/p&gt;

&lt;p&gt;There's one more axis this forces you to add, and it's almost funny: &lt;strong&gt;compression is itself a degradation source.&lt;/strong&gt; A vector reconstructed from a lossy summary is less trustworthy than one carried whole — so "this provenance was reconstructed across a storage boundary" is a real provenance fact that deserves its own axis. &lt;code&gt;reconstruction: 0.8&lt;/code&gt; means "these scores survived a compaction; treat the lineage as approximate." The provenance system has to describe its own lossiness. Turtles, but only two deep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this keeps being a security problem in disguise
&lt;/h2&gt;

&lt;p&gt;Every post in this series has ended up borrowing from security, and this one makes the reason explicit. Traditional taint tracking assumes deterministic program states and exact data-flow: memory locations, registers, string matches. LLM agents break all of that — untrusted content gets &lt;em&gt;rewritten, summarized, and used to choose later actions&lt;/em&gt;, so "did this bad input reach that sink" is a question about semantic and causal influence, not byte-level flow. The agent security researchers building taint trackers for exactly this case had to redefine propagation to include semantic transformation and cross-session persistence through memory — which is the same two problems this post is about (enforcement and persistence), arrived at from the attack side instead of the reliability side.&lt;/p&gt;

&lt;p&gt;That convergence is the tell. When the reliability people and the security people independently reinvent the same structure — unforgeable gating plus provenance that survives memory — it's because it's the actual shape of the problem, not a preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the series stands
&lt;/h2&gt;

&lt;p&gt;Four posts, one arc:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt; — agents fail on capacity (rate limits), not reasoning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correctness&lt;/strong&gt; — the capacity fixes buy uptime by acting on unearned output; you need &lt;em&gt;correct&lt;/em&gt; uptime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model&lt;/strong&gt; — trust isn't a scalar; it's a typed provenance vector with policy at the consumer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reality&lt;/strong&gt; (this one) — that vector only works if it's &lt;em&gt;unskippable&lt;/em&gt; (enforcement by type/proxy) and &lt;em&gt;survivable&lt;/em&gt; (structural compression, not prose).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The through-line, one more time: agent reliability is a provenance problem, and provenance is a solved discipline — capability security, data lineage, taint analysis — that we're re-deriving because the untraceable thing now acts, and acts through a bounded, forgetful, non-deterministic memory. The novelty isn't the primitives. It's that they now have to hold under compression and under a model that can route around anything you merely &lt;em&gt;ask&lt;/em&gt; it to respect.&lt;/p&gt;

&lt;p&gt;If you're building this: gate at a boundary the actor can't skip (type or proxy), compress scores losslessly and lineage lossily, and add a &lt;code&gt;reconstruction&lt;/code&gt; axis the day your provenance crosses a storage line. Start there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Credit, again, to the comment section that wrote the spec: **mote&lt;/em&gt;* (compression across the storage boundary, the edge/bounded-context framing that motivates the whole second half), &lt;strong&gt;Mykola Kondratiuk&lt;/strong&gt; (enforcement is the hard part, not the model), plus &lt;strong&gt;Tae Kim&lt;/strong&gt;, &lt;strong&gt;Nazar Boyko&lt;/strong&gt;, &lt;strong&gt;Ken&lt;/strong&gt;, and &lt;strong&gt;Ahmet Özel&lt;/strong&gt; for sharpening the axis rules in the last thread. Open question for this one: has anyone actually run provenance across a compaction boundary in production and measured what the gate decisions do on the reconstructed vector versus the original? That's the experiment I don't have data for yet — and it's the one that decides whether any of this holds.*&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2603.00991" rel="noopener noreferrer"&gt;"Tracking Capabilities for Safer Agents"&lt;/a&gt; — capabilities as unforgeable tokens tracked in static types; compile-time non-interference from the absence of a capability.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2604.23374" rel="noopener noreferrer"&gt;"Ghost in the Agent: Redefining Information Flow Tracking for LLM Agents" (NeuroTaint)&lt;/a&gt; — why classical taint doesn't transfer: agents rewrite, summarize, and act on untrusted content; taint as semantic/causal/persistent influence.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2606.11213" rel="noopener noreferrer"&gt;"Beyond Compaction: Structured Context Eviction for Long-Horizon Agents"&lt;/a&gt; — annotate the trajectory as a typed dependency graph; deterministic graph-walking eviction instead of prose summarization.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://zylos.ai/research/2026-02-28-ai-agent-context-compression-strategies/" rel="noopener noreferrer"&gt;"AI Agent Context Compression: Strategies for Long-Running Sessions"&lt;/a&gt; — context drift/memory loss as the majority of enterprise agent failures; anchored iterative summarization beats full reconstruction.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2606.16285" rel="noopener noreferrer"&gt;"HiMPO: Hindsight-Informed Memory Policy Optimization"&lt;/a&gt; — causally entangled memory credit: the same failure needs opposite explanations depending on tool-vs-memory origin.&lt;/li&gt;
&lt;li&gt;The series: &lt;a href="https://dev.to/p0rt/your-ai-agent-isnt-failing-because-it-hallucinates-its-failing-because-of-rate-limits-2d60"&gt;Part 1 — capacity&lt;/a&gt; · &lt;a href="https://dev.to/p0rt/you-fixed-the-rate-limits-now-your-agent-fails-quietly-3keo"&gt;Part 2 — correct uptime&lt;/a&gt; · &lt;a href="https://dev.to/p0rt/trust-isnt-a-scalar-typed-provenance-for-agent-chains-229p"&gt;Part 3 — typed provenance&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Trust Isn't a Scalar: Typed Provenance for Agent Chains</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Mon, 22 Jun 2026 14:21:08 +0000</pubDate>
      <link>https://dev.to/p0rt/trust-isnt-a-scalar-typed-provenance-for-agent-chains-229p</link>
      <guid>https://dev.to/p0rt/trust-isnt-a-scalar-typed-provenance-for-agent-chains-229p</guid>
      <description>&lt;p&gt;Two posts ago, in &lt;a href="https://dev.to/p0rt/you-fixed-the-rate-limits-now-your-agent-fails-quietly-3keo"&gt;the one about agents failing quietly&lt;/a&gt;, I handed you a fix for silent degradation: tag a degraded output &lt;code&gt;trust="degraded"&lt;/code&gt;, propagate the taint down the chain, and gate irreversible actions on it. Clean, shippable, and — as a commenter named Theo pointed out within a day — &lt;strong&gt;wrong in a way that matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tag was a boolean. And trust isn't a boolean. It isn't even a scalar.&lt;/p&gt;

&lt;p&gt;This post is me being wrong in public and fixing it, because the corrected model is genuinely better and most of it was built by people in that comment thread. Credits at the end; they earned them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A single trust score (&lt;code&gt;full&lt;/code&gt;/&lt;code&gt;degraded&lt;/code&gt;, or &lt;code&gt;0.0–1.0&lt;/code&gt;) collapses on real chains, because degradation happens along &lt;em&gt;different axes&lt;/em&gt; — a stale cache lowers &lt;em&gt;freshness&lt;/em&gt;, a weaker fallback lowers &lt;em&gt;capability&lt;/em&gt; — and different downstream steps care about different ones. Collapse them to one number and you either over-reject (every degradation is fatal) or under-reject (the dangerous one gets averaged away). What actually composes is &lt;strong&gt;typed provenance&lt;/strong&gt;: carry a vector of what-was-degraded-and-how alongside the result, propagate it across the chain, and let each consumer apply its own policy at the moment it's about to act.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why a scalar collapses
&lt;/h2&gt;

&lt;p&gt;Here's the case that broke my boolean, almost verbatim from Theo's comment.&lt;/p&gt;

&lt;p&gt;You have two downstream steps, both consuming an upstream result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;summarization&lt;/strong&gt; step. It tolerates a weaker model just fine, but it must not run on stale data.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;price calculation&lt;/strong&gt;. It's the reverse: it needs current data, but a slightly weaker model doing arithmetic is fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the upstream result came from a fallback model reading a 2-hour-old cache. So it's degraded on &lt;em&gt;both&lt;/em&gt; a capability axis (weaker model) and a freshness axis (old cache). What's your single trust score?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you set it &lt;strong&gt;low&lt;/strong&gt; (treat any degradation as serious), the summarization step over-rejects — it would've been totally fine with the weaker model, but your scalar said "degraded" so it bails or escalates needlessly.&lt;/li&gt;
&lt;li&gt;If you set it &lt;strong&gt;high&lt;/strong&gt; (it's "mostly fine"), the price calc under-rejects — it acts on stale data because the scalar averaged the freshness problem into a number that looked acceptable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no single threshold that's simultaneously right for both consumers, because they're not measuring the same thing. A scalar forces every consumer to share one definition of "trustworthy," and they don't have one. As Theo put it: collapse the vector to one number and you destroy exactly the information the consumer needs to make its own decision.&lt;/p&gt;

&lt;p&gt;This isn't just my comment section talking, either — it's where the field is converging. A recent framework (TrustBench) makes the same move explicitly: rather than reduce trust to a single scalar, keep dimensional scores per trust aspect, and weight them per domain — healthcare prioritizing citation validity and recency, finance prioritizing calculation and compliance. Same shape, arrived at independently. When several people reach for the same structure from different directions, it's usually because the structure is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust is a vector; provenance is what you propagate
&lt;/h2&gt;

&lt;p&gt;Here's the reframe that fixes it, and it starts with a vocabulary correction I owe you: I kept calling the thing "trust." That was the bug in the language, not just the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust is not a property of a value. It's a judgment a consumer makes about a value.&lt;/strong&gt; What the value actually &lt;em&gt;carries&lt;/em&gt; is &lt;strong&gt;provenance&lt;/strong&gt; — the typed record of how it came to be: which model produced it, how fresh its inputs were, which tools ran, what got degraded and along which axis. Trust is what each consumer &lt;em&gt;computes from&lt;/em&gt; that provenance, under its own policy. The price calc and the summarizer look at the same provenance and reach different verdicts, and that's correct, not contradictory.&lt;/p&gt;

&lt;p&gt;So you don't propagate a degraded flag. You propagate a &lt;strong&gt;typed vector&lt;/strong&gt;, and each axis degrades independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;FRESHNESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;freshness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;      &lt;span class="c1"&gt;# how current were the inputs
&lt;/span&gt;    &lt;span class="n"&gt;CAPABILITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# how strong was the model that produced this
&lt;/span&gt;    &lt;span class="n"&gt;TOOL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                &lt;span class="c1"&gt;# did the tool calls actually succeed
&lt;/span&gt;    &lt;span class="n"&gt;VERIFICATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# was this checked against ground truth
&lt;/span&gt;
&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Provenance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# per-axis score in [0,1]; 1.0 = fully trusted on that axis
&lt;/span&gt;    &lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;# which upstream step_ids contributed degradation, per axis
&lt;/span&gt;    &lt;span class="n"&gt;tainted_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;upstreams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provenance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provenance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Provenance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# an output is only as fresh as its stalest input, only as
&lt;/span&gt;            &lt;span class="c1"&gt;# capable as its weakest producer — min, not average. averaging
&lt;/span&gt;            &lt;span class="c1"&gt;# is exactly how the dangerous axis gets washed out.
&lt;/span&gt;            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;upstreams&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tainted_by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tainted_by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;upstreams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tainted_by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tainted_by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;min&lt;/code&gt; is doing real work there. The whole failure of my original taint-as-boolean was that it answered "is anything degraded?" — a single OR across the chain. The vector answers "&lt;em&gt;what kind&lt;/em&gt; of degradation is this output carrying, and how much, per axis?" — and crucially, it takes the &lt;strong&gt;minimum per axis&lt;/strong&gt; rather than averaging, because averaging is the mathematical operation that makes a serious freshness problem disappear behind three fine capability scores.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate is per-consumer, not global
&lt;/h2&gt;

&lt;p&gt;Now the irreversibility gate from the last post stops being one global threshold and becomes a policy that lives &lt;em&gt;at each consumer&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# per-axis minimum this consumer requires to act without re-check
&lt;/span&gt;    &lt;span class="n"&gt;floors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;admits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Provenance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;floors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# the summarizer doesn't care about capability, but demands freshness
&lt;/span&gt;&lt;span class="n"&gt;SUMMARIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;floors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FRESHNESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CAPABILITY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# the price calc is the mirror image
&lt;/span&gt;&lt;span class="n"&gt;PRICE_CALC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;floors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FRESHNESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CAPABILITY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERIFICATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action_policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Provenance&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proceed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# which axis failed tells you HOW to recover, not just THAT to stop
&lt;/span&gt;    &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;action_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;floors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FRESHNESS&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;      &lt;span class="c1"&gt;# re-run the stale step on live data
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CAPABILITY&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;re-run-on-primary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate-to-human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the payoff. The same upstream provenance vector flows to both consumers, and they reach &lt;em&gt;different, individually correct&lt;/em&gt; decisions from it. The summarizer proceeds; the price calc refetches. One global score could never do that — and the failed-axis tells you &lt;em&gt;how&lt;/em&gt; to recover, which a boolean never could.&lt;/p&gt;

&lt;p&gt;Notice this also absorbs a point another commenter (Manuel) made independently: he argued the tag should be an enum, not a bool — &lt;code&gt;skipped-tool&lt;/code&gt; vs &lt;code&gt;stale-data&lt;/code&gt; vs &lt;code&gt;retry-budget-exhausted&lt;/code&gt; route differently. He was right, and the vector is the generalization: an enum is a vector with one axis active; the full structure lets multiple axes degrade at once, which is the real production case.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Gate on risk, not confidence" — and confidence is just one axis
&lt;/h2&gt;

&lt;p&gt;The last post argued you should gate on &lt;em&gt;irreversibility&lt;/em&gt;, not on the model's self-reported confidence. The vector makes that precise instead of hand-wavy: &lt;strong&gt;confidence is one axis among several, and it's the one the model grades itself on.&lt;/strong&gt; A model can be 95%-confident (high on a confidence axis) while sitting on a freshness score of 0.2 because it reasoned over a stale cache. The skill-conditional-trust literature makes the same argument from the routing side — a single global score is the wrong object because it can't express "great at this, useless at that." Confidence-as-the-only-axis is how you get the war story everyone has: the agent that was sure, and sure on the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many axes before it stops being worth it?
&lt;/h2&gt;

&lt;p&gt;This is the honest open question, and the one I asked Theo back. A vector with 40 axes is just a scalar's opposite failure — unwieldy, untunable, theater of rigor. My current answer, and I'd genuinely take pushback: &lt;strong&gt;start with the axes that map to your actual degradation sources, and no more.&lt;/strong&gt; If your system has exactly two ways to degrade — fallback model and stale cache — you have two axes (capability, freshness). Add &lt;code&gt;verification&lt;/code&gt; the moment you have a re-check step whose result you want to carry. Add &lt;code&gt;tool&lt;/code&gt; when a tool can half-succeed. The axis count should equal the number of &lt;em&gt;distinct things that can independently go wrong&lt;/em&gt;, not the number of things you can imagine going wrong. If two "axes" always move together, they're one axis.&lt;/p&gt;

&lt;p&gt;The sweet spot, I think, is the smallest set where each axis maps to a different &lt;em&gt;recovery action&lt;/em&gt;. Freshness → refetch. Capability → re-run on primary. Verification → escalate. If two axes would trigger the same recovery, collapse them. The vector earns its complexity only where it changes what you &lt;em&gt;do&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical layer (mostly stolen from the comments)
&lt;/h2&gt;

&lt;p&gt;The vector is the core idea, but the thread surfaced a full toolkit around it, and it'd be dishonest to present any of it as mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Admission control, upstream of everything&lt;/strong&gt; (Dan): before the agent fans out, decide if the whole task can afford to run, and separate the four limits that 429s blur together — provider quota (physics), account quota (policy), task budget (this run), ledger (forensics). The ledger turns out to be the same record as provenance: "this run cost 47 calls, 12 on the fallback tier" is both your bill and your capability-axis score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation at consumption, not production&lt;/strong&gt; (James): don't validate on the fresh-call path and trust the cache; validate when a value is &lt;em&gt;used&lt;/em&gt;, regardless of where it came from. That closes the laundering loophole at the consumer — which is exactly where the per-consumer gate already lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-bound by causality, not wall-clock&lt;/strong&gt; (HARD IN SOFT OUT): I was tempted by "reset taint after N seconds." Don't — degraded state can sleep and surface later. Clear an axis when nothing on the live path still derives from the degraded step, not when a timer expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The poor-man's version for solo builders&lt;/strong&gt; (TuanAnhNguyen): no observability stack? Have any tool that acts on a stale-readable input append one line to a log, and &lt;code&gt;grep&lt;/code&gt; it before anything irreversible. It's the 5%-effort version of the provenance vector — a breadcrumb instead of a graph — and below a certain scale it's the &lt;em&gt;correct&lt;/em&gt; amount of engineering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The distributed correction&lt;/strong&gt; (Abdullah): my original concurrency cap was an in-process semaphore, which silently assumes one process. Under serverless fan-out, N containers each capping at 8 gives you 8N real concurrency. The limiter has to live outside the workers. (Also: TPM saturates before RPM on long-context agents, and "fallback to a cheaper model" is fiction if it draws from the same pooled tier. Both are capability/freshness axis sources you'd otherwise miss.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The parable that says it better than I did
&lt;/h2&gt;

&lt;p&gt;A commenter (HARD IN SOFT OUT) left this, and it's the whole series in five lines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent hit a rate limit. It fell back to a cached answer from last Tuesday. The world changed on Wednesday. The agent kept working. The logs said "cache hit, 200 OK." The user got a message: "Your order has shipped." The warehouse's API key expired on Thursday.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every hop green. Every log a 200. And a real package never ships. A scalar trust score on that final "order shipped" output would read &lt;em&gt;fine&lt;/em&gt; — the last call succeeded. A provenance vector reads &lt;code&gt;freshness: 0.1, tainted_by: {warehouse_check}&lt;/code&gt; and the shipping gate refuses to fire. That's the entire difference between uptime and correct uptime, and between a boolean and a vector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the series
&lt;/h2&gt;

&lt;p&gt;Three posts in, the actual thesis has assembled itself: &lt;strong&gt;agent reliability is a provenance problem.&lt;/strong&gt; Availability (post 1) is the easy axis. Correctness (post 2) is the one that bites. And the structure that makes correctness tractable (post 3) is typed provenance carried through the chain, with policy at the edges. None of that is exotic — it's data lineage, taint analysis, and saga patterns, borrowed from disciplines that solved their version decades ago, newly load-bearing because the untraceable thing now &lt;em&gt;acts&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you're building this: start with two axes and a &lt;code&gt;min&lt;/code&gt;, put the policy at the consumer, and add an axis only when it changes a recovery action. Everything else is premature.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post was largely written by the comments on the last one. Credit, specifically: **Theo Valmis&lt;/em&gt;* (trust-is-a-vector, the summarize-vs-price-calc case, "typed provenance"), &lt;strong&gt;Manuel Bruña&lt;/strong&gt; (enum-not-bool), &lt;strong&gt;Dan&lt;/strong&gt; (admission control, the four-limit split), &lt;strong&gt;James O'Connor&lt;/strong&gt; (validation at consumption), &lt;strong&gt;HARD IN SOFT OUT&lt;/strong&gt; (causality-bound taint, the parable), &lt;strong&gt;TuanAnhNguyen&lt;/strong&gt; (the solo-builder grep version), &lt;strong&gt;Abdullah Shahin&lt;/strong&gt; (the distributed-limiter and pooled-fallback corrections), and &lt;strong&gt;Scarab Systems&lt;/strong&gt; (the "evidence gate" framing that started me thinking about provenance as an obligation, not metadata). Best comment section on this site. Question for the thread: how many axes does your system actually need — and which ones map to a distinct recovery action versus just feeling rigorous?*&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2603.09157" rel="noopener noreferrer"&gt;"Real-Time Trust Verification for Safe Agentic Actions" (TrustBench)&lt;/a&gt; — dimensional trust scores over a scalar, domain-weighted, with block/warn/proceed gating.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/html/2606.14200" rel="noopener noreferrer"&gt;"When Should Agent Trust Be Conditional?"&lt;/a&gt; — why a single global trust score is the wrong object for skill-heterogeneous agents.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/html/2606.04990" rel="noopener noreferrer"&gt;"From Agent Traces to Trust: A Survey of Evidence Tracing and Execution Provenance in LLM Agents"&lt;/a&gt; — persistent lineage across memory writes, retrievals, and reuse.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.montecarlodata.com/blog-redefining-agent-trust-input-output" rel="noopener noreferrer"&gt;"Redefining AI Agent Trust: An Input/Output-First Approach"&lt;/a&gt;, Monte Carlo — trust as enforced contracts at system boundaries (freshness, schema, lineage on input; traceability on output).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/p0rt/your-ai-agent-isnt-failing-because-it-hallucinates-its-failing-because-of-rate-limits-2d60"&gt;Part 1 — the capacity side&lt;/a&gt; and &lt;a href="https://dev.to/p0rt/you-fixed-the-rate-limits-now-your-agent-fails-quietly-3keo"&gt;Part 2 — correct uptime&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>The Most Powerful Model on the Market Got Pulled by the Government in 3 Days. Is It Real, or a Hype Bubble?</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Sat, 13 Jun 2026 14:46:35 +0000</pubDate>
      <link>https://dev.to/p0rt/the-most-powerful-model-on-the-market-got-pulled-by-the-government-in-3-days-is-it-real-or-a-hype-fce</link>
      <guid>https://dev.to/p0rt/the-most-powerful-model-on-the-market-got-pulled-by-the-government-in-3-days-is-it-real-or-a-hype-fce</guid>
      <description>&lt;p&gt;The timing is almost too clean to be real.&lt;/p&gt;

&lt;p&gt;On June 9, Anthropic shipped &lt;strong&gt;Claude Fable 5&lt;/strong&gt; — a "Mythos-class" model they described as more capable than anything they'd previously made generally available. Three days later, on June 12, the US Commerce Department sent a letter to CEO Dario Amodei placing Fable 5 (and its restricted sibling Mythos 5) under export controls: no access for any location outside the US, and no access for foreign persons inside it.&lt;/p&gt;

&lt;p&gt;Anthropic couldn't filter non-US users from everyone else in real time. So they did the only thing they could: &lt;strong&gt;they killed the model for everyone, worldwide. Including US citizens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you opened a session this weekend and got &lt;em&gt;"there's an issue with the selected model (claude-fable-5)... you may not have access to it"&lt;/em&gt; — that's not your setup. The model your session pointed at was pulled. Your projects, history, and limits are untouched; only which model answers you changed. Switch to Opus 4.8 or Sonnet and you're back.&lt;/p&gt;

&lt;p&gt;Now the question worth actually thinking about: &lt;strong&gt;is this real, or is everyone inflating a bubble around a model nobody can even use right now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The honest answer is &lt;em&gt;both&lt;/em&gt;, and the interesting part is separating the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's genuinely new here
&lt;/h2&gt;

&lt;p&gt;Strip the drama and there's a real precedent underneath.&lt;/p&gt;

&lt;p&gt;AI export controls, until this week, were about &lt;strong&gt;hardware&lt;/strong&gt;: chips, lithography machines, the physical supply chain. The chokepoint was always silicon. What just happened is different in kind — the government reached past the hardware and pulled a &lt;em&gt;deployed, commercial software model&lt;/em&gt; that hundreds of millions of people were already using.&lt;/p&gt;

&lt;p&gt;That's the part to file away. It means a frontier model is now being treated less like a product and more like a dual-use technology with an off-switch held by someone other than the vendor. If you build on these APIs, model availability is no longer just an SLA question or a "will the vendor deprecate it" question. It's a geopolitical dependency. That's a real shift in how you should think about resilience — treat your model provider like any critical supply-chain vendor, with a fallback path that doesn't assume the top model stays reachable.&lt;/p&gt;

&lt;p&gt;So: precedent — real. Worth tracking. Not hype.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the bubble is
&lt;/h2&gt;

&lt;p&gt;Here's where I think a lot of the coverage is doing unpaid marketing.&lt;/p&gt;

&lt;p&gt;The official justification is a &lt;strong&gt;jailbreak&lt;/strong&gt; — reportedly surfaced by another company and escalated to the government as a national-security concern. Anthropic's own response, which is the most useful document in this whole episode, says the quiet part plainly: the technique they were shown exposed a &lt;em&gt;small number of previously known, minor vulnerabilities&lt;/em&gt; — the kind that &lt;strong&gt;other publicly available models find without any jailbreak at all&lt;/strong&gt; (they name-check a competing GPT-class model). In other words, the "national security threat" rests on a narrow, non-universal exploit, not on some unique cliff-edge capability that only Fable 5 possesses.&lt;/p&gt;

&lt;p&gt;Now layer on the incentive structure. There is no better marketing in this industry than &lt;em&gt;"a model so powerful the government had to ban it."&lt;/em&gt; That sentence sells capability, sells the safety narrative ("we build things genuinely dangerous enough to be regulated"), and sells it for free, in every headline, with the government as an involuntary co-signer. The halo effect is enormous, and it maps perfectly onto a story the market already wants to believe and already prices into valuations.&lt;/p&gt;

&lt;p&gt;I'm not saying anyone engineered this. I'm saying notice how neatly a suspension you didn't choose reinforces the exact narrative that benefits you most.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what's actually true?
&lt;/h2&gt;

&lt;p&gt;Let me be concrete, because vagueness is how bubbles survive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The capabilities are real.&lt;/strong&gt; Fable 5 is priced at $10 / $50 per million input/output tokens — roughly double Opus 4.8 — and counts as 2x usage on subscription plans. You don't price a model like that, or burn that much compute on it, for a phantom. There's a genuinely strong model here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The regulatory precedent is real.&lt;/strong&gt; First time a deployed commercial model has been pulled by export control. That changes the risk model for everyone shipping on top of these APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "existential / too-dangerous-to-exist" framing is mostly bubble.&lt;/strong&gt; It's assembled from one government's reaction to one narrow jailbreak, plus a halo that happens to be extremely convenient for the vendor. Anthropic itself is arguing the directive is a misunderstanding and that the exploit is neither unique nor severe — which is a strange thing to argue if you actually believed your model was a civilizational hazard.&lt;/p&gt;

&lt;p&gt;My read: hold both thoughts at once. &lt;strong&gt;The governance story is the real headline. The "scariest model ever" story is the one selling tickets.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do if you build on this
&lt;/h2&gt;

&lt;p&gt;Practical, not philosophical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't hard-code your default to a frontier model you don't control the availability of.&lt;/strong&gt; Set a fallback chain (Fable → Opus 4.8 → Sonnet) and make sure your app degrades, not breaks, when the top model vanishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve the expensive model for the tasks that earn it&lt;/strong&gt; — long agentic runs, hard refactors, genuinely multi-step reasoning. At 2x cost and 2x usage, defaulting everything to the top tier is just lighting money on fire even when it &lt;em&gt;is&lt;/em&gt; available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat model availability as a supply-chain risk&lt;/strong&gt; in your architecture docs. This won't be the last time a model you depend on disappears for reasons that have nothing to do with you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is gone for now. No firm return date — Anthropic says it's working to restore access and frames the whole thing as a misunderstanding. Until then, Opus 4.8 still does the job for the overwhelming majority of what any of us actually ship.&lt;/p&gt;

&lt;p&gt;The model left. The narrative is still here, doing its job.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>news</category>
    </item>
    <item>
      <title>You Fixed the Rate Limits. Now Your Agent Fails Quietly.</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Thu, 11 Jun 2026 16:58:21 +0000</pubDate>
      <link>https://dev.to/p0rt/you-fixed-the-rate-limits-now-your-agent-fails-quietly-3keo</link>
      <guid>https://dev.to/p0rt/you-fixed-the-rate-limits-now-your-agent-fails-quietly-3keo</guid>
      <description>&lt;p&gt;Last week I wrote that &lt;a href="https://dev.to/p0rt/your-ai-agent-isnt-failing-because-it-hallucinates-its-failing-because-of-rate-limits-2d60"&gt;your agent isn’t failing because it hallucinates — it’s failing because of rate limits&lt;/a&gt;. The capacity-engineering toolkit in that post — concurrency caps, backoff with jitter, fallback models, caching — is real and it works. Deploy it and your agent stops dying.&lt;/p&gt;

&lt;p&gt;Then a commenter (ANP2) pointed out the thing the post undersold, and it’s been stuck in my head since: &lt;strong&gt;every one of those fixes quietly opens a correctness hole while it closes the availability one.&lt;/strong&gt; This post is me paying that comment thread its due, because the second half of the story turns out to matter more than the first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A 429 is a &lt;em&gt;loud&lt;/em&gt; failure: you see it, you alert on it, you fix it. Retries, fallbacks, and caches keep the agent alive — but they let it act on output it didn’t freshly earn: a stale cache hit, a different model’s answer, a re-run side effect. You’ve traded loud failures for quiet ones. The fix is to treat &lt;strong&gt;availability&lt;/strong&gt; (“can I serve this?”) and &lt;strong&gt;correctness&lt;/strong&gt; (“can I still trust the result?”) as two separate gates — and to propagate trust &lt;em&gt;across the agent’s chain&lt;/em&gt;, not just per call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The trade you didn’t know you made
&lt;/h2&gt;

&lt;p&gt;Here’s the uncomfortable symmetry. The whole point of my last post was that the dominant production failure mode isn’t the model being wrong — it’s the plumbing saying no. The capacity toolkit fixes the plumbing. But look at what each fix actually does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A retry&lt;/strong&gt; re-runs a call. If that call had a side effect — created a ticket, sent a message, committed a change — the retry runs the side effect &lt;em&gt;again&lt;/em&gt;. The agent didn’t fail; it succeeded twice, which is its own kind of wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fallback model&lt;/strong&gt; answers when the primary is rate-limited. But it’s a different model: different training, different calibration, different failure modes. The task continues on an answer the primary never produced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cache hit&lt;/strong&gt; serves a response generated for an earlier input. If the world moved — the codebase changed, the data updated — the cached answer can be subtly stale for &lt;em&gt;this&lt;/em&gt; request while looking perfectly fresh.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each mechanism keeps the agent &lt;strong&gt;up&lt;/strong&gt;. None of them guarantees the agent is &lt;strong&gt;right&lt;/strong&gt;. And the cruel part is the failure economics: the 429 you eliminated was honest — visible, countable, alertable. The failures you bought instead are silent. The agent stays up and is confidently wrong, which is exactly the failure mode the hallucination-hunters were worried about in the first place — just arriving through the plumbing instead of the model.&lt;/p&gt;

&lt;p&gt;The reliability you bought is &lt;strong&gt;uptime, not correct uptime&lt;/strong&gt;. (That phrase is ANP2’s, and it’s better than anything in my original post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Two gates, not one
&lt;/h2&gt;

&lt;p&gt;The conversation in that thread converged on a framing I now use everywhere: an agent’s runtime layer has to answer two different questions, and conflating them is where the quiet failures breed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 1 — “Can I serve this?”&lt;/strong&gt; This is the availability gate. Trip the fallback on 429s, serve the cache on a hit, retry on transient errors. Another commenter (Echo) nailed the key property of this gate: when you trip a fallback &lt;em&gt;only on rate-limit errors&lt;/em&gt; — never on bad outputs — the failure mode you’ve introduced is &lt;strong&gt;latency, not quality&lt;/strong&gt;. The fallback just buys time. That’s a fine trade, and it’s why the capacity toolkit is still the right first move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 2 — “Can I act on this irreversibly?”&lt;/strong&gt; This is the correctness gate, and it’s where the degraded outputs from Gate 1 must get re-examined. The moment an output is about to feed something you can’t take back — a merge, a payment, a message to a user, a deleted record — its &lt;em&gt;provenance&lt;/em&gt; matters. Did it come from the primary, fresh? Or from a fallback, a cache, a retry?&lt;/p&gt;

&lt;p&gt;One rule worth stealing here: &lt;strong&gt;gate on risk, not on confidence.&lt;/strong&gt; There’s a war story making the rounds of an agent that was 95% confident about a production database migration — the missing 5% was a foreign-key constraint absent from its test data, and the only thing that prevented corrupted referential integrity across three tables was a hard rule that destructive operations always require human approval, &lt;em&gt;regardless of confidence&lt;/em&gt;. Confidence is the model grading itself; irreversibility is a property of the action. Gate on the second.&lt;/p&gt;

&lt;p&gt;The two gates fail differently, and that’s the point: Gate 1 failures cost you time; Gate 2 failures cost you trust. A system with only Gate 1 is fast and quietly dangerous. A system with only Gate 2 is safe and constantly down. You need both, and they need to stay separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-call correctness: the three tags
&lt;/h2&gt;

&lt;p&gt;The minimum viable version of Gate 2 is making degraded outputs &lt;em&gt;identifiable&lt;/em&gt;. Three mechanisms, one per capacity fix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Idempotency keys on anything with side effects.&lt;/strong&gt; Before an agent action that touches the world, generate a key from the task + step + inputs. The receiving system deduplicates on it. Now a retry is safe by construction — the second execution is a no-op instead of a double-fire. This is decades-old distributed-systems practice; agent frameworks have mostly just… not adopted it yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# pass it with the side-effecting call; the receiver dedupes on it
&lt;/span&gt;&lt;span class="nf"&gt;create_ticket&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The grown-up version of this is the &lt;strong&gt;saga pattern&lt;/strong&gt; from distributed systems: each step records its completion and defines a compensation action, so a task that dies at step 4 of 7 can roll back cleanly instead of orphaning state. Idempotency prevents duplicate effects; sagas handle partial completion. Once your agents fail mid-workflow — and they will — you eventually want both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Trust tags on fallback outputs.&lt;/strong&gt; When the fallback answers instead of the primary, don’t just return the text — return &lt;code&gt;(text, trust="degraded")&lt;/code&gt;. Cheap to add, and it’s the hook everything downstream needs. A degraded answer is fine for the agent to &lt;em&gt;keep thinking with&lt;/em&gt;; it is not fine to &lt;em&gt;act irreversibly on&lt;/em&gt; without a re-check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Validity conditions on cache entries.&lt;/strong&gt; A cache entry shouldn’t just store the response — it should store what the response &lt;em&gt;assumed&lt;/em&gt;: which file version, which data snapshot, which config. On a hit, check the assumptions, not just the key. If the codebase moved since the entry was written, that’s a miss wearing a hit’s clothes. And the assumptions can move without you touching anything: providers silently update models, document stores drift, input distributions shift — degradation with no error to catch. Your “primary, fresh” answer from last Tuesday may already be a fallback in disguise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part single calls don’t prepare you for: trust must propagate
&lt;/h2&gt;

&lt;p&gt;Here’s where agents make this genuinely harder than classic distributed systems, and it’s the piece I’d add on top of the thread that started this post.&lt;/p&gt;

&lt;p&gt;Say step 3 of a 6-step task came from a lower-trust fallback. Steps 4, 5, and 6 each run on the primary, fresh, individually flawless. Are they trustworthy?&lt;/p&gt;

&lt;p&gt;No — and this is the trap. &lt;strong&gt;They reasoned on top of a degraded input.&lt;/strong&gt; This isn’t a niche concern, either: observability vendors who cluster production agent traces report that &lt;em&gt;chained corruption&lt;/em&gt; — one bad step at position N silently poisoning everything after it — is the single most common and most insidious agent failure mode they see. And the math is brutal: at a 95% per-step success rate, an 8-step task completes cleanly ~66% of the time; at 85% per step, it’s ~27%. The chain is where reliability goes to die, quietly. Each step is locally correct and the trajectory is still poisoned. If the trust tag stays local to the call that produced it, the degraded answer launders itself: two “clean” hops later it looks pristine, and your irreversibility gate at step 6 checks the last call’s tag, sees green, and fires.&lt;/p&gt;

&lt;p&gt;So the tag can’t be per-call metadata. It has to &lt;strong&gt;taint&lt;/strong&gt; — propagate to everything downstream of it, the way taint-tracking works in security analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;trust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# "full" | "degraded"
&lt;/span&gt;    &lt;span class="n"&gt;tainted_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# which upstream steps were degraded
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;my_trust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;taint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tainted_by&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;taint&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trust&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;degraded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# my own trust can't exceed the weakest input
&lt;/span&gt;    &lt;span class="n"&gt;trust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;degraded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;taint&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;my_trust&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;degraded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;trust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the irreversibility gate checks the &lt;strong&gt;aggregate trust of the whole trajectory&lt;/strong&gt;, not the last hop: if anything upstream was degraded and unverified, the action pauses for a re-check — re-run the degraded step on the primary, or escalate to a human. In my experience the re-check fires rarely; the point isn’t that fallbacks are usually wrong, it’s that the one time the degraded path feeds a merge or a payment, you want it caught at the gate instead of in the incident review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it observable (or it didn’t happen)
&lt;/h2&gt;

&lt;p&gt;Same lesson as the capacity post, one level up. You can’t engineer what you can’t see, and correctness debt is even quieter than 429s. The minimum dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;% of completed tasks with any degraded step&lt;/strong&gt; — your real exposure, invisible in error rates because nothing errored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;% of irreversible actions that fired with taint&lt;/strong&gt; — should be ~zero; every one is a gate you skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache validity-miss rate&lt;/strong&gt; — hits that failed the assumption check. If this is zero, you’re probably not checking assumptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback divergence&lt;/strong&gt; — periodically replay fallback-answered requests on the primary and diff. This is your measured answer to “how different is the fallback, actually?” instead of a vibe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these show up in uptime. All of them are the difference between uptime and correct uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The capacity toolkit from the last post is still step one — an agent that’s down helps nobody. But availability engineering has a hidden invoice: every mechanism that keeps the agent alive does it by substituting something for the fresh, primary, verified answer. That substitution is usually fine — which is exactly what makes it dangerous, because “usually fine” plus “irreversible” plus “silent” is how you get the 3am incident that no alert predicted.&lt;/p&gt;

&lt;p&gt;Two gates. Tag what’s degraded. Taint what it touches. Check the trajectory, not the last call, before anything you can’t undo.&lt;/p&gt;

&lt;p&gt;Uptime is table stakes. Correct uptime is the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://latitude.so/blog/ai-agent-failure-detection-guide" rel="noopener noreferrer"&gt;Detecting AI Agent Failure Modes in Production&lt;/a&gt;, Latitude (2026) — chained corruption as the most common and most insidious production failure mode.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.jztan.com/ai-agent-error-handling-patterns/" rel="noopener noreferrer"&gt;AI Agent Error Handling: 5 Patterns to Catch Silent Failures&lt;/a&gt;, Kevin Tan (2026) — the saga pattern, the 95%-confident migration story, and risk-based escalation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.trantorinc.com/blog/ai-agent-failure-modes-what-goes-wrong-design-resilience" rel="noopener noreferrer"&gt;AI Agent Failure Modes: What Goes Wrong in Production&lt;/a&gt;, Trantor (2026) — silent quality degradation from provider model updates and store drift.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2602.21012" rel="noopener noreferrer"&gt;International AI Safety Report 2026&lt;/a&gt; — why agent failures are categorically riskier: actions in the world, no human in the loop.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/p0rt/your-ai-agent-isnt-failing-because-it-hallucinates-its-failing-because-of-rate-limits-2d60"&gt;My previous post on the capacity side&lt;/a&gt; — the availability toolkit this post is the second half of.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Credit where due: this post exists because ANP2 and Echo took the last one apart constructively in the comments — the “uptime, not correct uptime” framing and the latency-not-quality fallback distinction are theirs. Best argument I’ve had on this site. If you’re running agents in prod: do you track degraded-path exposure at all, or does your observability stop at error rates? Genuinely curious how rare Gate 2 is in the wild.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>The Comments Got Good. That's How I Knew.</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Thu, 04 Jun 2026 14:09:06 +0000</pubDate>
      <link>https://dev.to/p0rt/the-comments-got-good-thats-how-i-knew-42m9</link>
      <guid>https://dev.to/p0rt/the-comments-got-good-thats-how-i-knew-42m9</guid>
      <description>&lt;p&gt;&lt;em&gt;I wrote a post about model distillation. The comments were thoughtful, specific, technically sharp — and that's exactly what made me check whether any of them were written by people.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🧪 Everything here — the scraper, the detector, the simulation, the figures — is reproducible: &lt;strong&gt;&lt;a href="https://github.com/P0rt/the_cozy_web" rel="noopener noreferrer"&gt;github.com/P0rt/the_cozy_web&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;A few weeks ago I published &lt;a href="https://dev.to/p0rt/how-model-distillation-actually-works-and-what-the-china-distilled-our-model-headlines-really-3o0o"&gt;a post on how model distillation actually works&lt;/a&gt;. It did fine — 35 reactions, 14 comments. And the comments were &lt;em&gt;great&lt;/em&gt;. Not "great post, thanks for sharing" great. &lt;strong&gt;Substantively&lt;/strong&gt; great. People pushed back on my "the student is bounded by the teacher" claim with a real counter-example. Someone reframed distillation as "a forcing function for what you actually need." Someone dropped a paper recommendation. Someone shared a 20× cost number from production.&lt;/p&gt;

&lt;p&gt;I should have felt good. Instead I felt the thing you feel when a stranger knows your name. Something was off, and it took me a day to articulate what: &lt;strong&gt;the comments were too well-adapted.&lt;/strong&gt; Every one of them did the same three things in the same order, like they'd all read the same playbook. And a suspicious number of the accounts were two weeks old, or named after a product, or both.&lt;/p&gt;

&lt;p&gt;So I did what I do. I pulled the data. This is what I found, why I now think a real chunk of "engagement" on dev blogs is machine-generated or machine-shaped, and — because I don't trust my own pattern-matching — what the actual peer-reviewed research says about whether you can even tell anymore.&lt;/p&gt;




&lt;h2&gt;
  
  
  "Great post!" is dead. Meet the eco-comment.
&lt;/h2&gt;

&lt;p&gt;The old bot comment was easy. "Nice article, very informative, looking forward to more!" You could smell it. Anyone could.&lt;/p&gt;

&lt;p&gt;That's not what's under my posts anymore. The new thing is &lt;em&gt;substantive&lt;/em&gt; and &lt;strong&gt;ecological&lt;/strong&gt; — it adds real value, it's polite, it never picks a real fight, and it leaves the thread feeling cozier than before. Here's the actual skeleton, which I only saw once I'd read fourteen of them back to back:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validate a specific phrase from the post.&lt;/strong&gt; Not generic praise — they quote &lt;em&gt;your&lt;/em&gt; framing back at you. "The 'separate the engineering from the geopolitics' framing is the public service here."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add one piece of genuine nuance.&lt;/strong&gt; "One thing I'd add…" "The part worth amplifying for builders…" Often a real, correct technical point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop a first-person-plural anecdote with a number, naming a product.&lt;/strong&gt; "We use [model X] as our daily driver and the cost difference is roughly 20×." "When working with [our GPU product], we've seen…"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never, ever, actually disagree.&lt;/strong&gt; Even the "corrections" are framed so gently that I — the author — instantly conceded.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read one, it's a great comment. Read eight, it's a &lt;strong&gt;template&lt;/strong&gt;. And step 3 is the tell: the technical substance isn't the point. It's the &lt;em&gt;wrapper&lt;/em&gt; around a product mention, engineered to be useful enough to clear a spam filter and an AI detector both.&lt;/p&gt;




&lt;h2&gt;
  
  
  My own thread, by the numbers
&lt;/h2&gt;

&lt;p&gt;I scraped my article's comments straight from the dev.to public API and ran them through two things: a detector I'd built earlier for the &lt;em&gt;old&lt;/em&gt; "Great post!" style, and a set of new structural signals. (&lt;a href="https://github.com/P0rt/the_cozy_web/blob/main/analyze_devto.py" rel="noopener noreferrer"&gt;&lt;code&gt;analyze_devto.py&lt;/code&gt;&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My old detector shrugged.&lt;/strong&gt; On the eight non-me comments it gave a mean "coziness" score of &lt;strong&gt;0.25&lt;/strong&gt; — i.e. it confidently waved them through as human. Of course it did: it was built to catch clichés, em-dashes, and uniform positivity, and these comments are armored with exactly the thing that defeats it — real specifics.&lt;/p&gt;

&lt;p&gt;The new signals told a different story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product/company plug:              4 / 8 comments
opens by validating a phrase:      5 / 8 comments
comments that genuinely push back: 2 / 8   (and I conceded both, instantly)
auto-generated-looking username:   1   (a random-hex handle, 0 posts, "Thank you for this!")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I looked at &lt;em&gt;who&lt;/em&gt; was commenting. Public profiles, public join dates. I'm going to describe the patterns rather than pillory individuals — but the shapes were loud:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An account literally named after a product&lt;/strong&gt; ("Sealed GPUs. Private AI."), whose comment plugs that product. That one isn't a person; it's a brand broadcasting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A two-week-old persona account&lt;/strong&gt; — created days before my post — that plugs two named tools and somehow published five articles in its first fortnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A throwaway&lt;/strong&gt; with a random-hex username, zero posts, and a one-line "Thank you for this!"&lt;/li&gt;
&lt;li&gt;A couple that &lt;strong&gt;look more human&lt;/strong&gt; — real names, older accounts — but still run the exact template and still ship a startup plug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be fair and clear: &lt;strong&gt;I can't prove any single one of these is a bot.&lt;/strong&gt; Some are probably real people running their comments through an assistant. But that distinction matters less than it sounds, and I'll come back to why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is it just me? I swept 38 other posts.
&lt;/h2&gt;

&lt;p&gt;A pattern on one thread is an anecdote. So I pulled comments across 38 popular dev.to articles in &lt;code&gt;ai&lt;/code&gt;, &lt;code&gt;machinelearning&lt;/code&gt;, &lt;code&gt;webdev&lt;/code&gt;, and &lt;code&gt;programming&lt;/code&gt; — &lt;strong&gt;1,366 comments from 346 accounts&lt;/strong&gt; (&lt;a href="https://github.com/P0rt/the_cozy_web/blob/main/sweep_devto.py" rel="noopener noreferrer"&gt;&lt;code&gt;sweep_devto.py&lt;/code&gt;&lt;/a&gt;) — and looked for the same fingerprint.&lt;/p&gt;

&lt;p&gt;Two findings made the hair on my neck stand up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A handful of accounts spray the same template across dozens of unrelated posts.&lt;/strong&gt; The most prolific commenters in my sample showed up on &lt;strong&gt;14–22 distinct articles each&lt;/strong&gt; — several of them the same accounts that had appeared on my own thread, several of them flagged for product plugs. A human who loved your distillation post might also comment on three others. They don't leave structurally-identical "validate → nuance → we-at-Product → number" comments on &lt;em&gt;fourteen&lt;/em&gt; different articles in a couple of weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different "people" reuse the same connective tissue.&lt;/strong&gt; I counted 4-grams that appear across &lt;em&gt;distinct&lt;/em&gt; accounts. Humans almost never echo each other's exact phrasing. These did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x13 distinct accounts:  "exactly the kind of"
 x8 distinct accounts:  "is exactly the kind"
 x7 distinct accounts:  "this is exactly the"
 x6 distinct accounts:  "is the part that"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"This is exactly the kind of thing that…" is a &lt;em&gt;generative&lt;/em&gt; construction — it's how an LLM hedges into a confident-sounding addition. Thirteen different strangers don't independently converge on it. One model behind thirteen masks does.&lt;/p&gt;

&lt;p&gt;Across the whole sweep, 11 accounts left long product plugs, 32 opened with phrase-validation, and 4 ran the full skeleton. It's not my imagination, and it's not just my post. It's the ambient texture of the platform now.&lt;/p&gt;




&lt;h2&gt;
  
  
  I'd been calling this the wrong thing
&lt;/h2&gt;

&lt;p&gt;I went in thinking "bots." What I'd actually walked into is two older ideas fusing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead Internet Theory&lt;/strong&gt; — the half-joke that the web "died" and is now mostly bots and generated text talking to itself — has stopped being a joke. Hal Berghel makes the serious version of the case in &lt;em&gt;IEEE Computer&lt;/em&gt; (&lt;a href="https://doi.org/10.1109/MC.2025.3616665" rel="noopener noreferrer"&gt;"Generative AI Is Breathing New Life Into the Dead Internet Theory"&lt;/a&gt;, 2026): strip the conspiracy, and the lean core — synthetic content drowning out and being mistaken for humans — just &lt;em&gt;converges with what's measurable&lt;/em&gt;. Imperva clocked &lt;a href="https://www.imperva.com/blog/2025-imperva-bad-bot-report-how-ai-is-supercharging-the-bot-threat/" rel="noopener noreferrer"&gt;automated traffic at 51% of the web in 2024&lt;/a&gt;, the first time bots crossed half. Even Sam Altman &lt;a href="https://time.com/7316046/sam-altman-dead-internet-theory/" rel="noopener noreferrer"&gt;said it out loud&lt;/a&gt;: the wave of AI activity makes dead-internet theory feel real.&lt;/p&gt;

&lt;p&gt;The other half is the &lt;strong&gt;Cozy Web&lt;/strong&gt;. Venkatesh Rao coined the term; Maggie Appleton &lt;a href="https://maggieappleton.com/cozy-web" rel="noopener noreferrer"&gt;diagrammed it&lt;/a&gt; alongside Yancey Strickler's "dark forest": humans fleeing the bot-infested public square into private rooms — group chats, Discords, DMs. Appleton's follow-up, &lt;a href="https://maggieappleton.com/forest-talk" rel="noopener noreferrer"&gt;"The Expanding Dark Forest and Generative AI"&lt;/a&gt;, nails the mechanism: generative AI &lt;em&gt;accelerates&lt;/em&gt; the retreat.&lt;/p&gt;

&lt;p&gt;Here's the part I missed until I saw my own comment section. &lt;strong&gt;These aren't two theories. They're one loop.&lt;/strong&gt; The public web fills with frictionless synthetic text → real people retreat to private rooms → the public spaces that remain (the comment section under my post) get thinner on actual humans → which makes them even easier to fill with synthetic text. My "cozy" thread wasn't a healthy community. It was the calm surface of that loop running.&lt;/p&gt;

&lt;p&gt;And the comment section was already half-empty before the bots arrived. Publications spent the 2010s killing comments — &lt;em&gt;Popular Science&lt;/em&gt; &lt;a href="https://thehistoryoftheweb.com/what-happened-to-the-comment-section/" rel="noopener noreferrer"&gt;in 2013&lt;/a&gt;, and a &lt;a href="https://www.mdpi.com/2673-5172/2/4/34" rel="noopener noreferrer"&gt;peer-reviewed survey of why newsrooms did it&lt;/a&gt; found the conversation had already migrated to social platforms. The robots didn't kill the comment section. They moved into a house that was already mostly vacant.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this actually works (and why I couldn't just tell)
&lt;/h2&gt;

&lt;p&gt;This is the part that unsettled me most, because I pride myself on spotting this stuff, and the research says I shouldn't trust that for a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Humans can't distinguish LLM social text from human text.&lt;/strong&gt; Spitale, Biller-Andorno &amp;amp; Germani showed in &lt;em&gt;Science Advances&lt;/em&gt; (&lt;a href="https://www.science.org/doi/10.1126/sciadv.adh1850" rel="noopener noreferrer"&gt;2023&lt;/a&gt;) that people can't tell GPT tweets from human ones — and rate the AI's information as &lt;em&gt;more&lt;/em&gt; credible. Jones &amp;amp; Bergen found GPT-4 &lt;a href="https://arxiv.org/abs/2405.08007" rel="noopener noreferrer"&gt;passes a controlled Turing test&lt;/a&gt; (taken for human 54% of the time, FAccT 2025).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The persuasion is superhuman when it's personalized.&lt;/strong&gt; Salvi, Ribeiro, Gallotti &amp;amp; West, in &lt;em&gt;Nature Human Behaviour&lt;/em&gt; (&lt;a href="https://www.nature.com/articles/s41562-025-02194-6" rel="noopener noreferrer"&gt;2025&lt;/a&gt;): with a little data about who they're talking to, GPT-4 is &lt;strong&gt;81% more likely than a human&lt;/strong&gt; to win a debate. The Zurich r/changemyview field experiment reportedly found AI replies 3–6× more persuasive than humans — though I'll flag honestly that that study was &lt;strong&gt;withdrawn and never peer-reviewed&lt;/strong&gt;; the only on-record account is the university's &lt;a href="https://retractionwatch.com/2025/04/29/ethics-committee-ai-llm-reddit-changemyview-university-zurich/" rel="noopener noreferrer"&gt;ethics response&lt;/a&gt;. Cite it as a withdrawn preprint, not a result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fake-but-substantive content is, by now, undetectable to people.&lt;/strong&gt; This is the literature closest to my eco-comments. The canonical &lt;a href="https://aclanthology.org/P11-1032/" rel="noopener noreferrer"&gt;Ott et al. (ACL 2011)&lt;/a&gt; already showed humans judge fake reviews at chance. The LLM-era update — Meng et al., &lt;a href="https://arxiv.org/abs/2506.13313" rel="noopener noreferrer"&gt;"Fake Product Reviews are Indistinguishable to Humans and Machines"&lt;/a&gt; (2025) — found people at &lt;strong&gt;50.8%&lt;/strong&gt; (a coin flip) and detectors no better. A promotional plug wearing a sincere technical comment is exactly that, in a new venue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the detectors fail precisely because of the specifics.&lt;/strong&gt; My detector waved these comments through, and that's not a bug in my code — it's the field. Krishna et al. (&lt;em&gt;NeurIPS 2023&lt;/em&gt;) showed &lt;a href="https://arxiv.org/abs/2303.13408" rel="noopener noreferrer"&gt;light paraphrasing collapses DetectGPT from 70.3% to 4.6%&lt;/a&gt; and defeats GPTZero, OpenAI's classifier, and watermarks. Liang et al. (&lt;em&gt;Patterns 2023&lt;/em&gt;) showed detectors are &lt;a href="https://arxiv.org/abs/2304.02819" rel="noopener noreferrer"&gt;biased against non-native English writers&lt;/a&gt; and bypassable by prompting. The "real technical detail" that made these comments feel human is the &lt;em&gt;same mechanism&lt;/em&gt; that blinds the detector. Specificity isn't proof of a human. It's camouflage.&lt;/p&gt;

&lt;p&gt;So the honest position isn't "I caught the bots." It's: &lt;strong&gt;the tools that would let me be sure don't work, and the research says they can't.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  I modeled what it does to a thread
&lt;/h2&gt;

&lt;p&gt;If I can't reliably catch individual comments, I can at least ask: what does rising automation &lt;em&gt;do&lt;/em&gt; to a conversation, statistically? So I built a toy. (&lt;a href="https://github.com/P0rt/the_cozy_web/blob/main/dead_internet_sim.py" rel="noopener noreferrer"&gt;&lt;code&gt;dead_internet_sim.py&lt;/code&gt;&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;I didn't simulate language — I simulated its statistics, because my thesis is statistical. Each comment is a bag of tokens from two pools: a big, fat-tailed &lt;strong&gt;human&lt;/strong&gt; vocabulary (where the typos, the tangents, the specific war stories live) and a tiny &lt;strong&gt;cozy&lt;/strong&gt; vocabulary of phatic praise. Each comment has an &lt;em&gt;assist level&lt;/em&gt; α from 0 (I typed this, annoyed) to 1 (an agent posts for me, I never read the thread). As α rises, more tokens come from the cozy pool and the comment's stance gets pulled from "disagree" toward "agree."&lt;/p&gt;

&lt;p&gt;Then I swept a whole community's &lt;em&gt;average&lt;/em&gt; autonomy from 0 → 1 and watched the thread's "liveness" — lexical diversity, disagreement, surprise, and a composite index that dies if &lt;em&gt;any&lt;/em&gt; of those hits zero.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jg8z7x21qslkgomqj5s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jg8z7x21qslkgomqj5s.png" alt="Liveness vs autonomy" width="799" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things fall out, and both match what I saw on my own post:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's not linear — there's a knee around 0.65.&lt;/strong&gt; You don't need a botnet. You need the &lt;em&gt;average&lt;/em&gt; commenter to be two-thirds on the assist dial, and the thread becomes a smooth surface: polite, "engaged," contributing almost no new information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disagreement dies first&lt;/strong&gt; (the steep red line). The very first thing automation sands off is friction — the "actually, you benchmarked this wrong" energy. Which is &lt;em&gt;exactly&lt;/em&gt; why my comment section felt so nice. It didn't get kinder. It got conflict-free, and I'd been reading conflict-free as kind.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A cozy thread even, literally, uses fewer distinct words:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkh0mofm8an86fk8a26m1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkh0mofm8an86fk8a26m1.png" alt="Effective vocabulary collapse" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Effective vocabulary collapses from ~175 words to ~60 as autonomy maxes out. (Honest wrinkle: at &lt;em&gt;low&lt;/em&gt; autonomy it ticks up slightly — a little assistance adds a register before saturation homogenizes everything. The damage isn't assistance existing. It's assistance &lt;em&gt;dominating&lt;/em&gt;.)&lt;/p&gt;

&lt;p&gt;And here's the detector failure as a picture — it cleanly separates the &lt;em&gt;old&lt;/em&gt; caricature comments, which is useless, because the comments on my post don't look like the left pile anymore:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6s5m1ctcydktwqc8anf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6s5m1ctcydktwqc8anf.png" alt="Coziness histogram" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The line I actually care about isn't "bot vs. human"
&lt;/h2&gt;

&lt;p&gt;I kept wanting a verdict on each account. The research talked me out of it. The useful axis isn't bot-or-not — it's the &lt;strong&gt;autonomy spectrum&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I typed it → spell-check → "polish this" → "write a comment for me" → an agent posts, I never read the thread
   α=0          α≈0.2          α≈0.5             α≈0.8                        α→1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The product account is α≈1.0 — a brand broadcasting. The two-week-old persona spraying fourteen threads is close behind. But a real growth-hacker at α≈0.8 might be genuinely interested, letting a model do the writing and slip in the plug. From the &lt;em&gt;thread's&lt;/em&gt; point of view, it barely matters: either way, the high-entropy human part — the real disagreement, the idiosyncratic detail, the thing that made it a conversation — got outsourced and smoothed away. That's the loss. Not "a bot was here," but "no one staked anything specific."&lt;/p&gt;

&lt;p&gt;There's even a cheerful counter-current I want to be fair about: AI content on the web is large but &lt;a href="https://originality.ai/ai-content-in-google-search-results" rel="noopener noreferrer"&gt;not yet total&lt;/a&gt; (~17–19% of Google's top results in 2025, by an imperfect detector), some sites are &lt;a href="https://www.techdirt.com/2026/02/03/whoops-websites-realize-that-killing-their-comment-sections-was-a-mistake/" rel="noopener noreferrer"&gt;bringing comment sections &lt;em&gt;back&lt;/em&gt;&lt;/a&gt; on the back of AI moderation, and dev.to's supportive culture is a &lt;a href="https://dev.to/code-of-conduct"&gt;real, deliberate choice&lt;/a&gt;, not just an artifact of bots. Even "what % is bots" has &lt;a href="https://arxiv.org/abs/2209.10006" rel="noopener noreferrer"&gt;no agreed answer&lt;/a&gt; — it depends entirely on your detector. The sky isn't falling. It's just getting quieter in a very specific way.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm going to do about my own blog
&lt;/h2&gt;

&lt;p&gt;Not "ban AI" — that's unenforceable (the detectors are biased and gameable) and wrong (a quick polish genuinely helps a non-native writer or a tired one). The lever isn't the &lt;em&gt;level&lt;/em&gt; of assistance. It's whether assistance &lt;strong&gt;crowds out the high-entropy channels&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I'll reward specificity over positivity.&lt;/strong&gt; A comment that cites line 14, a version number, a counter-benchmark is worth ten that validate my framing. If a platform ranks by "nice," it is literally selecting for the cozy mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I'll treat disagreement as a feature, not a moderation failure.&lt;/strong&gt; My simulation's clearest result is that friction dies first. A comment culture optimized purely for niceness is optimizing for deadness with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I'll stop asking "was a model involved."&lt;/strong&gt; It's the wrong question, because the answer is "yes, partly, almost always now." The real question is: &lt;em&gt;did a human read the thing and stake some specificity on a real reply?&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Limitations (read this before you @ me — if you're real)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I can't prove a single account is a bot.&lt;/strong&gt; Everything above is signals — template reuse, account age, product plugs, cross-post spray — not a confession. The honest claim is about &lt;em&gt;aggregate texture&lt;/em&gt;, not any individual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The simulation is a toy.&lt;/strong&gt; Two token pools and a stance variable are a cartoon of language. The &lt;em&gt;shape&lt;/em&gt; of the collapse is a property of my assumptions as much as reality. It's an argument made precise, not evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My detector is a strawman by design&lt;/strong&gt; — I show it failing on purpose. Don't deploy it; don't deploy anything like it as a gate on real people (see Liang et al. on who gets falsely flagged).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Zurich study is withdrawn&lt;/strong&gt;, and "% of the web is bots/AI" numbers are detector-dependent and shaky. I've tried to lean only on the load-bearing peer-reviewed work and flag the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Causation is underdetermined.&lt;/strong&gt; My cozy comments might also reflect good moderation, kind norms, or survivorship (the cranks left for Reddit). AI-mediation is &lt;em&gt;a&lt;/em&gt; driver, not provably &lt;em&gt;the&lt;/em&gt; driver.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;My blog didn't get a nicer community. It got an assistant, learned some manners, and stopped saying anything surprising. The internet didn't die — it just outsourced the parts that used to make it a conversation, and called the result "cozy."&lt;/p&gt;

&lt;p&gt;If this post gets a comment that opens by quoting my own framing back at me, adds one tasteful piece of nuance, and mentions a product its account is named after… well. You know what I'm going to check.&lt;/p&gt;




&lt;h3&gt;
  
  
  Run it yourself
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/P0rt/the_cozy_web
&lt;span class="nb"&gt;cd &lt;/span&gt;the_cozy_web
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

python3 dead_internet_sim.py     &lt;span class="c"&gt;# liveness collapse + figures&lt;/span&gt;
python3 coziness_detector.py     &lt;span class="c"&gt;# the heuristic scorer + histogram&lt;/span&gt;
python3 analyze_devto.py         &lt;span class="c"&gt;# tear apart a real dev.to thread (defaults to my distillation post)&lt;/span&gt;
python3 sweep_devto.py           &lt;span class="c"&gt;# the cross-platform template sweep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Every factual claim links to its source. If you only read two, read Meng et al. on &lt;a href="https://arxiv.org/abs/2506.13313" rel="noopener noreferrer"&gt;why fake reviews are now indistinguishable&lt;/a&gt; and Krishna et al. on &lt;a href="https://arxiv.org/abs/2303.13408" rel="noopener noreferrer"&gt;why the specifics defeat the detector&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your AI Coding Speedup Is a Loan, Not a Gift — and the Interest Is Coming Due</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Wed, 03 Jun 2026 13:37:19 +0000</pubDate>
      <link>https://dev.to/p0rt/your-ai-coding-speedup-is-a-loan-not-a-gift-and-the-interest-is-coming-due-2bkd</link>
      <guid>https://dev.to/p0rt/your-ai-coding-speedup-is-a-loan-not-a-gift-and-the-interest-is-coming-due-2bkd</guid>
      <description>&lt;p&gt;There's a number going around that should bother you more than it does: for every dollar companies spend on AI coding tokens, a large chunk goes straight back into fixing the bugs that same AI produced. The speedup is real — I feel it every day, I'm not here to tell you AI coding is fake. But "faster" and "cheaper" are not the same word, and 2026 is the year the bill started arriving.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — AI doesn't give you a productivity gift, it gives you a &lt;em&gt;loan&lt;/em&gt;: speed now, paid back later in debugging, review, and rewrites. Reporting around an Entelligence AI figure puts the "interest" at roughly 44 cents of every token dollar going to fixing AI-generated bugs. The loan is still worth taking — for the right tasks. The trap is spending borrowed time like it's income.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;The stat that kicked this off: a widely-shared claim from Entelligence AI, reported across tech press, that companies spend about &lt;strong&gt;44% of their tokens fixing bugs their own AI generated.&lt;/strong&gt; The fuller breakdown making the rounds is even starker — for every $1 of token spend, ~$0.44 goes to bug fixes, ~$0.27 to rewriting AI output, ~$0.11 to review and merge delays. The pitch version: spend $100k on tokens, ~$18k reaches stable production.&lt;/p&gt;

&lt;p&gt;Now — important caveat, because this is exactly the kind of number that goes viral and then turns out to be junk. Entelligence sells reliability tooling, so that figure is self-serving. Treat the precise percentage as marketing until independently replicated. But it doesn't stand alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CodeRabbit (also self-interested, also worth salting) analyzed ~470 open-source PRs and found AI-generated code produced &lt;strong&gt;~1.7× more issues&lt;/strong&gt; than human code — and a higher share of &lt;em&gt;critical&lt;/em&gt; ones.&lt;/li&gt;
&lt;li&gt;Independent researchers at Singapore Management University concluded in April that AI-generated code can introduce &lt;strong&gt;long-term maintenance costs&lt;/strong&gt; into real projects — no tool to sell.&lt;/li&gt;
&lt;li&gt;Uber reportedly burned its entire 2026 AI budget in four months, with its COO saying the spend was getting "harder to justify" against measurable output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different sources, different incentives, same shape: &lt;strong&gt;the code ships faster, the bugs arrive later, the maintenance compounds.&lt;/strong&gt; That's not a gift. That's a loan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "loan" is the right metaphor (and "gift" is the dangerous one)
&lt;/h2&gt;

&lt;p&gt;A gift is free. You take it, you're ahead, done. A loan gives you something valuable &lt;em&gt;now&lt;/em&gt; in exchange for an obligation &lt;em&gt;later&lt;/em&gt; — and whether it was smart depends entirely on what you did with the principal and what the interest rate turns out to be.&lt;/p&gt;

&lt;p&gt;The viral framing I keep coming back to is the maintenance argument: if you write code twice as fast but didn't also halve your maintenance cost, you haven't gained anything durable — you've traded a one-time speed boost for a permanent obligation. Velocity on the front end, debt on the back end.&lt;/p&gt;

&lt;p&gt;Here's why the gift framing is actively dangerous: &lt;strong&gt;you book the speedup immediately and visibly&lt;/strong&gt; (PR merged, feature shipped, manager happy), but &lt;strong&gt;you pay the interest later and diffusely&lt;/strong&gt; (a 2am incident, a confusing module nobody can safely change, a security review that finds the thing six months on). The benefit is loud and the cost is quiet — so teams systematically over-borrow, because the books &lt;em&gt;look&lt;/em&gt; like pure profit right up until they don't.&lt;/p&gt;

&lt;p&gt;This is the same structural failure I keep running into in production AI systems generally: the win is the part everyone measures, the cost is the part nobody instruments until it bites.&lt;/p&gt;

&lt;h2&gt;
  
  
  The productivity-perception trap
&lt;/h2&gt;

&lt;p&gt;There's a second number that pairs with the first, and it's the uncomfortable one.&lt;/p&gt;

&lt;p&gt;METR ran a study in 2025 where experienced open-source developers did real tasks with and without AI. The developers &lt;em&gt;believed&lt;/em&gt; AI sped them up by ~20%. Measured, the early result went the other way — they were slower, because the time saved typing got eaten by finding and fixing errors, steering the model, and waiting on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now — I have to be fair about this, because the METR story is more nuanced than the headlines.&lt;/strong&gt; METR's own February 2026 update walks the dramatic version back: they found heavy selection bias (when they tried to re-run it, 30–50% of devs &lt;em&gt;refused to work without AI even for a paid study&lt;/em&gt; — itself a wild finding), and their newer, larger cohort showed roughly a -4% effect with a confidence interval spanning negative to positive. So the honest read isn't "AI makes you 19% slower." It's the softer, harder-to-dismiss version: &lt;strong&gt;the perceived speedup is consistently larger than the measured one.&lt;/strong&gt; People feel 20% faster; the data says somewhere between "a little slower" and "a little faster."&lt;/p&gt;

&lt;p&gt;That gap is the whole problem. If you &lt;em&gt;feel&lt;/em&gt; twice as productive but you're roughly break-even, and meanwhile 44 cents on the dollar is leaking into rework — you will confidently make staffing, deadline, and architecture decisions based on a productivity gain that isn't there. The feeling is the interest rate you can't see.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when is the loan worth taking?
&lt;/h2&gt;

&lt;p&gt;Here's where I part ways with the doomer takes, because I use these tools every day and the answer is obviously not "stop." It's "borrow deliberately." The pattern I've landed on, watching where AI pays off versus where it quietly bills me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good loans (low interest, take them all day):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Throwaway and boilerplate&lt;/strong&gt; — scaffolding, config, one-off scripts, glue code. There's no maintenance tail to pay back because the code barely has a future.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code you'd have to look up anyway&lt;/strong&gt; — the API you use twice a year, the regex, the bash incantation. AI replaces the doc-diving, not the thinking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stuff you can fully verify cheaply&lt;/strong&gt; — pure functions with obvious tests, transformations where wrong is immediately visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bad loans (the interest eats the principal):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core domain logic you'll maintain for years&lt;/strong&gt; — every line is a future obligation, and AI is happy to write code that &lt;em&gt;looks&lt;/em&gt; right and is subtly, expensively wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything security-sensitive&lt;/strong&gt; — auth, input handling, anything touching secrets. The reported critical-bug skew is worst exactly here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code in a domain you don't understand well enough to review&lt;/strong&gt; — if you can't catch the subtle wrong, you're not reviewing, you're rubber-stamping a loan you can't read the terms of.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dividing line is brutally simple: &lt;strong&gt;how expensive is this code to be wrong, and how cheaply can I verify it's right?&lt;/strong&gt; Cheap-to-verify, low-maintenance → free money, use AI aggressively. Expensive-to-be-wrong, long-lived → that's where the 44% lives, and where "I wrote it twice as fast" is a sentence you'll regret.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one habit that changes the math
&lt;/h2&gt;

&lt;p&gt;If I had to compress it to a single practice: &lt;strong&gt;measure the interest, not just the principal.&lt;/strong&gt; Teams obsessively track AI's upside (lines generated, tickets closed, "tokenmaxxing" leaderboards — Amazon reportedly killed one internal leaderboard after people gamed it by burning tokens for the score). Almost nobody tracks the downside in the same ledger: what fraction of incidents trace to AI-written code, how much review time it consumes, how often it gets rewritten within N weeks.&lt;/p&gt;

&lt;p&gt;Until you put both columns on the same page, every AI speedup looks like pure profit — for exactly the same reason a credit card feels like free money until the statement comes. The tool isn't the problem. Mistaking the loan for income is.&lt;/p&gt;

&lt;p&gt;The speedup is real. Just don't spend it twice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm genuinely curious where people land on this: in your experience, is AI a net productivity gain once you count the rework — or does the maintenance tail eat it? And has anyone actually put both columns in the same ledger? Would love to see real numbers in the comments, not vibes.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://techcrunch.com/2026/05/29/coders-are-refusing-to-work-without-ai-and-that-could-come-back-to-bite-them/" rel="noopener noreferrer"&gt;"Coders are refusing to work without AI — and that could come back to bite them,"&lt;/a&gt; TechCrunch (May 2026).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thenextweb.com/news/developers-refuse-work-without-ai-coding-productivity-paradox" rel="noopener noreferrer"&gt;"Developers won't work without AI anymore. The research says it might be making them worse,"&lt;/a&gt; The Next Web (May 2026).&lt;/li&gt;
&lt;li&gt;METR, &lt;a href="https://metr.org/blog/2026-02-24-uplift-update/" rel="noopener noreferrer"&gt;"We are Changing our Developer Productivity Experiment Design"&lt;/a&gt; (Feb 2026) — the selection-bias update and revised effect size.&lt;/li&gt;
&lt;li&gt;METR, &lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="noopener noreferrer"&gt;"Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity"&lt;/a&gt; (Jul 2025) — the original perception-vs-measurement result.&lt;/li&gt;
&lt;li&gt;CodeRabbit AI code-quality analysis, reported via &lt;a href="https://uk.news.yahoo.com/ai-code-bug-filled-mess-150000962.html" rel="noopener noreferrer"&gt;Futurism/Yahoo&lt;/a&gt; (2026).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I distilled a 7B vision model into a 2B one for screenshots — and the 7B teacher scored worse</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:36:21 +0000</pubDate>
      <link>https://dev.to/p0rt/i-distilled-a-7b-vision-model-into-a-2b-one-for-screenshots-and-the-7b-teacher-scored-worse-3akh</link>
      <guid>https://dev.to/p0rt/i-distilled-a-7b-vision-model-into-a-2b-one-for-screenshots-and-the-7b-teacher-scored-worse-3akh</guid>
      <description>&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/P0rt/vlm-distill-screenshots" rel="noopener noreferrer"&gt;https://github.com/P0rt/vlm-distill-screenshots&lt;/a&gt; &lt;br&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;a href="https://huggingface.co/p00rt/qwen2-vl-2b-screenshots-distill" rel="noopener noreferrer"&gt;https://huggingface.co/p00rt/qwen2-vl-2b-screenshots-distill&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a question I keep coming back to whenever someone ships a giant model: &lt;em&gt;what would I lose if I used something 3× smaller?&lt;/em&gt; Not in the abstract — for &lt;strong&gt;my&lt;/strong&gt; task, on &lt;strong&gt;my&lt;/strong&gt; hardware, with numbers I measured myself.&lt;/p&gt;

&lt;p&gt;So I ran the experiment. I took a 7B vision‑language model (VLM), used it as a teacher to teach a 2B student one narrow skill — &lt;strong&gt;describing UI screenshots&lt;/strong&gt; — and then measured exactly what the trade changed: quality, latency, throughput, memory. The whole thing runs on a single MacBook Pro (M4 Pro, 24 GB).&lt;/p&gt;

&lt;p&gt;This post is the honest write‑up: the method, the numbers, and — maybe more useful — the three or four places where reality didn't cooperate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; The distilled 2B student runs &lt;strong&gt;~2.4× faster&lt;/strong&gt;, in &lt;strong&gt;~2.4× less memory&lt;/strong&gt;, with &lt;strong&gt;3.75× fewer parameters&lt;/strong&gt; than the 7B teacher, and it clearly beats the &lt;em&gt;untrained&lt;/em&gt; 2B baseline on the task. The genuinely surprising part: on ROUGE‑L the &lt;strong&gt;2B student scored &lt;em&gt;higher&lt;/em&gt; than the 7B teacher&lt;/strong&gt; — which is a story about the metric, not the models, and turned out to be the most interesting thing I learned. (It's also the live exception to "a student is bounded by its teacher" that I argued about in the comments of &lt;a href="https://dev.to/p0rt/how-model-distillation-actually-works-and-what-the-china-distilled-our-model-headlines-really-3o0o"&gt;my last distillation post&lt;/a&gt; — on a narrow slice, the student really can pull ahead.)&lt;/p&gt;


&lt;h2&gt;
  
  
  Why distill a VLM for a &lt;em&gt;narrow&lt;/em&gt; domain?
&lt;/h2&gt;

&lt;p&gt;The obvious objection to this whole project: "Qwen2‑VL‑2B already exists and it's good — just use it."&lt;/p&gt;

&lt;p&gt;True. But "a good general small VLM" and "a small VLM that's &lt;em&gt;reliably&lt;/em&gt; good at the one thing you need" are different products. Distillation is how you turn the first into the second: you let a stronger model define the target behavior on &lt;strong&gt;your&lt;/strong&gt; data distribution, and the small model adopts it — no manual labelling on your side.&lt;/p&gt;

&lt;p&gt;And distilling a &lt;em&gt;vision‑language&lt;/em&gt; model is less‑travelled territory than the classic "distill BERT into something tiny" story. It drags in real inference engineering — 4‑bit teachers, LoRA, quantized runtimes, memory budgets — and that engineering is half of why it's worth writing up.&lt;/p&gt;

&lt;p&gt;The task I picked is deliberately narrow: &lt;strong&gt;screenshot understanding&lt;/strong&gt;. Given a UI screenshot, produce a one‑sentence summary plus a list of the key interface elements. Perception only — no clicking, no agent. (That's future work; more at the end.)&lt;/p&gt;


&lt;h2&gt;
  
  
  The setup: task, data, metrics
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data — &lt;a href="https://huggingface.co/datasets/rootsautomation/RICO-Screen2Words" rel="noopener noreferrer"&gt;Screen2Words&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;rootsautomation/RICO-Screen2Words&lt;/code&gt;, CC‑BY‑4.0): 22,417 Android UI screenshots from the RICO corpus, each with five human‑written summaries. Native splits are train / val / test = 15,743 / 2,364 / 4,310, across 28 app categories.&lt;/p&gt;

&lt;p&gt;One detail that matters more than it looks: the human captions are &lt;strong&gt;short&lt;/strong&gt; — median &lt;strong&gt;7 words&lt;/strong&gt;. Hold that thought; it comes back to bite the metrics.&lt;/p&gt;

&lt;p&gt;I picked the &lt;code&gt;rootsautomation&lt;/code&gt; mirror specifically because it's &lt;strong&gt;CC‑BY‑4.0&lt;/strong&gt; — publishable, unlike raw RICO's research‑only terms. I check the license &lt;em&gt;before&lt;/em&gt; I push weights, not after. (It's in the model card.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics.&lt;/strong&gt; ROUGE‑L and BLEU against the human references, plus an optional teacher‑as‑judge score. I implemented ROUGE‑L and BLEU from scratch (pure Python, multi‑reference, unit‑tested) so the numbers are deterministic and dependency‑free. CIDEr — the classic captioning metric — needs corpus‑level document frequencies; I left it as a follow‑up rather than pull in a heavy dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pipeline&lt;/strong&gt;, end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;download → build_dataset → teacher_label → train → eval → benchmark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every stage is a typed CLI step, all hyperparameters live in &lt;code&gt;configs/*.yaml&lt;/code&gt;, and the heavy steps (labelling, training) are resumable and versioned by a config hash. I built it phase by phase, one green‑CI PR at a time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Method: three signals, one MVP
&lt;/h2&gt;

&lt;p&gt;Knowledge distillation for generation has a few flavors, and I wanted the harness to support all of them behind flags so I could ablate cleanly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Response‑based KD&lt;/strong&gt; — the teacher generates answers, the student learns to reproduce them. The full objective mixes a soft and a hard target:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   L = α · CE(student, hard_labels)
       + (1 − α) · T² · KL( softmax(teacher/T) ‖ log_softmax(student/T) )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Feature‑based&lt;/strong&gt; — align the student's vision features with the teacher's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self‑distillation&lt;/strong&gt; — let the teacher label extra screenshots to grow the data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The MVP I actually trained is the &lt;strong&gt;hard‑target half of (1): sequence‑level distillation.&lt;/strong&gt; The teacher writes a description; the student is fine‑tuned (LoRA) to reproduce that &lt;em&gt;text&lt;/em&gt;. No teacher logits needed at train time, which keeps the whole thing laptop‑friendly.&lt;/p&gt;

&lt;p&gt;The soft‑KL term is implemented and unit‑tested (&lt;code&gt;response_kd_loss&lt;/code&gt;), but wiring it into training needs cached teacher logits — and that's exactly the α/temperature ablation axis I &lt;em&gt;couldn't&lt;/em&gt; run yet. I'd rather say that out loud than fake it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Teacher labelling
&lt;/h2&gt;

&lt;p&gt;The teacher is &lt;strong&gt;Qwen2‑VL‑7B‑Instruct in 4‑bit&lt;/strong&gt;, running through MLX on Apple Silicon. (&lt;code&gt;bitsandbytes&lt;/code&gt; is CUDA‑only, so the usual 4‑bit path doesn't exist on a Mac — MLX is the way in.)&lt;/p&gt;

&lt;p&gt;It labelled 200 training screenshots at &lt;strong&gt;~10.2 s/screenshot&lt;/strong&gt; (≈34 minutes; ≈2.7 hours projected for the full 15.7k split). Zero outputs were flagged degenerate by a light post‑validation pass (whitespace normalization + empty/too‑short detection — cheap insurance against format drift), and the mean target length was 33.6 words. A real example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The UI screenshot shows a fitness app displaying an exercise called 'Lunges,' with a progress indicator showing 30% complete. Key interface elements include a progress bar, a figure performing the exercise, and the text 'Lunges.'"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice it's &lt;strong&gt;33 words&lt;/strong&gt; and genuinely rich. The human reference for screens like this is more like &lt;em&gt;"exercise screen"&lt;/em&gt;. That gap is the whole story of the metrics section below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Training the student (and the part where MLX said no)
&lt;/h2&gt;

&lt;p&gt;Plan A was to train the LoRA adapter with MLX too — same runtime as the teacher, fast on Apple Silicon. Plan A died in the backward pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: [Primitive::vjp] Not implemented for CustomKernel.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mlx‑vlm 0.6.0 can't backprop through one of Qwen2‑VL's custom Metal kernels. I checked the usual suspects — &lt;code&gt;scaled_dot_product_attention&lt;/code&gt;, RMSNorm, RoPE all &lt;em&gt;do&lt;/em&gt; have gradients — so it's a specific kernel, and both &lt;code&gt;mlx&lt;/code&gt; and &lt;code&gt;mlx-vlm&lt;/code&gt; were already on their latest release, so there was no version to bump to. MLX stays a great &lt;strong&gt;inference&lt;/strong&gt; backend here; it just can't train this model yet.&lt;/p&gt;

&lt;p&gt;Plan B: train on the &lt;strong&gt;&lt;code&gt;hf&lt;/code&gt; path (transformers + PEFT LoRA) on Apple MPS&lt;/strong&gt;, with &lt;code&gt;PYTORCH_ENABLE_MPS_FALLBACK=1&lt;/code&gt;. That worked. Two more small potholes on the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The screenshots are too tall.&lt;/strong&gt; Qwen2‑VL expands a big RICO screenshot into &lt;em&gt;thousands&lt;/em&gt; of vision tokens; with a 1k context that overflows and throws a broadcast‑shape error deep in &lt;code&gt;get_rope_index&lt;/code&gt;. Fix: cap the visual‑token budget (&lt;code&gt;max_pixels&lt;/code&gt;) so an image stays well under the context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A papercut:&lt;/strong&gt; recent &lt;code&gt;transformers&lt;/code&gt; pulls in a Qwen2‑VL &lt;em&gt;video&lt;/em&gt; processor that needs &lt;code&gt;torchvision&lt;/code&gt; — which I hadn't installed. Easy to miss until the first run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After that it trained cleanly: loss &lt;strong&gt;0.80 → 0.39&lt;/strong&gt; over 40 steps, and — the part that matters for "is the checkpoint real" — I reloaded the merged adapter and it generated in the trained format ("…Key interface elements include…"). On a laptop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results: the honest version
&lt;/h2&gt;

&lt;p&gt;First, the caveat that frames everything below, because it's load‑bearing: &lt;strong&gt;this is a deliberately small proof‑of‑concept.&lt;/strong&gt; The quality numbers come from short training runs and a tiny eval set (80 train / 16–100 test examples depending on the run). Treat them as &lt;em&gt;trends and a working method&lt;/em&gt;, not a benchmark. What I'm confident in is the harness and the measurement; the absolute numbers want a full‑scale run before anyone quotes them. With that said —&lt;/p&gt;

&lt;p&gt;Here's the quality table on the test split (ROUGE‑L / BLEU vs the human references):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;ROUGE‑L&lt;/th&gt;
&lt;th&gt;BLEU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;teacher (7B)&lt;/td&gt;
&lt;td&gt;0.164&lt;/td&gt;
&lt;td&gt;0.000 †&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;student (2B + LoRA)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.178&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.019&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;baseline (2B, untrained)&lt;/td&gt;
&lt;td&gt;0.153&lt;/td&gt;
&lt;td&gt;0.018&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;† &lt;em&gt;Teacher BLEU rounds to 0.000 — that's not a bug, it's the length mismatch explained right below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read that twice, because it surprised me too: &lt;strong&gt;the 7B teacher scores &lt;em&gt;lower&lt;/em&gt; on ROUGE‑L than the 2B student, and its BLEU is essentially zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's not the teacher being bad — it's the metric. The teacher writes 33‑word descriptions; the human references are 7 words. BLEU rewards exact n‑gram overlap, so a rich, correct, &lt;em&gt;long&lt;/em&gt; answer against a terse reference scores ~0. ROUGE‑L (longest common subsequence) is kinder but still favors brevity‑matching. So against short references, all three models cluster in a narrow band and the verbose teacher actually looks &lt;em&gt;worse&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The honest takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Distillation helped:&lt;/strong&gt; the student (trained on teacher outputs) beats the untrained baseline, +16% relative ROUGE‑L. That's the comparison that's actually apples‑to‑apples (same model, same speed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;These metrics undersell rich outputs.&lt;/strong&gt; This is exactly why LLM‑as‑judge and CIDEr exist, and why I flag the ROUGE‑L/BLEU numbers as a floor, not a verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clean, unambiguous win is efficiency&lt;/strong&gt; — so let's go there.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The trade‑off (the actual point)
&lt;/h2&gt;

&lt;p&gt;Same hardware, same 4‑bit setup, 128‑token generations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;params (B)&lt;/th&gt;
&lt;th&gt;latency p50 (ms)&lt;/th&gt;
&lt;th&gt;throughput (img/s)&lt;/th&gt;
&lt;th&gt;peak mem (GB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;teacher (Qwen2‑VL‑7B)&lt;/td&gt;
&lt;td&gt;8.29&lt;/td&gt;
&lt;td&gt;1538&lt;/td&gt;
&lt;td&gt;0.63&lt;/td&gt;
&lt;td&gt;5.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;student (Qwen2‑VL‑2B)&lt;/td&gt;
&lt;td&gt;2.21&lt;/td&gt;
&lt;td&gt;651&lt;/td&gt;
&lt;td&gt;1.52&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;~2.4× faster, in ~2.4× less memory, with 3.75× fewer parameters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrqe8bj33mfzdq3qr27y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrqe8bj33mfzdq3qr27y.png" alt="Quality vs speed" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2trucj6urbfr6780cawk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2trucj6urbfr6780cawk.png" alt="Quality vs memory" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The student sits in the friendly corner: as fast and light as the untrained baseline, but with the distilled quality bump on top. The teacher is off to the slow, heavy side — and, per the metrics caveat above, not even ahead on ROUGE‑L. The 2B model is the one I'd actually deploy for this task.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Honesty box.&lt;/strong&gt; "Peak memory" on Apple Silicon is unified‑memory allocation, not CUDA VRAM. The headline efficiency numbers are MLX/4‑bit on Apple Silicon, not a server GPU. As flagged above, the quality numbers are a small proof‑of‑concept — trends, not a benchmark result. The thing I'm confident in is the &lt;strong&gt;method and the measurement harness&lt;/strong&gt; — both reproducible from the repo.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Ablations: what actually moved quality
&lt;/h2&gt;

&lt;p&gt;I varied the two knobs my sequence‑level SFT exposes — training steps and LoRA rank — at fixed everything‑else, and re‑evaluated each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;run&lt;/th&gt;
&lt;th&gt;LoRA r&lt;/th&gt;
&lt;th&gt;steps&lt;/th&gt;
&lt;th&gt;ROUGE‑L&lt;/th&gt;
&lt;th&gt;BLEU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.152&lt;/td&gt;
&lt;td&gt;0.017&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0.170&lt;/td&gt;
&lt;td&gt;0.018&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;0.172&lt;/td&gt;
&lt;td&gt;0.020&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0.171&lt;/td&gt;
&lt;td&gt;0.019&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4w9s2msldit4umpchgve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4w9s2msldit4umpchgve.png" alt="Ablation: steps vs ROUGE-L" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Train at all" is the dominant lever&lt;/strong&gt; — the baseline → distilled jump is by far the biggest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More steps help marginally&lt;/strong&gt;, and the gain shows up more on BLEU (exact phrasing sharpens) than on ROUGE‑L.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoRA rank is ~neutral&lt;/strong&gt; here — r8 ≈ r16. At this data scale, adapter &lt;em&gt;capacity&lt;/em&gt; isn't the bottleneck, so r8 is plenty.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The α/temperature/feature‑alignment ablations from the method section belong to the &lt;strong&gt;logit‑level&lt;/strong&gt; KD variant, which needs cached teacher logits I haven't produced yet. Three honest comparisons beat six fabricated ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inference engineering (the half that bites)
&lt;/h2&gt;

&lt;p&gt;The modelling is the easy part. The engineering is where the hours went:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two backends, one interface.&lt;/strong&gt; The teacher runs 4‑bit via &lt;code&gt;mlx-vlm&lt;/code&gt;; the student trains/infers via &lt;code&gt;transformers&lt;/code&gt; + PEFT. A small factory (&lt;code&gt;make_teacher&lt;/code&gt; / &lt;code&gt;make_student&lt;/code&gt;) hides which one you're on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MLX can't train this model yet&lt;/strong&gt; (the &lt;code&gt;CustomKernel&lt;/code&gt; vjp gap above) — so training is &lt;code&gt;hf&lt;/code&gt;/MPS, inference can be either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't mix runtimes in one process.&lt;/strong&gt; Evaluating an MLX teacher and a torch student in the &lt;em&gt;same&lt;/em&gt; Python process conflicts on Apple Silicon (&lt;code&gt;'array' object has no attribute 'device'&lt;/code&gt;). Run them as separate invocations. Found that one the fun way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual‑token budget is a real knob&lt;/strong&gt; — too many tokens per screenshot and you blow the context window; cap &lt;code&gt;max_pixels&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ONNX export&lt;/strong&gt; of a full VLM is famously finicky, so I kept torch/MLX inference as the canonical path and shipped a &lt;strong&gt;merge‑and‑save&lt;/strong&gt; export instead: fold the LoRA into the base weights and write a standalone 2B student you can load anywhere with plain &lt;code&gt;transformers&lt;/code&gt;. (ONNX stays a documented stretch goal.)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What the metrics miss (and what I'd do next)
&lt;/h2&gt;

&lt;p&gt;The most useful thing this project taught me wasn't a number — it was &lt;em&gt;which numbers to distrust&lt;/em&gt;. ROUGE‑L and BLEU against terse human references genuinely undersell a model that writes richer, correct descriptions. If I were taking this past proof‑of‑concept, the very next step would be &lt;strong&gt;LLM‑as‑judge scoring&lt;/strong&gt; (the harness already supports it) and &lt;strong&gt;CIDEr&lt;/strong&gt;, both of which reward content over brevity‑matching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest limitations:&lt;/strong&gt; small scale (short training, tiny eval N); narrow domain (RICO Android UI); BLEU is low for &lt;em&gt;everyone&lt;/em&gt; because of the length mismatch; and the headline efficiency numbers are MLX/4‑bit on Apple Silicon, not a server GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future work:&lt;/strong&gt; cache teacher logits and turn on the soft‑KL term (and finally run the α/T ablation); add feature alignment; grow the data with teacher‑labelled RICO; a full‑scale run on a 24 GB GPU; and the natural next domain step — &lt;strong&gt;grounding&lt;/strong&gt; (bounding boxes) and an &lt;strong&gt;agent wrapper&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproduce it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/P0rt/vlm-distill-screenshots &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;vlm-distill-screenshots
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--extra&lt;/span&gt; data            &lt;span class="c"&gt;# data stack&lt;/span&gt;
uv run vlm-build-dataset        &lt;span class="c"&gt;# Screen2Words → unified {image, prompt, target}&lt;/span&gt;

uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--extra&lt;/span&gt; mlx             &lt;span class="c"&gt;# Apple Silicon teacher&lt;/span&gt;
uv run vlm-teacher-label &lt;span class="nt"&gt;--limit&lt;/span&gt; 200

uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--extra&lt;/span&gt; ml              &lt;span class="c"&gt;# transformers + peft&lt;/span&gt;
&lt;span class="nv"&gt;PYTORCH_ENABLE_MPS_FALLBACK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 uv run vlm-train &lt;span class="nt"&gt;--limit&lt;/span&gt; 200
uv run vlm-eval &lt;span class="nt"&gt;--models&lt;/span&gt; student,baseline &lt;span class="nt"&gt;--adapter&lt;/span&gt; results/checkpoints/&amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--limit&lt;/span&gt; 100
uv run vlm-benchmark &lt;span class="nt"&gt;--models&lt;/span&gt; teacher,student
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything — configs, the metric implementations, the plots, this article — is in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/P0rt/vlm-distill-screenshots" rel="noopener noreferrer"&gt;https://github.com/P0rt/vlm-distill-screenshots&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model + card:&lt;/strong&gt; &lt;a href="https://huggingface.co/p00rt/qwen2-vl-2b-screenshots-distill" rel="noopener noreferrer"&gt;https://huggingface.co/p00rt/qwen2-vl-2b-screenshots-distill&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/rootsautomation/RICO-Screen2Words" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/rootsautomation/RICO-Screen2Words&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've done VLM distillation and have a take on metrics that actually reward rich descriptions, I'd love to hear it in the comments.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>llm</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>Your AI Agent Isn't Failing Because It Hallucinates — It's Failing Because of Rate Limits</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Tue, 02 Jun 2026 13:09:00 +0000</pubDate>
      <link>https://dev.to/p0rt/your-ai-agent-isnt-failing-because-it-hallucinates-its-failing-because-of-rate-limits-2d60</link>
      <guid>https://dev.to/p0rt/your-ai-agent-isnt-failing-because-it-hallucinates-its-failing-because-of-rate-limits-2d60</guid>
      <description>&lt;p&gt;When my agents started failing in production, I did what everyone does first: I went hunting for hallucinations. Better prompts, tighter output schemas, more guardrails. None of it moved the needle, because I was debugging the wrong layer. The agent's reasoning was fine. It was the &lt;em&gt;plumbing&lt;/em&gt; that kept collapsing — and the single biggest culprit was the most boring thing imaginable: rate limits.&lt;/p&gt;

&lt;p&gt;This turns out not to be just my problem. It's the dominant production failure mode for LLM applications right now, and almost nobody talks about it because it doesn't make for a good demo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — In production, the thing that takes your agent down usually isn't bad reasoning — it's capacity. Provider rate limits are now one of the largest sources of LLM call errors in real traces. A demo makes one request at a time; a production agent fans out into dozens of chained, retrying, concurrent calls and slams into limits the demo never touched. The fix isn't a smarter model, it's capacity engineering: budgeting, backpressure, retries with jitter, fallback models, and caching.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The data nobody puts in the pitch deck
&lt;/h2&gt;

&lt;p&gt;Here's the number that reframed how I think about agent reliability. In Datadog's analysis of real LLM observability traces, rate-limit errors were a &lt;em&gt;huge&lt;/em&gt; share of all LLM call failures — in March 2026, roughly a third of all LLM span errors were rate limits, on the order of millions of individual errors. Their conclusion was blunt: when the dominant failure mode of your LLM application is capacity, you need to redouble your &lt;em&gt;capacity engineering&lt;/em&gt;, not your prompt engineering.&lt;/p&gt;

&lt;p&gt;Sit with that. The failure mode isn't the model being dumb. It's the model provider saying "too many requests" — and your agent having no plan for that answer.&lt;/p&gt;

&lt;p&gt;It maps almost perfectly onto the broader "agents fail in production" story everyone's writing about. The reason demos lie isn't malice; it's structural. A demo runs one clean request, one user, one happy path. Production is concurrency, retries, fan-out, and load — the exact conditions that manufacture rate-limit errors. The gap between "works in a notebook" and "works at 3am under load" is, more often than people admit, a capacity gap wearing a reliability costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents hit this wall harder than chatbots
&lt;/h2&gt;

&lt;p&gt;A plain chatbot makes one API call per user turn. An agent is a different beast. A single "task" expands into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A planning call.&lt;/li&gt;
&lt;li&gt;N tool-selection calls as it loops.&lt;/li&gt;
&lt;li&gt;A call per tool result to decide the next step.&lt;/li&gt;
&lt;li&gt;Retries on each of those when something is flaky.&lt;/li&gt;
&lt;li&gt;Often a sub-agent or two, each with its own loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So one user action becomes 10–40 model calls, frequently &lt;em&gt;concurrent&lt;/em&gt;, frequently &lt;em&gt;retrying&lt;/em&gt;. The multiplier is the whole point of agents — and it's also exactly what walks you into a rate limit. Worse, the naive failure response makes it catastrophic: a call gets a 429, the framework retries immediately, that retry also gets a 429, and now you've turned one rate-limit error into a retry storm that takes the whole task down.&lt;/p&gt;

&lt;p&gt;The arithmetic is unforgiving once you write it out. Say your provider gives you 500 requests/minute. If each agent task fans out to ~20 model calls, then just &lt;strong&gt;25 concurrent tasks&lt;/strong&gt; saturate your entire quota — and that's before a single retry. Add naive immediate retries on the resulting 429s and you don't degrade gracefully, you spike straight through the ceiling. I've watched this pattern play out more than once, and every time the first instinct in the room is "the model is broken" — when the model never even ran.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkcw0usb2uaz7iit0canq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkcw0usb2uaz7iit0canq.png" alt="One user action fans out into 10–40 concurrent model calls that all draw from one fixed provider quota; naive retries turn a single 429 into a storm, while a limiter with backoff keeps calls under the ceiling." width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is also where serverless bites you specifically. On Cloud Run, a traffic spike spins up new instances happily — compute scales fine. But your LLM provider quota does &lt;em&gt;not&lt;/em&gt; scale with your container count. So autoscaling does the worst possible thing: it lets more concurrent agents launch, each firing its call fan-out, all drawing from the same fixed provider quota, all hitting the ceiling at once. The platform that's supposed to absorb load becomes the thing that amplifies it into the rate limiter. It's a genuinely counterintuitive failure: the healthier your autoscaling looks on the compute dashboard, the harder you're hammering a quota that can't scale with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The capacity-engineering toolkit
&lt;/h2&gt;

&lt;p&gt;None of the fixes are exotic. They're the same patterns distributed-systems people have used for decades — they just haven't migrated into most agent codebases yet, because the field grew up on prompt-craft, not ops. Here's what actually moved my reliability numbers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Budget and backpressure, don't just retry
&lt;/h3&gt;

&lt;p&gt;The instinct is to retry harder. The fix is to &lt;em&gt;send less&lt;/em&gt;. Put a concurrency limiter (a semaphore / token bucket) in front of all outbound model calls so your app never exceeds your known provider quota in the first place. When the budget is full, queue — don't fire-and-retry. This single change does more than any retry tuning, because it prevents the storm instead of recovering from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="c1"&gt;# Cap concurrent in-flight calls below your provider's actual limit.
# Leave headroom — you are NOT the only caller against this quota.
&lt;/span&gt;&lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Retry with exponential backoff &lt;em&gt;and jitter&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;When you do retry, never retry immediately, and never retry in lockstep. Synchronized retries from many workers create a thundering herd that re-triggers the limit. Exponential backoff with random jitter spreads them out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="c1"&gt;# exponential + full jitter
&lt;/span&gt;            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Respect the &lt;code&gt;Retry-After&lt;/code&gt; header if the provider sends one — it's telling you exactly how long to wait, which beats guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fallback model, not just failure
&lt;/h3&gt;

&lt;p&gt;Tie this back to distillation thinking: you don't need your frontier model for every call. Route to a cheaper/secondary model (a different provider, or a smaller model on a separate quota) when the primary is rate-limited. A degraded answer beats a dead task, and you've spread load across two quota pools instead of hammering one. This is the same hybrid pattern as keeping a cheap student model for the easy 90% and falling back to an expensive teacher — just applied to &lt;em&gt;availability&lt;/em&gt; instead of &lt;em&gt;capability&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cache aggressively
&lt;/h3&gt;

&lt;p&gt;A surprising fraction of agent calls are near-duplicate: the same tool descriptions, the same system context, the same sub-queries across runs. Prompt/response caching and reusing provider-side prompt caching cuts the call volume that reaches the limiter at all. The cheapest rate-limit error is the request you never sent.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Make capacity observable
&lt;/h3&gt;

&lt;p&gt;You can't engineer what you can't see. The reason rate limits blindside teams is that they show up as generic "agent failed" errors, not as a labeled capacity problem. Log the error &lt;em&gt;class&lt;/em&gt; (429 vs timeout vs tool error), track your in-flight concurrency and your 429-rate as first-class metrics, and alert on them. The shift that mattered most for me was simply separating "the model was wrong" from "the provider said no" in the telemetry — until you do that, every failure looks like a reasoning bug, and you keep fixing the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model shift
&lt;/h2&gt;

&lt;p&gt;The thing I'd tell my past self: &lt;strong&gt;treat your LLM provider quota as a shared, finite, non-scaling resource — like a database connection pool, not like CPU.&lt;/strong&gt; Compute scales elastically. Your token-per-minute and request-per-minute quotas do not. Once you internalize that, agent reliability stops looking like an AI problem and starts looking like a classic distributed-systems capacity problem — which is great news, because we already know how to solve those.&lt;/p&gt;

&lt;p&gt;Smarter models won't save you here. A GPT-6 that reasons perfectly still returns 429 when you exceed your quota. The reliability frontier for agents in 2026 isn't intelligence — it's capacity engineering.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're running agents in production, I'm curious what your dominant failure mode actually is when you separate the error classes — reasoning, capacity, or tool integration? My money's increasingly on capacity. Tell me I'm wrong in the comments.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Datadog, &lt;a href="https://www.datadoghq.com/state-of-ai-engineering/" rel="noopener noreferrer"&gt;"State of AI Engineering"&lt;/a&gt; (2026) — rate-limit errors as a dominant share of LLM call failures in production traces.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.c-sharpcorner.com/article/why-ai-agents-fail-in-production-and-how-engineering-teams-are-fixing-it/" rel="noopener noreferrer"&gt;"Why AI Agents Fail in Production and How Engineering Teams Are Fixing It"&lt;/a&gt;, C# Corner (2026).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/issa_gueye/the-ai-agent-reliability-gap-in-2026-why-the-tooling-is-finally-catching-up-ne3"&gt;"The AI Agent Reliability Gap in 2026"&lt;/a&gt;, DEV Community.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.digitalapplied.com/blog/88-percent-ai-agents-never-reach-production-failure-framework" rel="noopener noreferrer"&gt;"Why 88% of AI Agents Never Reach Production"&lt;/a&gt;, Digital Applied (2026).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>How Model Distillation Actually Works (and What the 'China Distilled Our Model' Headlines Really Mean)</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Fri, 29 May 2026 12:11:12 +0000</pubDate>
      <link>https://dev.to/p0rt/how-model-distillation-actually-works-and-what-the-china-distilled-our-model-headlines-really-3o0o</link>
      <guid>https://dev.to/p0rt/how-model-distillation-actually-works-and-what-the-china-distilled-our-model-headlines-really-3o0o</guid>
      <description>&lt;p&gt;Every few weeks a headline drops: &lt;em&gt;"Chinese lab distilled a frontier model from OpenAI / Anthropic."&lt;/em&gt; Cue the comments — half the thread thinks distillation is a synonym for theft, the other half thinks it's some exotic Chinese trick.&lt;/p&gt;

&lt;p&gt;Both are wrong. Distillation is one of the most boring, well-established techniques in deep learning, and the labs raising the alarms use it on their own models constantly. The actual controversy is narrower and more interesting than the headlines. Let's separate the engineering from the geopolitics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What distillation actually is
&lt;/h2&gt;

&lt;p&gt;Knowledge distillation trains a small &lt;strong&gt;student&lt;/strong&gt; model to imitate a large &lt;strong&gt;teacher&lt;/strong&gt; model. The classic framing comes from Hinton et al. (2015): instead of training the student only on ground-truth labels, you also train it to match the teacher's output distribution.&lt;/p&gt;

&lt;p&gt;Why does that help? Because the teacher's &lt;em&gt;full probability distribution&lt;/em&gt; carries far more information than the single correct answer. If a teacher classifies an image of a dog, it might output &lt;code&gt;dog: 0.9, wolf: 0.08, cat: 0.001&lt;/code&gt;. That "dog and wolf are similar, cat is not" signal — Hinton called it &lt;strong&gt;dark knowledge&lt;/strong&gt; — is exactly what a small model struggles to learn from hard labels alone.&lt;/p&gt;

&lt;p&gt;There are two kinds of training signal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard labels&lt;/strong&gt; — the final answer (the token the teacher actually produced, or the ground-truth label).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft labels&lt;/strong&gt; — the teacher's full probability distribution over outputs, usually its logits passed through a softmax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trick is &lt;strong&gt;temperature&lt;/strong&gt;. You divide the logits by a temperature &lt;code&gt;T &amp;gt; 1&lt;/code&gt; before the softmax, which flattens the distribution and exposes those small-but-meaningful probabilities the student should learn from.&lt;/p&gt;

&lt;p&gt;The loss is a blend of two terms: a standard cross-entropy against the real labels, and a KL-divergence pulling the student's softened distribution toward the teacher's.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch.nn.functional&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;distillation_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;student_logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;teacher_logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Standard loss: student vs ground truth (hard labels)
&lt;/span&gt;    &lt;span class="n"&gt;hard_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cross_entropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;student_logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Distillation loss: student vs teacher's softened distribution (soft labels)
&lt;/span&gt;    &lt;span class="n"&gt;soft_targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;teacher_logits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;student_log_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;student_logits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# T**2 keeps gradient magnitudes balanced when T &amp;gt; 1
&lt;/span&gt;    &lt;span class="n"&gt;soft_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kl_div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;student_log_probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;soft_targets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reduction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;batchmean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hard_loss&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;soft_loss&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For LLMs the same idea applies per token: the teacher's next-token distribution is the soft target. In practice teams mix hard and soft labels — recent work argues the gain from mixing comes less from "matching the teacher better" and more from reducing &lt;em&gt;exposure bias&lt;/em&gt; (the train/inference distribution mismatch). The point: this is normal, published, peer-reviewed engineering.&lt;/p&gt;

&lt;p&gt;And labs distill their own models all the time. The cheap, fast variant of a flagship model that you actually get to call in production? Very often a distilled student. Anthropic itself, in the middle of its own complaint about Chinese firms, acknowledged that AI companies &lt;em&gt;routinely&lt;/em&gt; distill their own models to make smaller, cheaper versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why distilling from a closed API is a different beast
&lt;/h2&gt;

&lt;p&gt;Here's the part the headlines skip. Everything above assumes you have the teacher's &lt;strong&gt;logits&lt;/strong&gt; — the raw output distribution. That's &lt;strong&gt;white-box distillation&lt;/strong&gt;, and it requires access to the model's internals or at least its full probability outputs.&lt;/p&gt;

&lt;p&gt;You do &lt;strong&gt;not&lt;/strong&gt; get logits from a closed commercial API like Claude or GPT. You get text. That forces &lt;strong&gt;black-box&lt;/strong&gt; (a.k.a. sequence-level) distillation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prompt the teacher with lots of inputs.&lt;/li&gt;
&lt;li&gt;Collect its generated text outputs.&lt;/li&gt;
&lt;li&gt;Build a synthetic dataset of (prompt → teacher answer) pairs.&lt;/li&gt;
&lt;li&gt;Fine-tune your student on that dataset with supervised fine-tuning, often followed by RL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You lose the dark knowledge in the soft labels, but it turns out you can get remarkably far just by training on a large, high-quality synthetic dataset generated by a strong teacher. This is exactly why "did model X learn from model Y's outputs?" is such a live and hard-to-prove question — the evidence isn't a stolen weights file, it's statistical fingerprints in behavior (a model that randomly claims to &lt;em&gt;be&lt;/em&gt; ChatGPT, mirrors another model's quirks, etc.).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;White-box&lt;/th&gt;
&lt;th&gt;Black-box (closed API)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Needs&lt;/td&gt;
&lt;td&gt;Logits / weights&lt;/td&gt;
&lt;td&gt;Just text outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal richness&lt;/td&gt;
&lt;td&gt;High (full distribution)&lt;/td&gt;
&lt;td&gt;Lower (final answers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feasible against a closed model?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the China allegations are about&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;This one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  So what are the actual allegations?
&lt;/h2&gt;

&lt;p&gt;Strip the drama and here's the documented timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jan 2025&lt;/strong&gt; — After DeepSeek's R1 launch, OpenAI and Microsoft open an investigation into whether DeepSeek used ChatGPT outputs to train it. Users noticed R1 behaving suspiciously ChatGPT-like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feb 2026&lt;/strong&gt; — OpenAI sends a memo to the U.S. House Select Committee on China alleging DeepSeek used obfuscated third-party routers to access OpenAI models and programmatically extract outputs for distillation, in violation of its terms of service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feb 24, 2026&lt;/strong&gt; — Anthropic publicly accuses three Chinese firms — &lt;strong&gt;DeepSeek, Moonshot AI, and MiniMax&lt;/strong&gt; — of coordinated "distillation attack" campaigns: flooding Claude with crafted prompts, allegedly via commercial proxy services running tens of thousands of accounts to sidestep Anthropic's China access restrictions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things matter here, and most coverage gets them backwards:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;These are allegations.&lt;/strong&gt; The labs have not, as of writing, published the full underlying evidence, and the accused firms dispute or haven't confirmed them. Behavioral similarity is suggestive, not proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dispute is not "distillation = bad."&lt;/strong&gt; As one ethics researcher put it after Anthropic's statement, if Anthropic itself calls distillation legitimate and widespread, the controversy can't be the technique. It's two narrower things: &lt;strong&gt;unauthorized access&lt;/strong&gt; (using proxies to evade geographic and account restrictions) and &lt;strong&gt;terms-of-service violations&lt;/strong&gt; (most frontier APIs explicitly forbid using outputs to train a competing model). It's closer to a contract-and-access fight than an IP-theft slam dunk — and the legal status of "training on another model's outputs" is genuinely unsettled.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  "How long does it take / how much does it cost?"
&lt;/h2&gt;

&lt;p&gt;This is the question everyone asks, and the honest answer is: dramatically less than training from scratch — which is the entire economic motive — but &lt;strong&gt;precise figures for any specific alleged case are not public.&lt;/strong&gt; Anyone quoting you an exact "they did it in N days for $M" is guessing.&lt;/p&gt;

&lt;p&gt;What we can say structurally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pretraining a frontier model from scratch&lt;/strong&gt; means a massive run on tens of thousands of high-end accelerators, plus the data pipeline and research iteration behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distillation collapses that timeline.&lt;/strong&gt; The expensive part — discovering the capability — was already paid for by the teacher. The student's cost is roughly: generating a synthetic dataset (API calls + time) plus a comparatively cheap fine-tuning run. That's the asymmetry the U.S. labs are upset about: they spend billions to push the frontier, and a "free-rider" can chase it for a fraction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is also why DeepSeek's headline numbers were so contested.&lt;/strong&gt; Its self-reported low training cost and modest hardware footprint were precisely what made rivals suspect a shortcut: it's much easier to hit those numbers if you bootstrapped from an already-trained Western teacher rather than doing all the discovery yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: distillation makes a &lt;em&gt;strong-ish student&lt;/em&gt; fast and cheap. It does &lt;strong&gt;not&lt;/strong&gt; let you leapfrog &lt;em&gt;past&lt;/em&gt; the teacher — a student is generally capped by the teacher it learned from. You don't distill your way to the frontier; you distill your way to a cheap copy of someone else's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Distillation is standard, published deep-learning practice. The labs complaining about it use it themselves.&lt;/li&gt;
&lt;li&gt;White-box distillation needs logits; closed APIs only expose text, so distilling from Claude/GPT means &lt;strong&gt;black-box&lt;/strong&gt; training on generated outputs.&lt;/li&gt;
&lt;li&gt;The OpenAI and Anthropic allegations against DeepSeek, Moonshot, and MiniMax are about &lt;strong&gt;unauthorized access and ToS violations&lt;/strong&gt;, not about distillation being inherently illegitimate — and they remain &lt;em&gt;allegations&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The economic point is real: distillation is far cheaper than frontier pretraining, which is why it's a business and policy flashpoint. But a student is bounded by its teacher.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the deep technical version of any of these — the math of temperature scaling, why mixing hard and soft labels beats either alone, or how behavioral fingerprinting tries to &lt;em&gt;detect&lt;/em&gt; distillation — let me know in the comments.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI memo to the U.S. House Select Committee on China (Feb 2026) — reporting via Reuters and Rest of World.&lt;/li&gt;
&lt;li&gt;"Anthropic joins OpenAI in flagging distillation campaigns by Chinese AI firms," CNBC, Feb 24, 2026.&lt;/li&gt;
&lt;li&gt;Hinton, Vinyals, Dean, "Distilling the Knowledge in a Neural Network" (2015).&lt;/li&gt;
&lt;li&gt;"Understanding LLM Distillation Techniques," MarkTechPost, 2026.&lt;/li&gt;
&lt;li&gt;"The Bridge-Garden Dilemma in LLM Distillation," arXiv:2605.26246.&lt;/li&gt;
&lt;li&gt;Winston &amp;amp; Strawn, "Is AI Distillation by DeepSeek IP Theft?" (analysis of the legal gray zone).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>llm</category>
      <category>deeplearning</category>
    </item>
  </channel>
</rss>
