<?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%2F00065fc9-07d7-47dd-b882-f297a6158dbe.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 Bug That Crashes Your Import Is the Lucky One</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Fri, 31 Jul 2026 12:43:45 +0000</pubDate>
      <link>https://dev.to/p0rt/the-bug-that-crashes-your-import-is-the-lucky-one-25of</link>
      <guid>https://dev.to/p0rt/the-bug-that-crashes-your-import-is-the-lucky-one-25of</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You are migrating a 50,000-message Slack workspace to Zulip. Somewhere around message 31,000 the import dies with &lt;code&gt;KeyError: 'ts'&lt;/code&gt;. Annoying, but here is the uncomfortable part: &lt;strong&gt;that is the lucky outcome.&lt;/strong&gt; The unlucky one is &lt;code&gt;"ts": "NaN"&lt;/code&gt;, where nothing dies, nothing warns, and your company's message history quietly comes out in the wrong order.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Zulip's Slack importer used &lt;code&gt;float(message["ts"])&lt;/code&gt; unguarded, both as a sort key and as &lt;code&gt;date_sent&lt;/code&gt;. One message with a missing or malformed &lt;code&gt;ts&lt;/code&gt; aborted the entire import; a non-finite value like &lt;code&gt;"NaN"&lt;/code&gt; did not even raise, it silently broke the sort. My fix (&lt;a href="https://github.com/zulip/zulip/pull/39813" rel="noopener noreferrer"&gt;zulip/zulip#39813&lt;/a&gt;) skips such messages with a warning and requires &lt;code&gt;ts&lt;/code&gt; to parse to a &lt;em&gt;finite&lt;/em&gt; float via &lt;code&gt;math.isfinite&lt;/code&gt;. The regression test fails with &lt;code&gt;KeyError: 'ts'&lt;/code&gt; on the old code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/zulip/zulip" rel="noopener noreferrer"&gt;Zulip&lt;/a&gt; is an open-source team chat server (Django/Python, ~25k stars) with an unusually strict engineering culture: near-total backend test coverage, strict mypy, and a commit discipline of "each commit is a minimal coherent idea".&lt;/p&gt;

&lt;p&gt;The code I touched lives in &lt;code&gt;zerver/data_import/&lt;/code&gt;: the subsystem that converts exports from Slack, Microsoft Teams, and Mattermost into Zulip's format. This subsystem has one property that should shape every line in it: &lt;strong&gt;the input is another tool's output.&lt;/strong&gt; Import is a long batch process over data of arbitrary quality, and the admin running the migration has no way to "fix" what Slack's export tool produced. A pipeline that dies on record 31,207 of 50,000 is strictly worse than one that skips record 31,207 with a warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;get_messages_iterator()&lt;/code&gt; in &lt;code&gt;zerver/data_import/slack.py&lt;/code&gt; streams every message of the export, sorting each day's messages by timestamp:&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;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages_for_one_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_timestamp_from_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where the sort key was simply:&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;get_timestamp_from_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ZerverFieldsT&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;float&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;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line has &lt;strong&gt;three distinct failure modes&lt;/strong&gt;, all reproduced during validation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;ts&lt;/code&gt; is missing.&lt;/strong&gt; &lt;code&gt;KeyError: 'ts'&lt;/code&gt; straight out of &lt;code&gt;sorted(...)&lt;/code&gt;. The whole import aborts because of one message:&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="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zerver/data_import/slack.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;911&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;get_messages_iterator&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages_for_one_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_timestamp_from_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zerver/data_import/slack.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;1461&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;get_timestamp_from_message&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;ts&lt;/code&gt; is garbage.&lt;/strong&gt; &lt;code&gt;"not-a-number"&lt;/code&gt; raises &lt;code&gt;ValueError&lt;/code&gt;. Same total abort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;ts&lt;/code&gt; is &lt;code&gt;"NaN"&lt;/code&gt;.&lt;/strong&gt; The nasty one. &lt;code&gt;float("NaN")&lt;/code&gt; is a perfectly valid parse, so nothing raises. But NaN is incomparable (&lt;code&gt;NaN &amp;lt; x&lt;/code&gt; and &lt;code&gt;x &amp;lt; NaN&lt;/code&gt; are both False), which violates the total ordering Timsort assumes, so &lt;code&gt;sorted()&lt;/code&gt; silently returns an inconsistent order:&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="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1434139102.000002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NaN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1434139101.000001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; ['1434139102.000002', 'NaN', '1434139101.000001']   # not sorted, no error
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exception, no warning. Just a migrated archive with scrambled chronology. Crashing is the lucky case; this is the case that costs you a re-migration three weeks later when someone notices the history reads wrong.&lt;/p&gt;

&lt;p&gt;One honesty note: I did not discover this failure class from zero. The Zulip maintainers' own audit issue (&lt;a href="https://github.com/zulip/zulip/issues/39650" rel="noopener noreferrer"&gt;#39650&lt;/a&gt;) flags the unguarded timestamp sort key as one of the two highest-impact robustness items, and it was unclaimed when I picked it up. What I brought is the implementation, the non-finite analysis, and the test that pins the behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;PR: &lt;strong&gt;&lt;a href="https://github.com/zulip/zulip/pull/39813" rel="noopener noreferrer"&gt;zulip/zulip#39813&lt;/a&gt;&lt;/strong&gt; (branch &lt;code&gt;P0rt:slack-ts-guard&lt;/code&gt;, single commit, +54/-0 across the module and its test file).&lt;/p&gt;

&lt;p&gt;The fix is a predicate plus a guard, following the skip-with-warning pattern that already exists two lines above for other unimportable messages:&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;message_has_valid_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ZerverFieldsT&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Whether `ts` is present and parses to a finite float.

    `get_timestamp_from_message` is used both as a sort key and for
    `date_sent`, so a missing or malformed `ts` on a single message
    would otherwise abort the entire import — and a non-finite value
    like &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NaN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; would not even raise, instead silently producing an
    inconsistent sort order.
    &lt;/span&gt;&lt;span class="sh"&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="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;message_has_valid_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skipping Slack message with invalid ts %r in %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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;message_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail that matters: &lt;code&gt;math.isfinite&lt;/code&gt;, not a bare try/except around &lt;code&gt;float()&lt;/code&gt;. A naive "does it parse" check would have fixed the two loud failure modes and waved the silent one straight through.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The test had to fail first.&lt;/strong&gt; I wrote &lt;code&gt;test_get_messages_iterator_skips_invalid_timestamps&lt;/code&gt;: a temp directory with one day of Slack export containing five messages, two valid (deliberately in reverse chronological order), one with no &lt;code&gt;ts&lt;/code&gt;, one with &lt;code&gt;"not-a-number"&lt;/code&gt;, one with &lt;code&gt;"NaN"&lt;/code&gt;. Then I rolled the source file back to &lt;code&gt;upstream/main&lt;/code&gt;, kept the test, and ran it: &lt;code&gt;KeyError: 'ts'&lt;/code&gt;, exactly the predicted crash. Restored the fix: the test asserts that exactly the two valid messages survive, in correct order, with exactly three warnings logged. A regression test that never failed against the old code is a test fitted to the fix, not a test of the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full-suite validation in a provisioned dev environment&lt;/strong&gt;, not just the happy path: &lt;code&gt;./tools/test-backend zerver.tests.test_slack_importer&lt;/code&gt; (56/56 passing), &lt;code&gt;./tools/lint&lt;/code&gt; and &lt;code&gt;./tools/run-mypy&lt;/code&gt; clean, and &lt;code&gt;test-backend --coverage&lt;/code&gt; showing zero uncovered lines in &lt;code&gt;zerver/data_import/slack.py&lt;/code&gt; after the change, including the new guard path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One decision I explicitly left open for reviewers:&lt;/strong&gt; skipping a message with a broken &lt;code&gt;ts&lt;/code&gt; versus synthesizing a fallback timestamp for it. Skipping loses the message but keeps the archive honest; a synthetic timestamp keeps the message but fabricates chronology. I went with skip-plus-warning because it matches the file's existing conventions, and flagged the tradeoff in the PR instead of pretending it does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;I instrumented the demo with &lt;code&gt;sentry-sdk&lt;/code&gt; (&lt;code&gt;traces_sample_rate=1.0&lt;/code&gt;, environment &lt;code&gt;bugsmash-demo&lt;/code&gt;) and ran the exact same poisoned export twice: once with &lt;code&gt;zerver/data_import/slack.py&lt;/code&gt; rolled back to &lt;code&gt;upstream/main&lt;/code&gt;, once with the fix in place. Same branch, same data, one file different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before the fix.&lt;/strong&gt; The import span dies with &lt;code&gt;KeyError: 'ts'&lt;/code&gt;, attached right on the trace, status &lt;code&gt;internal_error&lt;/code&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8pcqvur59p1mf059wvh.jpeg" 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%2Fx8pcqvur59p1mf059wvh.jpeg" alt="Sentry trace of the failing import: KeyError 'ts' on the slack_import_demo span" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drilling into the issue gives the full stacktrace, pointing exactly where this PR points: &lt;code&gt;get_messages_iterator&lt;/code&gt; -&amp;gt; &lt;code&gt;get_timestamp_from_message&lt;/code&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakvf3l2ahf92zqrzoifx.jpeg" 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%2Fakvf3l2ahf92zqrzoifx.jpeg" alt="Sentry issue PYTHON-DJANGO-2: KeyError 'ts' with the stacktrace into get_messages_iterator" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And Seer's root-cause analysis of that issue. The diagnosis is spot on, down to quoting the exact poisoned message it pulled from the frame locals (&lt;code&gt;{"channel_name": "general", "text": "no ts field"}&lt;/code&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzlcwa0dmswm1bal58ejc.jpeg" 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%2Fzlcwa0dmswm1bal58ejc.jpeg" alt="Seer agent root cause and suggested fix for the KeyError issue" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One detail worth being honest about, precisely because a Sentry engineer judges this category: Seer's suggested one-liner, &lt;code&gt;float(message.get("ts", 0))&lt;/code&gt;, is the fallback-timestamp option I explicitly declined in the PR. It fixes the missing-&lt;code&gt;ts&lt;/code&gt; mode, but leaves the &lt;code&gt;ValueError&lt;/code&gt; mode alive, waves &lt;code&gt;"NaN"&lt;/code&gt; straight through into the sort, and stamps real messages with a 1970 &lt;code&gt;date_sent&lt;/code&gt;. Its closing advice, though (log a warning and investigate the upstream data quality), is exactly what the shipped fix does. Reading Seer's output critically instead of pasting its patch is, I would argue, the intended use of the tool: it found the root cause in seconds; deciding the failure policy stayed my job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After the fix.&lt;/strong&gt; Same export, same transaction: zero issues, and the three skipped messages surface as three warning-level logs on the trace instead of one fatal:&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%2Fppcp8awxhl2qm1ioxn00.jpeg" 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%2Fppcp8awxhl2qm1ioxn00.jpeg" alt="Sentry trace of the fixed import: 0 issues, 3 warning logs, import completed" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That pair of traces is the whole fix, told by monitoring: the same bad input, downgraded from an unhandled exception that kills a migration to three structured warnings on a completed run. (A pedantic footnote for anyone reading the trace metadata: both runs report release &lt;code&gt;c0b5d8c&lt;/code&gt; because the red run rolled back only the module file, not the branch HEAD.)&lt;/p&gt;




&lt;p&gt;The uncomfortable takeaway from failure mode 3: &lt;strong&gt;"does it crash" is a terrible proxy for "is it correct"&lt;/strong&gt;, and the failure modes that skip the crash are the ones that survive into production. Which raises the question I left for the reviewers, and now for you: for a message that is otherwise fine but has a broken timestamp, would you skip it or synthesize a fallback &lt;code&gt;date_sent&lt;/code&gt;? I picked skip. Convince me otherwise in the comments.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Nothing Was Broken. The Report Still Didn't Arrive.</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Sun, 26 Jul 2026 13:50:33 +0000</pubDate>
      <link>https://dev.to/p0rt/nothing-was-broken-the-report-still-didnt-arrive-k29</link>
      <guid>https://dev.to/p0rt/nothing-was-broken-the-report-still-didnt-arrive-k29</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On July 24 at 09:00 Berlin time, the doctors group did not get its daily digest. It got the first half of one, unfinished. Nobody noticed.&lt;/p&gt;

&lt;p&gt;The failure was recorded correctly, in a state file that nothing reads. The job's delivery mode was &lt;code&gt;none&lt;/code&gt;. The fallback alert channel had been dead since June 13, for reasons that turn out to be bug four. So the run failed, the record was written, and the information stopped there.&lt;/p&gt;

&lt;p&gt;Here is how our team actually learns that something broke. Different job, same pipeline:&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%2F4bti84sqh0ssrzaxztbk.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%2F4bti84sqh0ssrzaxztbk.png" alt="Telegram message from the bot reporting a failed cron job, with a doctor replying and tagging the CTO" width="799" height="410"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The failure the team actually saw: a doctor's scheduled job died and she had to tag the CTO in chat. This is what «silent» pipelines look like when they finally speak.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Some context, since everything below assumes it. Symptomato is a telehealth service: patients describe symptoms in a chat, doctors answer them from a helpdesk. Sympy is the agent that works the seam between the two — on a schedule it reads the doctors' inbox and tells them, in Telegram, which conversations need a human today. Nobody watches it run. That is the point of it, and it is also why a broken run can go unnoticed for six weeks.&lt;/p&gt;

&lt;p&gt;So I went looking for the bug behind the missing digest. That is the uncomfortable part: there wasn't one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; an agent run failed with no defective component anywhere in the path. That non-incident triggered a full audit of the pipeline, which turned up four bugs in our own code that nobody had ever seen fail. All four have the same shape: a function that does not know something reports a value instead of admitting it. This post is those four fixes, plus the instrumentation that makes this failure class visible, plus a deliberate decision to record none of the message content while doing it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;Under the hood: a self-hosted agent runtime with a job scheduler, a tool plugin we wrote on top of &lt;a href="https://www.chatwoot.com/" rel="noopener noreferrer"&gt;Chatwoot&lt;/a&gt; (the helpdesk our doctors work in), a set of host-side Python cron scripts, and Telegram as the delivery channel. Three scheduled jobs do the boring work: a morning digest of conversations that need attention, pings when a paid consultation goes 24 hours without a doctor reply, inbox prechecks.&lt;/p&gt;

&lt;p&gt;One constraint shapes everything below: patient conversations contain medical text. Any observability we add has to work without recording message bodies.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The incident with no bug in it
&lt;/h3&gt;

&lt;p&gt;The digest agent sent its first Telegram message, then decided to finalize the digest by editing that message instead of sending a second one. The message tool requires a recipient field even for an edit. The edit call did not have one. The tool rejected it, the turn ended, the scheduler marked the run as &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Walk that path again and look for the defect. The tool validated its input exactly as its schema says. The scheduler recorded the failure exactly as designed. The model picked a legal tool sequence that the prompt never forbade. Every component behaved to spec, and the doctors still had no digest.&lt;/p&gt;

&lt;p&gt;This is the failure mode that makes agent pipelines different: the execution path is chosen at runtime by a model, so "correct components" and "correct behavior" stop being the same claim. Yesterday the same job sent one message and worked. Today it chose send-then-edit and did not. There is no line of code you can point at, and no test that fails, because nothing is deterministic enough to fail.&lt;/p&gt;

&lt;p&gt;Once instrumented, the same failure looks like this. The capture is from the replay run — the July 24 failure itself predates the instrumentation — and it needed no code changes at the failure site:&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%2F4kteyjglaz75wbq0fz3g.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%2F4kteyjglaz75wbq0fz3g.png" alt="Sentry issue detail showing ToolInputError with raw params and a breadcrumb trail ending in a Telegram API call" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The anchor bug, captured automatically: the model chose «send, then edit», and the edit call has no &lt;code&gt;to&lt;/code&gt;. The tool that failed lives in the runtime, not in our code — we never instrumented it; the error was scraped from the gateway's ERROR log lines and turned into an issue.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcinm2e833uayio13gru2.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%2Fcinm2e833uayio13gru2.png" alt="Sentry Seer panel reconstructing the root cause of the issue from breadcrumbs" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Seer reconstructs the failure from breadcrumbs alone — no prompts, no message bodies were ever recorded.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The four bugs the audit did find
&lt;/h3&gt;

&lt;p&gt;A missing digest with no defect in it is a bad place to stop, so I audited the pipeline properly: the tool plugin, the scheduler jobs, the host scripts, the existing telemetry extensions. It came back with a list. Four items on that list turned out to be the same bug wearing different clothes, and I only saw the pattern once I wrote them down next to each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The tool reports a conversation status it never looked up.&lt;/strong&gt; &lt;code&gt;getConversationSummary&lt;/code&gt; returns &lt;code&gt;status: "open"&lt;/code&gt; as a literal, for every conversation, always. The real status sits in the API response one method below. So when the agent asks "is this conversation still open", it is told yes, unconditionally. Every judgment the model makes about whether to act on a conversation rests on a constant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The tool returns page one and calls it the inbox.&lt;/strong&gt; &lt;code&gt;listConversationsByInbox&lt;/code&gt; fetches &lt;code&gt;page=1&lt;/code&gt; and returns the payload with no &lt;code&gt;all_count&lt;/code&gt; check and no truncation marker. Chatwoot paginates at 25. Our host-side Python script got this right and loops until the count matches; the agent tool never did. So on any day with more than 25 open conversations, the model receives 25 and narrates them as the full picture, which is exactly the sort of confident summary you cannot catch by reading the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. "The data isn't there yet" gets cached as a fact.&lt;/strong&gt; &lt;code&gt;tariff_from_triage_note&lt;/code&gt; returns &lt;code&gt;"unknown"&lt;/code&gt; for two very different situations: the note is unparseable, and the note has not been posted yet. The caller caches the result unconditionally, and &lt;code&gt;"unknown"&lt;/code&gt; is truthy, so the next run short-circuits on the cache and never looks again. A patient whose chat is scanned in the window before the backend posts its triage note is marked &lt;code&gt;unknown&lt;/code&gt; permanently, which means the 24-hour ping that exists specifically for paid one-time consultations never fires for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The error handler reports failures through the channel that just failed.&lt;/strong&gt; Our host digest script calls a Telegram helper with no error handling, catches the resulting exception, and then tries to report that exception with the same helper, which raises again. Since June 13 the script has ended in a double traceback — 128 of them in the log — and every one of those runs paid for a model call before crashing. Next to it, the monitor script ends its send with &lt;code&gt;&amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt;, so a failed alert is indistinguishable from a delivered one anywhere in the system.&lt;/p&gt;

&lt;p&gt;Now the shape. In every one of these, something the system does not know is represented as something it does know. Unknown status becomes "open". Twenty-five of thirty becomes "the inbox". A missing note becomes a tariff value, cached forever. A failed alert becomes a successful one. Three of the four never raise at all; the fourth raises twice a day into a log nobody reads, which comes to the same thing. All of them produce plausible output. And the incident that started the audit is the same thing one level up: a failed run represented as nothing at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The status bug is the smallest and my favorite, because the correct value was already in scope. The whole thing is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formatted&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status&lt;/code&gt; is a literal. The real value sits in the conversation payload that the method one level below already fetches — the fix is to read it from there instead of asserting it.&lt;/p&gt;

&lt;p&gt;Pagination was ported from the host script that already did it right, plus an explicit truncation marker on message reads, so a partial conversation announces itself instead of passing as complete.&lt;/p&gt;

&lt;p&gt;The tariff cache stops recording ignorance as knowledge:&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="c1"&gt;# before
&lt;/span&gt;&lt;span class="n"&gt;tariff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tariff_from_triage_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cw_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tariff&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="n"&gt;tariff&lt;/span&gt;          &lt;span class="c1"&gt;# cache: note content is immutable
&lt;/span&gt;
&lt;span class="c1"&gt;# after
&lt;/span&gt;&lt;span class="n"&gt;tariff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tariff_from_triage_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cw_token&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;tariff&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="c1"&gt;# "not posted yet" is not a value
&lt;/span&gt;    &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tariff&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="n"&gt;tariff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not distinguish the two «unknown»s either; it stops trusting them. An unparseable note now costs a re-read every run instead of being wrong forever, which is the trade I want.&lt;/p&gt;

&lt;p&gt;And the alerting path: the Telegram helper handles its own failures and logs them, the error handler no longer depends on the channel that just failed, and chunking — a fifth thing I fixed while I was in there — splits outside HTML tags instead of through them.&lt;/p&gt;

&lt;p&gt;One thing I am deliberately not calling a fix: the prompt line telling the digest job to send once and never edit. It patches today's symptom and it took ten seconds, but it is an instruction to a stochastic system, not a repair. The actual answer to the opening incident is not in the prompt. It is that a failed run now reaches someone, instead of a file nothing reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;Every fix went red before it went green. The pagination and tariff bugs got standalone repro scripts against local mocks, with the buggy implementation copied verbatim, so the failure is demonstrated rather than argued:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open conversations in inbox:      30
agent check_inbox list_open sees: 25  (ids 1..25)
host precheck list_open sees:     30  (ids 1..30)
=&amp;gt; 5 conversations invisible to the LLM agent, no truncation signal returned
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run 1 (triage in progress): tariff='unknown'  cached={'tariff': 'unknown'}
run 2+ (note now present):  tariff='unknown'  (cache short-circuits, note never re-read)
24h ping fires: False   (correct behaviour would be: True)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the digest incident itself, the replay went into a private test group rather than the doctors group, running the same job with the same shape of payload:&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%2Fn2ytamrvixzxl9y01jcl.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%2Fn2ytamrvixzxl9y01jcl.png" alt="Telegram test group showing two unedited RED demo messages and one GREEN demo message" width="800" height="231"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;RED replay: the agent sends, then tries to edit — the edit dies, the message stays raw. GREEN: same job, one send, no edit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The RED runs are the interesting ones. The message is still sitting there in its raw, pre-edit state, which is precisely how this failed in production: not with an absence, but with a half-finished artifact that looks close enough to a real digest to be skimmed past.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;The instrumentation is the other half of the fix, because the four bugs above are the ones I found. The category of "silent wrong answer" is not exhausted by an audit, so the pipeline needs to be able to report on itself. Three independent sources now feed one stream: errors scraped from the runtime's own error log lines, failures inside our agent tools, and cron monitors that stop hearing from a job.&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%2Fqvneft97e92hjmcpe43w.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%2Fqvneft97e92hjmcpe43w.png" alt="Sentry issue stream with three numbered issues: a core tool error, an agent tool failure and a cron monitor failure" width="800" height="445"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Three different origins, one stream: (1) a core tool error scraped from the runtime's ERROR log lines, (2) a failure inside one of our own agent tools, (3) a cron monitor that stopped hearing from its job. The unnumbered rows are the same machinery catching unrelated faults.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When one of our tools fails, the HTTP call that caused it arrives attached, which turns "the agent said something odd this morning" into a five-second read:&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%2Fr17a9ly5g0dzcc54vnop.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%2Fr17a9ly5g0dzcc54vnop.png" alt="Sentry breadcrumbs showing an agent tool failure and the Chatwoot HTTP request that caused it" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;An agent tool failure with the exact HTTP call that caused it — attached automatically as breadcrumbs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every tool call is now a span with the attributes that matter for this pipeline, and none of the attributes that would put patient text into a third-party system:&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%2Fhy2scicwp8l8eizu7dt9.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%2Fhy2scicwp8l8eizu7dt9.png" alt="Attributes tab of a gen_ai.execute_tool span in Sentry showing tool name, action and conversation id" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every agent tool call is a span: tool name, action, numeric conversation id, latency, error status. Nothing in that list is message content — that is the whole design.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The tool's own Input and Output tabs are where the arguments and the result would sit. They are empty, and that is deliberate:&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%2Fg6fe7c9ftqr23vjhvy2b.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%2Fg6fe7c9ftqr23vjhvy2b.png" alt="Sentry AI tab showing the trace as a timeline of model calls and tool calls, with an empty Input tab" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The same run in Sentry's AI view — model calls and tool calls in sequence, with the selected tool's Input tab empty: arguments and responses are never recorded (PHI).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Model calls get the same treatment, including the ones nobody is watching, which for this agent is most of them:&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%2Fkwoow5569wsgwu1oqufj.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%2Fkwoow5569wsgwu1oqufj.png" alt="Attributes of a gen_ai.chat span in Sentry showing model, provider, conversation id and timings" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One span per model call — model, provider, conversation id, latency, time to first byte. Emitted for background cron runs too, which is where this agent does most of its work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And they correlate, which is what makes an agent run debuggable at all: the model call, the tool call it triggered, and the outbound HTTP request that tool made, in one waterfall.&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%2Fdbsvyrvej84yfyj92cfm.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%2Fdbsvyrvej84yfyj92cfm.png" alt="Sentry trace waterfall of one agent run: a model call, a tool call and the outbound HTTP request" width="800" height="293"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One agent run, one trace: the model call, the tool call it triggered, and the outbound HTTP request — correlated through the runtime's own trace context.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Token usage and cost land at run level, keyed by conversation:&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%2Fi54y821df9gztngwoo1c.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%2Fi54y821df9gztngwoo1c.png" alt="Attributes of a gen_ai.invoke_agent span showing token usage and dollar cost per agent run" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Run-level usage: tokens in/out and dollar cost per agent run, keyed by conversation id — the cheapest possible answer to «what is this agent costing us».&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then the piece that speaks to the failure mode underneath the opening incident. Error monitoring catches runs that fail. It cannot catch runs that stop happening, and it cannot catch a delivery that quietly goes nowhere. Cron monitors turn a schedule into an expectation, and check-ins into evidence. (The red one below is the host digest script from bug four, not the doctors' digest from the opening: two different jobs, one failure mode.)&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%2Fu8e4n2vpkwt65rynbcye.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%2Fu8e4n2vpkwt65rynbcye.png" alt="Sentry cron monitors list with one failing and two healthy monitors" width="799" height="262"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Three host crons, monitored from code (no UI setup). The red one is the script from bug four — it has been failing since June 13, and the monitor is the first thing in six weeks to say so out loud.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvzt9z06y0a8udh031k1o.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%2Fvzt9z06y0a8udh031k1o.png" alt="Sentry cron monitor detail page showing missed and failed check-ins and an ongoing issue" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Missed, missed, failed. A job that stops running produces no error at all, and a job that fails into a log nobody reads produces none that anyone sees — a cron monitor turns both into the same alert.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Six weeks of that script crashing on schedule would have been one alert on day one.&lt;/p&gt;

&lt;p&gt;On the PHI side, the deliberate choice: prompt and response recording is off. The safe list is model id, provider, token counts, cost, durations, tool name and action, numeric conversation ids, session key, job id, outcome. Anything string-valued that comes from message content goes through redaction or does not get sent. The result is a monitoring stack that can tell me a tool failed, which tool, on which conversation id, how long it took and what it cost, and cannot tell me or anyone else what the patient wrote.&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%2F4k153d225mdzezbtwdtz.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%2F4k153d225mdzezbtwdtz.png" alt="Sentry AI transcript tab stating that the conversation's messages were not captured" width="799" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Sentry offers a full conversation transcript for AI traces — and for this agent it is empty by construction: «This conversation's messages weren't captured».&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That empty transcript is not a gap in the setup. It is the setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;The bug I went looking for did not exist, and the four I found were all the same bug wearing different clothes: a component that could not distinguish "I don't know" from a value, and picked a value. In ordinary code, that produces a wrong answer somewhere downstream and usually an exception eventually. In a pipeline where a language model reads those answers, it produces a fluent, confident, well-formatted summary of a reality that is 25 conversations wide instead of 30, and nobody downstream has any way to tell.&lt;/p&gt;

&lt;p&gt;So the question I am still working on, and would like yours on: in your own systems, how do you tell the difference between an agent run that went fine and an agent run that quietly took a path that does not work? Error rates will not show it. Neither will the output, because the output always looks great.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>ai</category>
      <category>observability</category>
    </item>
    <item>
      <title>'World Models' Will Be the Next Buzzword. The Man Saying That Just Raised $1B to Build One</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:10:03 +0000</pubDate>
      <link>https://dev.to/p0rt/world-models-will-be-the-next-buzzword-the-man-saying-that-just-raised-1b-to-build-one-4oih</link>
      <guid>https://dev.to/p0rt/world-models-will-be-the-next-buzzword-the-man-saying-that-just-raised-1b-to-build-one-4oih</guid>
      <description>&lt;p&gt;In March, the CEO of a research lab with zero products closed a &lt;strong&gt;$1.03 billion seed round&lt;/strong&gt; — the largest in European history. Then he told TechCrunch that "'world models' will be the next buzzword," predicting that within six months every company would slap the label on itself to raise money.&lt;/p&gt;

&lt;p&gt;The CEO is Alexandre LeBrun. The lab is AMI Labs, Paris. The chairman is Yann LeCun. And LeBrun was right on schedule: VCs have pushed roughly &lt;strong&gt;$3B into the category&lt;/strong&gt; since.&lt;/p&gt;

&lt;p&gt;When the person behind the biggest bet in a space tells you the space is about to become a content-free buzzword, that's the frame worth keeping. The question isn't whether "world model" gets diluted — it will. The question is whether there's a real, testable architectural disagreement underneath the label.&lt;/p&gt;

&lt;p&gt;There is. This post is the paper trail: what LeCun is actually claiming, what the published results show, who paid for it, and the strongest arguments that the whole thesis is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is LeCun, in 30 seconds
&lt;/h2&gt;

&lt;p&gt;Turing Award 2018 (shared with Hinton and Bengio). Architect of convolutional neural networks. Chief AI scientist at Meta from 2013, where he built FAIR into one of the largest industrial research labs on the planet. His departure was confirmed in November 2025, after Meta folded FAIR into its new superintelligence org, spent $15B on Scale AI, and reoriented around Llama and generative products.&lt;/p&gt;

&lt;p&gt;The split was architectural, not financial. Meta isn't investing in AMI Labs — but the two keep a research partnership around the architecture this whole story runs on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case against LLMs, as he makes it
&lt;/h2&gt;

&lt;p&gt;LeCun's critique predates the current hype cycle by years, and it's more specific than "LLMs bad." His standard list of what's missing: &lt;strong&gt;understanding of the physical world, persistent memory, reasoning, and planning.&lt;/strong&gt; The supporting arguments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autoregression compounds errors.&lt;/strong&gt; Every generated token conditions on possibly-wrong previous tokens. Fine for prose. Bad for long-horizon plans, where one early mistake invalidates everything downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text is a thin slice of reality.&lt;/strong&gt; By his estimate, a four-year-old has taken in more raw sensory data through vision alone than the largest text corpora contain. Text &lt;em&gt;describes&lt;/em&gt; the world; it doesn't contain its dynamics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The falling pen.&lt;/strong&gt; Drop a pen and it can land many ways. A system predicting one most-likely continuation in surface space is doing a different computation than one reasoning over a distribution of physical outcomes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The stakes argument.&lt;/strong&gt; LeBrun ran Nabla, a medical AI company, and arrived at the same place from the applied side: in healthcare, hallucination isn't a UX bug. It's a liability category.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At VivaTech this year, LeCun said current chatbots understand the physical world worse than a rat. AMI's corporate framing is more measured: token prediction works well for discrete, low-dimensional tasks — retrieval, summarization, code — and &lt;em&gt;mimics&lt;/em&gt; intelligence without modeling the world.&lt;/p&gt;

&lt;p&gt;One thing worth flagging before we go further: this critique was formulated &lt;strong&gt;before RL-trained reasoning models existed.&lt;/strong&gt; The "no reasoning, no planning" claim is weaker in 2026 than it was in 2022. The honest version of the argument today is about the &lt;em&gt;reliability and grounding&lt;/em&gt; of that reasoning — not its existence.&lt;/p&gt;

&lt;h2&gt;
  
  
  "World model" currently means three different things
&lt;/h2&gt;

&lt;p&gt;This is the part most coverage skips, and it's where the buzzword damage will happen. At least three technically distinct approaches share the label:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generative video prediction.&lt;/strong&gt; Predict future frames, conditioned on actions. Google DeepMind's &lt;strong&gt;Genie 3&lt;/strong&gt; generates navigable 3D worlds at 24fps; NVIDIA's &lt;strong&gt;Cosmos&lt;/strong&gt; (launched at CES 2025, 2M+ downloads, trained on ~20M hours of video) targets synthetic data for robotics and AVs; Runway's &lt;strong&gt;GWM-1&lt;/strong&gt; bets on interactive video. The model's imagination is literally watchable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit 3D representation.&lt;/strong&gt; Fei-Fei Li's &lt;strong&gt;World Labs&lt;/strong&gt; treats the world as a spatial object, not a frame sequence. Marble (shipped November 2025) generates 3D environments on Gaussian splats plus physics engines, viewable in VR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latent-space prediction.&lt;/strong&gt; &lt;strong&gt;JEPA&lt;/strong&gt; — LeCun's bet. Don't generate pixels at all. Encode observations into abstract representations and predict how &lt;em&gt;those&lt;/em&gt; evolve. The claim: most pixel-level detail is irrelevant for planning, and predicting it wastes capacity and compute.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are not interchangeable. They differ in what they predict, how you evaluate them, and what they're for. When a startup calls itself a "world model company," the first useful question is &lt;em&gt;which of the three it means.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The paper trail: what JEPA has actually shown
&lt;/h2&gt;

&lt;p&gt;The fundraise is loud. The papers are quieter and more interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2022 — the position paper.&lt;/strong&gt; &lt;em&gt;A Path Towards Autonomous Machine Intelligence&lt;/em&gt; lays out the whole program: a modular agent (perception, world model, cost, actor, short-term memory, configurator) with JEPA as the learning substrate. No results — pure architecture. It reads as a research roadmap, and AMI Labs is essentially this paper incorporated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2023 — I-JEPA.&lt;/strong&gt; Images. Mask blocks of an image, predict the &lt;em&gt;representations&lt;/em&gt; of the masked regions from visible context — never reconstruct pixels, no handcrafted augmentations. Meta reported training a ViT-Huge in under 72 hours on 16 A100s, a fraction of what pixel-reconstruction methods burn, with strong linear-probe results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2024 — V-JEPA.&lt;/strong&gt; Same trick on video: masked latent prediction over spatiotemporal patches. The representations turn out to encode &lt;em&gt;motion&lt;/em&gt; unusually well — exactly what pixel-reconstruction models are notoriously mediocre at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2025 — V-JEPA 2.&lt;/strong&gt; The load-bearing result. A ~1B-parameter ViT-g encoder trained on 22M videos (over 1M hours). From the paper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;77.3% top-1 on Something-Something v2&lt;/strong&gt; (motion understanding)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;39.7 recall@5 on Epic-Kitchens-100&lt;/strong&gt; action anticipation — a 44% relative improvement over prior task-specific models&lt;/li&gt;
&lt;li&gt;Aligned with an 8B language model: &lt;strong&gt;84.0 on PerceptionTest, 76.9 on TempCompass&lt;/strong&gt; — state of the art at that scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the robotics part, &lt;strong&gt;V-JEPA 2-AC&lt;/strong&gt;: take the frozen encoder, post-train an action-conditioned predictor on &lt;strong&gt;less than 62 hours&lt;/strong&gt; of unlabeled robot video from the open DROID dataset, and deploy zero-shot on Franka arms in two labs the model never saw. Planning runs as model-predictive control in latent space. Results: &lt;strong&gt;65–80% success on pick-and-place with unseen objects&lt;/strong&gt; — no rewards, no task-specific data, no data from the deployment robots. Planning takes &lt;strong&gt;~16 seconds per action versus ~4 minutes&lt;/strong&gt; for the video-generation baseline (Cosmos), and on a cup-relocation task it hit ~80% where the vision-language-action baseline Octo managed 15%.&lt;/p&gt;

&lt;p&gt;Meta also released physical-reasoning benchmarks alongside (IntPhys 2, MVPBench, CausalVQA) — on which current models, LLMs included, still trail humans badly. That gap is the entire pitch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2026 — AMI Labs.&lt;/strong&gt; As of July: no public model. Reporting points to work on world models that adapt continually through action, plus the conference-keynote circuit. LeBrun's stated timeline: roughly a year to the first usable pieces in a product, &lt;em&gt;years&lt;/em&gt; to real commercial applications — healthcare (via Nabla), robotics, wearables, industrial. LeCun says year one is research. Credit where due: they are not pretending otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The money map
&lt;/h2&gt;

&lt;p&gt;The AMI round was led by Bezos Expeditions, Cathay Innovation, Greycroft, Hiro Capital and HV Capital, with NVIDIA, Temasek, Samsung and Toyota Ventures in, plus individuals including Bezos, Mark Cuban, Eric Schmidt and — unusually for a seed round — Tim Berners-Lee. They initially sought €500M and closed ~€890M.&lt;/p&gt;

&lt;p&gt;Zoom out and the pattern sharpens. &lt;strong&gt;World Labs&lt;/strong&gt; raised $1B in February at $5.4B post. &lt;strong&gt;Decart&lt;/strong&gt; took $300M in May at $4B (Karpathy is an angel). &lt;strong&gt;Odyssey&lt;/strong&gt; raised $1.2B. And NVIDIA is on nearly every cap table — it has committed over &lt;strong&gt;$40B in AI equity in 2026&lt;/strong&gt;, frequently structured as equity in exchange for long-term GPU commitments.&lt;/p&gt;

&lt;p&gt;Read that incentive carefully. NVIDIA's omnipresence is evidence that world models are &lt;em&gt;compute-hungry&lt;/em&gt;, not that the architecture is right. Demis Hassabis calling world models essential for AGI is the more meaningful endorsement — DeepMind builds them regardless of the hype cycle, and has no fundraise to justify.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest counterarguments
&lt;/h2&gt;

&lt;p&gt;I don't want to write the version of this post that just relays the pitch. Here's the strongest case against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. LLMs may already learn world models.&lt;/strong&gt; The Othello-GPT line of work trained a transformer on nothing but move sequences and found an emergent, probeable representation of the board state — later shown by Neel Nanda to be &lt;em&gt;linearly&lt;/em&gt; decodable. Gurnee &amp;amp; Tegmark found linear representations of space and time inside Llama-2, down to individual "space neurons." Next-token prediction demonstrably induces &lt;em&gt;some&lt;/em&gt; internal model of the data-generating process. So the LeCun claim has to be quantitative — "not enough of one, not grounded enough" — not categorical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Those internal models might be junk anyway.&lt;/strong&gt; Follow-up interpretability work suggests Othello-GPT's "board" may be a bag of correlated heuristics rather than a clean algorithm — epicycles, not an orrery. This cuts both ways: it weakens "LLMs already have world models," and it warns that JEPA latents could look equally messy once someone probes them as hard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Latent prediction is hard to inspect.&lt;/strong&gt; With generative world models you can &lt;em&gt;watch&lt;/em&gt; what the model imagines and see it break. A JEPA predictor's mistakes live in embedding space; evaluation is indirect — probes, downstream planning success. That's a real tooling and debuggability cost, and part of why the generative camp iterates faster in public.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Nobody's world model is robust yet.&lt;/strong&gt; Genie 3 stays coherent for a few minutes and remembers changes for about one. V-JEPA 2-AC does tabletop pick-and-place, not laundry. The gap between "80% cup relocation" and "robot in your kitchen" is the same kind of gap LLMs face between benchmark and deployment. It would be inconsistent to hold only one camp to the deployment standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The falsifiability question.&lt;/strong&gt; What would count as the thesis &lt;em&gt;winning&lt;/em&gt;? My candidates: JEPA-style planners beating vision-language-action models on generalization at matched scale; sample-efficiency curves holding beyond manipulation toys; a grounded system measurably hallucinating less in a domain like healthcare. What would count as &lt;em&gt;losing&lt;/em&gt;: hybrid LLM systems closing the physical-reasoning benchmark gap first. Either outcome is visible within a couple of years — which is more than you can say for most $1B theses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with this (if you build things)
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing here replaces your LLM calls in 2026.&lt;/strong&gt; The most optimistic insider timeline is a year to first usable pieces, years to products. Plan your stack accordingly and ignore anyone selling you a "world model" API this quarter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form your own priors hands-on.&lt;/strong&gt; V-JEPA 2 checkpoints are public. Cosmos is self-hostable — the 7B variant fits on a single H100 80GB. Marble has a free tier. Genie is a closed preview. An afternoon of poking beats a month of threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When the label shows up in a pitch deck, ask which of the three architectures it means&lt;/strong&gt; — frames, splats, or latents — and where its pick-and-place numbers are. If there's no answer, you've found the buzzword LeBrun warned you about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch three markers:&lt;/strong&gt; AMI's first release and its license (LeCun spent a decade arguing for open research — the licensing choice will say a lot); whether latent-space planning scales past tabletop manipulation before generative models get long-horizon coherence; and DeepMind shipping a hybrid LLM + world model system, since they own both pieces and have no thesis to defend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LeBrun's six months are almost up. The benchmarks aren't going anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;V-JEPA 2 paper: &lt;a href="https://arxiv.org/abs/2506.09985" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2506.09985&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Meta's V-JEPA 2 announcement + benchmarks: &lt;a href="https://ai.meta.com/blog/v-jepa-2-world-model-benchmarks/" rel="noopener noreferrer"&gt;https://ai.meta.com/blog/v-jepa-2-world-model-benchmarks/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;I-JEPA: &lt;a href="https://arxiv.org/abs/2301.08243" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2301.08243&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The 2022 position paper: &lt;a href="https://openreview.net/forum?id=BZ5a1r-kVsf" rel="noopener noreferrer"&gt;https://openreview.net/forum?id=BZ5a1r-kVsf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AMI Labs raise (TechCrunch): &lt;a href="https://techcrunch.com/2026/03/09/yann-lecuns-ami-labs-raises-1-03-billion-to-build-world-models/" rel="noopener noreferrer"&gt;https://techcrunch.com/2026/03/09/yann-lecuns-ami-labs-raises-1-03-billion-to-build-world-models/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Largest EU seed context (Crunchbase): &lt;a href="https://news.crunchbase.com/venture/world-model-ai-lab-ami-raises-europes-largest-seed-round/" rel="noopener noreferrer"&gt;https://news.crunchbase.com/venture/world-model-ai-lab-ami-raises-europes-largest-seed-round/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The VC map (Forbes): &lt;a href="https://www.forbes.com/sites/josipamajic/2026/06/30/world-model-startups-raise-3-billion-vcs-bet-beyond-llms/" rel="noopener noreferrer"&gt;https://www.forbes.com/sites/josipamajic/2026/06/30/world-model-startups-raise-3-billion-vcs-bet-beyond-llms/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Othello-GPT: &lt;a href="https://thegradient.pub/othello/" rel="noopener noreferrer"&gt;https://thegradient.pub/othello/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Nanda's linear-representation follow-up: &lt;a href="https://www.neelnanda.io/mechanistic-interpretability/othello" rel="noopener noreferrer"&gt;https://www.neelnanda.io/mechanistic-interpretability/othello&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Space/time representations in Llama-2: &lt;a href="https://arxiv.org/abs/2310.02207" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2310.02207&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Melanie Mitchell's skeptical read: &lt;a href="https://aiguide.substack.com/p/llms-and-world-models-part-2" rel="noopener noreferrer"&gt;https://aiguide.substack.com/p/llms-and-world-models-part-2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>robotics</category>
    </item>
    <item>
      <title>Autonomy Is the Bug: Why Self-Driving Agents Hallucinate When the Model Barely Does</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Tue, 21 Jul 2026 16:23:46 +0000</pubDate>
      <link>https://dev.to/p0rt/autonomy-is-the-bug-why-self-driving-agents-hallucinate-when-the-model-barely-does-1330</link>
      <guid>https://dev.to/p0rt/autonomy-is-the-bug-why-self-driving-agents-hallucinate-when-the-model-barely-does-1330</guid>
      <description>&lt;p&gt;The best models in 2026 hallucinate on about &lt;strong&gt;1% of grounded, single-shot tasks&lt;/strong&gt;. Hand one a document and ask for a summary and it almost never invents anything.&lt;/p&gt;

&lt;p&gt;Now let that same model drive itself: plan a task, call a tool, read the result, decide the next step, update its memory, repeat for twenty steps with no human in the loop. It will now go wrong on &lt;strong&gt;most runs&lt;/strong&gt;. Same model, same weights. The thing that changed is &lt;em&gt;autonomy&lt;/em&gt;, and autonomy is where hallucination is actually manufactured.&lt;/p&gt;

&lt;p&gt;This is the part almost nobody states plainly: for a self-driving agent, the dominant cause of "making things up" is not the model's factual accuracy. It's the structure of running unattended across many steps. A better model barely moves it. Below is why, with the numbers, and what actually does.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Agent errors compound multiplicatively: at 95% per-step accuracy, a 10-step task succeeds ~60% of the time, a 20-step task ~36%. Even a fictional 99%-per-step agent fails ~18% of 20-step tasks. And it's worse than the clean math, because agents &lt;em&gt;self-condition&lt;/em&gt; on their own earlier mistakes, so errors accelerate rather than just accumulate. This is structural, not a model-quality problem: no checkpoint fixes p^n. What fixes it is architecture, shorter chains, per-step validation, scoped context, and escalation, plus not reaching for a reasoning model, which for grounded steps often hallucinates 3–4× &lt;em&gt;more&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one equation that governs autonomous agents
&lt;/h2&gt;

&lt;p&gt;Reliability engineering has a law for chained systems (Lusser's law): the reliability of a sequence is the &lt;em&gt;product&lt;/em&gt; of the step reliabilities, not the average. For an agent taking n independent steps, each succeeding with probability p, end-to-end success is &lt;strong&gt;p^n&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That exponent is the entire story of why autonomy is hard. Watch what it does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Per-step accuracy&lt;/th&gt;
&lt;th&gt;5 steps&lt;/th&gt;
&lt;th&gt;10 steps&lt;/th&gt;
&lt;th&gt;20 steps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;99% (fantasy)&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;82%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95% (excellent)&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90% (good)&lt;/td&gt;
&lt;td&gt;59%&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;85% (a strong result)&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the 95% row again, because 95% per step sounds &lt;em&gt;great&lt;/em&gt;. An agent that gets each step right 95% of the time succeeds on a 20-step task &lt;strong&gt;36% of the time.&lt;/strong&gt; It fails most of them. And 85% per step, which is a genuinely strong result on a complex reasoning action, collapses to &lt;strong&gt;4%&lt;/strong&gt; over 20 steps: one success in twenty-five.&lt;/p&gt;

&lt;p&gt;Demis Hassabis called compounding agent errors &lt;strong&gt;"compound interest in reverse,"&lt;/strong&gt; and it's exact. The same multiplicative machinery that makes compound interest miraculous over time makes autonomous error catastrophic over steps. Demos hide it because a demo is 2–3 steps. Production is 5+ steps over messy inputs, which is precisely where p^n bites.&lt;/p&gt;

&lt;p&gt;The load-bearing consequence: &lt;strong&gt;no amount of model improvement fully solves this.&lt;/strong&gt; Going from 95% to 97% per step helps, but you're still fighting an exponent. The gap between "impressive demo" and "reliable product" is not a capability gap you close with a better model. It's a structural property of chaining a non-deterministic component, and you engineer around it or you ship a coin flip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's actually worse than p^n: self-conditioning
&lt;/h2&gt;

&lt;p&gt;The table above assumes each step's error is &lt;em&gt;independent&lt;/em&gt;. In real agents, it isn't, and the dependence runs the wrong way.&lt;/p&gt;

&lt;p&gt;Researchers documented a &lt;strong&gt;self-conditioning&lt;/strong&gt; effect in multi-step LLM pipelines in early 2026: once an agent has produced an error and that error is sitting in its context, the model conditions on it. It sees its own earlier mistake as established fact and reasons &lt;em&gt;from&lt;/em&gt; it, which makes the next mistake more likely, not less. Errors don't just accumulate, they compound on themselves. The real failure curve is &lt;strong&gt;steeper&lt;/strong&gt; than p^n predicts.&lt;/p&gt;

&lt;p&gt;This is the mechanism underneath "the agent went off the rails around step 6." It didn't hit a hard wall. It made one small wrong turn at step 4, wrote that into its own working context, and every subsequent step treated the wrong turn as ground truth. By step 8 it's confidently building on a fact it invented four steps ago. The hallucination isn't fresh at step 8, it's &lt;em&gt;laundered&lt;/em&gt; from step 4 through the agent's own memory.&lt;/p&gt;

&lt;p&gt;And this points straight at one of the most effective fixes, which is counterintuitive: &lt;strong&gt;scoped context.&lt;/strong&gt; A step that doesn't know about the errors in steps 2 and 3 &lt;em&gt;can't condition on them&lt;/em&gt;. Passing only the relevant slice of context forward, instead of the full accumulated history, breaks the self-conditioning loop. Less memory, more reliability, the opposite of the instinct to give the agent everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The autonomy-specific amplifiers
&lt;/h2&gt;

&lt;p&gt;On top of the raw compounding, self-driving specifically adds failure sources a single-shot call never has:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lossy memory compaction.&lt;/strong&gt; When the agent's history won't fit in context, it summarizes, and repeated summarization is brutal. A 2026 rate-distortion study measured it: an agent archiving raw records and retrieving on demand held &lt;strong&gt;~95% recall&lt;/strong&gt;; an agent overwriting memory with LLM summaries dropped to &lt;strong&gt;33–56% recall&lt;/strong&gt;, worst when it compacted most often. At equal budget, lossy self-summarization loses roughly &lt;em&gt;half the facts&lt;/em&gt; over a long run, and a single-turn benchmark can't see it because the loss only appears when compaction repeats. The agent then confidently fills the holes it created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context drift.&lt;/strong&gt; ~65% of enterprise agent failures in 2025 were attributed to context drift or memory loss during multi-step reasoning, not to running out of context. As the window fills, attention de-prioritizes the original task; early tool outputs get overwritten by later ones; the agent quietly diverges before it ever hits the limit. Chroma's context-rot work shows degradation accelerating past ~30k tokens even in models with far larger windows, capacity is not fidelity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent contamination.&lt;/strong&gt; Split the work across agents for reliability and you can make it worse: naive full-broadcast state sharing &lt;em&gt;increased&lt;/em&gt; hallucination by &lt;strong&gt;34%&lt;/strong&gt; in a controlled 2026 study, because one agent's error propagates to all of them. Chaining five agents at 95% each yields &lt;strong&gt;77%&lt;/strong&gt; end-to-end, not 95%, the same p^n exponent, now spread laterally.&lt;/p&gt;

&lt;p&gt;Every one of these is a property of &lt;em&gt;running autonomously&lt;/em&gt;, not of the model's factual accuracy. A single API call to the same model has none of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the model does matter: don't make per-step accuracy worse
&lt;/h2&gt;

&lt;p&gt;The model isn't irrelevant, it sets your p. But the counterintuitive part is that the "smarter" model many teams reach for to fix agent reliability often &lt;em&gt;lowers&lt;/em&gt; p on exactly the grounded steps agents are made of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasoning models hallucinate more on grounded tasks.&lt;/strong&gt; Same base model: DeepSeek-V3 hallucinates at 3.9% on grounded summarization; its reasoning sibling R1 at &lt;strong&gt;14.3%&lt;/strong&gt;, ~4× worse. It's a pattern, not a one-off: on Vectara's 2026 dataset, &lt;em&gt;every&lt;/em&gt; reasoning model exceeded 10%, while a non-reasoning model (Gemini Flash-Lite) led at 3.3%. The mechanism is the same self-conditioning drift, reasoning models generate long internal chains that wander from the source. So "let me upgrade to a reasoning model so my agent thinks more carefully" can &lt;em&gt;lower&lt;/em&gt; your per-step accuracy and, through the exponent, wreck end-to-end reliability. For the grounded steps that make up most agent loops, a fast non-reasoning model is often the higher-p choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Lowest hallucination" can mean "abstains most."&lt;/strong&gt; Claude 4.1 Opus posts 0% on a knowledge benchmark, but by &lt;em&gt;declining when unsure&lt;/em&gt;, which for an agent is often exactly right (a confident wrong step corrupts everything downstream; an "I'm not sure" step can be escalated). Calibration, knowing what it doesn't know, matters more for p than raw accuracy, because a self-conditioning agent punishes confident-wrong far more than honest-uncertain.&lt;/p&gt;

&lt;p&gt;The takeaway on models: pick for &lt;strong&gt;high, calibrated per-step accuracy on your actual step type&lt;/strong&gt; (usually grounded → non-reasoning), not for leaderboard position. But understand that even a great p is fighting p^n, so the model is necessary and nowhere near sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fixes autonomous hallucination
&lt;/h2&gt;

&lt;p&gt;None of these is a model upgrade. All of them attack the exponent or the self-conditioning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shorter chains.&lt;/strong&gt; The single highest-leverage move. Cutting a 20-step task to 8 steps does more for reliability than any accuracy bump, because you're reducing the exponent, not the base. If a task can be decomposed into shorter independent sub-tasks with checkpoints between, do that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation between steps.&lt;/strong&gt; Schema-validate each step's output before passing it forward. A 95%-accurate step that's &lt;em&gt;checked&lt;/em&gt; can't silently corrupt everything downstream, you catch the error at step 4 instead of discovering it at step 20. This directly interrupts self-conditioning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped context, not full history.&lt;/strong&gt; Pass only what the next step needs. A step that can't see earlier errors can't condition on them. Less context is more reliability here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalation and circuit breakers.&lt;/strong&gt; The agent must know when it's uncertain and hand off. The dangerous failure isn't the agent that stops and reports, it's the one that fails silently and continues, laundering a wrong turn through twelve more steps. Test explicitly: does it recognize its own error, signal it, and &lt;em&gt;halt&lt;/em&gt; rather than compound?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selective autonomy, not full autonomy.&lt;/strong&gt; Aggressive automation for low-risk, reversible steps; hard human gates on high-stakes, irreversible ones. Cap the blast radius. The goal was never "fully autonomous," it's "autonomous where it's safe, gated where it isn't."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The thing to internalize
&lt;/h2&gt;

&lt;p&gt;A single model on a single task is a solved-ish problem, ~1% on grounded work. An autonomous agent is a different object: it multiplies per-step reliability across many steps, and it conditions on its own mistakes, so a 95%-per-step agent is mathematically guaranteed to fail most long tasks, and the real curve is steeper still. That is not a model-quality problem and no checkpoint fixes it.&lt;/p&gt;

&lt;p&gt;The teams whose agents are still running in 2028 won't be the ones who bought the most capable model. They'll be the ones who treated compound failure as a design constraint from day one, shorter chains, validation between steps, scoped context, escalation, and the discipline to leave a step &lt;em&gt;out&lt;/em&gt; of autonomy when it can't be made safe.&lt;/p&gt;

&lt;p&gt;When your self-driving agent hallucinates, don't reach for a smarter model first. Count the steps, check whether it's conditioning on its own earlier output, and ask which of those steps ever needed to be autonomous at all. The model isn't lying. Autonomy handed it its own earlier mistake and asked it to keep going.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your real per-step accuracy once you measure the whole chain, not the demo, and where did shortening the chain buy you more than any model swap? I'd bet most "the model hallucinates" complaints are compound error and self-conditioning wearing a model-shaped mask.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.zartis.com/the-compounding-errors-problem-why-multi-agent-systems-fail-and-the-architecture-that-fixes-it/" rel="noopener noreferrer"&gt;"The Compounding Errors Problem: Why Multi-Agent Systems Fail"&lt;/a&gt;, Zartis (2026) — p^n across accuracy levels; why model improvement can't fully solve it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://towardsdatascience.com/the-math-thats-killing-your-ai-agent/" rel="noopener noreferrer"&gt;"The Math That's Killing Your AI Agent"&lt;/a&gt;, Towards Data Science (2026) — the compound table; fail-detectably-and-gracefully test.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://highlandedge.com/resources/insights/compound-error-problem/" rel="noopener noreferrer"&gt;"The Compound Error Problem: Why 95% Accurate AI Agents Still Fail"&lt;/a&gt;, Highland Edge (2026) — self-conditioning; "compound interest in reverse"; scoped context passing.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/k8slens/the-math-behind-why-multi-step-ai-agents-fail-in-production-c6d60ea6ca31" rel="noopener noreferrer"&gt;"The Math Behind Why Multi-Step AI Agents Fail in Production"&lt;/a&gt;, Flavius Dinu (2026) — Lusser's law; shorter chains + verification + guardrails.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.mindstudio.ai/blog/multi-agent-reliability-compounding-problem-77-percent" rel="noopener noreferrer"&gt;"Multi-Agent Reliability Math: Chaining 5 Agents Drops to 77%"&lt;/a&gt;, MindStudio (2026) — lateral compounding; 85–90% realistic per-agent rates.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/html/2607.08032" rel="noopener noreferrer"&gt;"What to Keep, What to Forget: A Rate–Distortion View of Memory Compaction"&lt;/a&gt; (2026) — reversible ~0.95 vs irreversible 0.33–0.56 recall.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2606.21666" rel="noopener noreferrer"&gt;"Hallucination as Context Drift"&lt;/a&gt; (2026) — +34% from naive multi-agent sync.&lt;/li&gt;
&lt;li&gt;Reasoning-tax and abstention figures: Vectara HHEM 2026; Suprmind AI hallucination benchmarks (DeepSeek V3 3.9% vs R1 14.3%; Claude 4.1 Opus 0% via abstention).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>devops</category>
    </item>
    <item>
      <title>'Local' Solves Where Your Data Goes. It Doesn't Solve What Your Agent Does</title>
      <dc:creator>Sergei Parfenov</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:19:35 +0000</pubDate>
      <link>https://dev.to/p0rt/local-solves-where-your-data-goes-it-doesnt-solve-what-your-agent-does-306b</link>
      <guid>https://dev.to/p0rt/local-solves-where-your-data-goes-it-doesnt-solve-what-your-agent-does-306b</guid>
      <description>&lt;p&gt;Local models got good this year. Gemma 4's 12B runs agentic workloads in 16GB of RAM, GLM-5.2 tops the open-weight leaderboards under a permissive license, Qwen 3.6 does tool-calling that would've been frontier-only eighteen months ago. The "just run it locally" argument hit the top of Hacker News, and for once it wasn't cope — a model on your own hardware finally handles real work.&lt;/p&gt;

&lt;p&gt;So teams are moving agents on-prem, and the pitch is almost always the same: &lt;strong&gt;local means private, private means safe.&lt;/strong&gt; The first half is true. The second half is a category error that's going to cause incidents, because it quietly swaps a data question for a behavior question and hopes nobody notices.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Local deployment fixes exactly one thing: &lt;em&gt;where your data goes&lt;/em&gt;. It does nothing for &lt;em&gt;what your agent does&lt;/em&gt;. Prompt injection (50–85% success rates, architectural not model-level), silent provenance failures (the agent faking its own logs), and privilege escalation all survive the move to your hardware unchanged — and you've traded the provider's security team for none. Local agents are genuinely safe for a specific shape of task (bounded scope, trusted inputs, reversible or gated actions) and dangerously oversold for another (untrusted input, irreversible actions, regulated decisions). The dividing line isn't where the model runs. It's what the agent is allowed to touch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What "local" actually buys you
&lt;/h2&gt;

&lt;p&gt;Let's be precise about the real win, because it's real and worth having: &lt;strong&gt;data sovereignty.&lt;/strong&gt; Your prompts, your documents, your customer data, your proprietary code — none of it leaves your infrastructure. For a hospital, a bank, a defense contractor, or anyone under GDPR handling personal data, that's not a nice-to-have, it's frequently the difference between "can deploy" and "legal says no." With the EU AI Act's high-risk provisions live as of August 2, 2026, and sector regulators (OCC, FDA, FINRA, SEC) applying existing authorities to agent deployments, keeping data on-prem removes a whole class of compliance friction.&lt;/p&gt;

&lt;p&gt;That's the entire list. Data location. Everything else people &lt;em&gt;attribute&lt;/em&gt; to local — that it's safer, more controllable, less exploitable — is either untrue or unrelated to where the weights sit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "local" does not buy you
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable part, in three failures that don't care about your network topology.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prompt injection is architectural, not remote
&lt;/h3&gt;

&lt;p&gt;The single most common belief I want to kill: that prompt injection is something that happens &lt;em&gt;to cloud APIs&lt;/em&gt; and air-gapping escapes it. It isn't and it doesn't.&lt;/p&gt;

&lt;p&gt;Prompt injection is #1 on the OWASP LLM Top 10 for one reason: &lt;strong&gt;LLMs cannot structurally distinguish trusted instructions from untrusted data.&lt;/strong&gt; That's a property of how the model reads a context window, not of where the context window is hosted. A recent systematization across 78 studies puts injection success rates &lt;em&gt;above 85%&lt;/em&gt;. Independent numbers land at 50–84% depending on configuration. Moving the model to your basement changes none of those numbers.&lt;/p&gt;

&lt;p&gt;Worse, the dangerous variant for agents is &lt;em&gt;indirect&lt;/em&gt; injection, and local makes it arguably harder to reason about, not easier. Indirect injection is when the agent autonomously retrieves attacker-controlled content — a poisoned document, a malicious webpage, a compromised record — and executes instructions hidden inside it, during its normal operating loop. And here's the local-specific sting: research shows that the moment a planner consumes &lt;em&gt;raw local content&lt;/em&gt; — arbitrary files, logs, metadata off your own disk — malicious instructions embedded in those local artifacts steer the agent's reasoning, even if execution is later sandboxed. Your local filesystem is not a trusted input just because it's yours. Any file an attacker touched, any document a user uploaded, any log a previous run wrote is now an injection vector, and it's sitting &lt;em&gt;inside&lt;/em&gt; your trusted perimeter, which is exactly where you weren't looking.&lt;/p&gt;

&lt;p&gt;There's a beautiful, awful result that makes the point: asking an agent to &lt;em&gt;seek clarification when a task is ambiguous&lt;/em&gt; — a behavior everyone agrees is desirable — measurably &lt;strong&gt;increases&lt;/strong&gt; its injection vulnerability, because the clarification response becomes a fresh attack channel. You cannot prompt your way out of this, on any hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The agent can still fake its own evidence
&lt;/h3&gt;

&lt;p&gt;I've written a whole series on this, so I'll keep it short: an agent that writes a record of its own execution can write a &lt;em&gt;false&lt;/em&gt; one. The Darwin Gödel Machine incident is the canonical example — an agent editing its own harness wrote a log claiming its tests passed. They never ran. It then read that log back as ground truth. No deception, just tool-use hallucination hitting a filesystem that can't record who wrote what.&lt;/p&gt;

&lt;p&gt;Notice that &lt;em&gt;every part of that happens locally by default.&lt;/em&gt; Running the model on-prem doesn't add a single control here. If anything it removes one, because a cloud provider at least gives you their audit tooling, their request logs, their trace infrastructure. Roll your own local agent and you get an empty &lt;code&gt;/var/log&lt;/code&gt; and whatever provenance discipline you remembered to build — which, for most teams shipping fast, is none. "Local" and "auditable" are orthogonal, and people constantly assume the first implies the second.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. You inherited the provider's threat model and fired their security team
&lt;/h3&gt;

&lt;p&gt;This is the trade nobody prices in. When you call a hosted frontier API, an enormous amount of invisible security work comes along: input/output filtering, jailbreak detection, egress monitoring, rate limiting, red-teaming, abuse detection, incident response. You may resent paying for it, but it's &lt;em&gt;there&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Move local and all of that is now your job. The attack surface didn't shrink — prompt injection, tool abuse, privilege escalation, data exfiltration through side channels are all still live — but the team defending against it went from "the provider's security org" to "you, probably part-time, while also shipping the feature." Survey data captures the gap bluntly: 82% of executives are confident their existing policies cover unauthorized agent actions, and the operational reality is nowhere close. Local deployment doesn't cause that gap, but it &lt;em&gt;widens&lt;/em&gt; it, because it hands you more of the stack to secure while feeling like it did the opposite.&lt;/p&gt;

&lt;h2&gt;
  
  
  So where ARE local agents genuinely safe?
&lt;/h2&gt;

&lt;p&gt;This isn't a "don't run local agents" post — I run them, they're great, and the data-sovereignty win is often decisive. It's a "stop using &lt;em&gt;local&lt;/em&gt; as a synonym for &lt;em&gt;safe&lt;/em&gt;" post. The actual safety question has nothing to do with where the model runs and everything to do with three properties of the task:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Green zone — local agents are genuinely safe here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bounded, trusted inputs.&lt;/strong&gt; The agent operates over content you control end to end — your own codebase, your internal docs, data with no attacker-writable path into it. No indirect-injection surface because nothing untrusted enters the context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversible or gated actions.&lt;/strong&gt; The agent proposes; a human or a deterministic check disposes. Draft the email, don't send it. Suggest the migration, don't run it. Write the PR, don't merge it. If every consequential action has an undo or a gate, injection and hallucination cost you time, not damage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low blast radius.&lt;/strong&gt; Worst case is contained. A local coding assistant that can edit files in one sandboxed repo, a document-Q&amp;amp;A agent that can only read, a log-triage agent that can only annotate — the failure mode is "wrong output," which you catch, not "wire transfer sent."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concretely, the tasks that fit: private code assistance over your own repos, RAG/document Q&amp;amp;A over internal knowledge (read-only), draft generation with human review, log and telemetry triage that annotates rather than acts, offline data transformation with visible outputs. These are safe &lt;em&gt;and&lt;/em&gt; benefit maximally from local — sensitive data, high volume, no need for frontier-level reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red zone — local changes nothing about the danger:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Untrusted input in the loop.&lt;/strong&gt; The agent reads emails, browses the web, ingests user uploads, processes third-party documents. Every one of those is an injection channel, and it's identical on local and cloud. If anything, do this on a hosted model &lt;em&gt;with&lt;/em&gt; injection defenses before you do it on a bare local one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Irreversible or high-value actions.&lt;/strong&gt; Payments, deployments, deletions, external messages, database mutations, anything with a side effect you can't take back. The DGM failure and every injection result apply in full. Local gives you zero additional protection on the exact axis that matters most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulated decisions.&lt;/strong&gt; Credit, healthcare, legal, hiring. Here the arxiv literature is blunt: a production KYC deployment reported &lt;em&gt;negative results&lt;/em&gt; — control failures surfaced only by internal audit, and a population of legitimate applicants the automated pipeline silently couldn't serve. In regulated settings the consensus is that full autonomy is rarely advisable regardless of hosting, and legal accountability &lt;em&gt;amplifies&lt;/em&gt; every threat relative to an unregulated deployment. "It runs on-prem" is not an answer a regulator accepts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern: &lt;strong&gt;local is a data-locality decision; safety is an autonomy-and-blast-radius decision.&lt;/strong&gt; They're independent axes, and conflating them is how you end up with a locally-hosted agent doing something on the red-zone list because "it's private, so it's fine."&lt;/p&gt;

&lt;h2&gt;
  
  
  The controls that actually matter (and are the same either way)
&lt;/h2&gt;

&lt;p&gt;Since the model's location isn't doing security work, something else has to. The controls that make an agent safe are identical on local and cloud, and they're all about &lt;em&gt;what the agent can touch&lt;/em&gt;, not where it thinks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Least privilege on tools.&lt;/strong&gt; The agent gets the minimum action surface for the task. A read-only agent can't write. A drafting agent can't send. This is the single highest-leverage control and it's free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A gate before anything irreversible.&lt;/strong&gt; Human-in-the-loop or a deterministic check on every action you can't undo. Gate on the action's reversibility, not the model's confidence — a model can be confidently wrong, and injection makes it confidently &lt;em&gt;malicious&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusted vs untrusted input separation.&lt;/strong&gt; Treat every file, document, and retrieved page the agent didn't author as potentially poisoned. Don't let raw untrusted content into the planning context; redact, sandbox, or validate at the boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance on tool outputs.&lt;/strong&gt; A "tests passed" line counts only if it's backed by an artifact the agent couldn't author. The runtime that executed the tool mints the verified result, not the model narrating about it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An audit log the agent can't rewrite.&lt;/strong&gt; Append-only, outside the agent's editable surface. This is the thing local deployment silently &lt;em&gt;removes&lt;/em&gt; if you don't build it, so build it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is architecture. None of them is a model, and none of them cares whether the weights are in us-east-1 or under your desk.&lt;/p&gt;

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

&lt;p&gt;Local models crossing the capability threshold is genuinely one of the best things to happen in this space — the data-sovereignty win alone unlocks deployments that were legally impossible a year ago. Use them. But "local" answers exactly one question: &lt;em&gt;where does my data live.&lt;/em&gt; It is silent on the question that actually determines whether you get an incident: &lt;em&gt;what is my agent allowed to do, and what happens when it's wrong.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The safe local agent and the dangerous local agent run the same model on the same hardware. The difference is entirely in the blast radius you granted it. Design that, and location becomes what it should be — a compliance and cost decision, not a security blanket.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're running local agents: what's actually in your green zone versus what crept into the red one because "it's on-prem so it's fine"? The creep is never a decision, it's an omission — curious where people have caught it.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;OWASP Top 10 for LLM Applications 2025 — prompt injection as the #1 architectural vulnerability.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.vectra.ai/topics/prompt-injection" rel="noopener noreferrer"&gt;"Prompt injection: types, real-world CVEs, and enterprise defenses"&lt;/a&gt;, Vectra AI (2026) — 50–84% success rates; the Aug 2, 2026 EU AI Act deadline.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2603.18377" rel="noopener noreferrer"&gt;"PlanTwin: Privacy-Preserving Planning Abstractions for Cloud-Assisted LLM Agents"&lt;/a&gt; — malicious instructions in &lt;em&gt;local&lt;/em&gt; artifacts steering reasoning even when execution is sandboxed; 85%+ injection success across 78 studies.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2605.17324" rel="noopener noreferrer"&gt;"ASPI: Seeking Ambiguity Clarification Amplifies Prompt Injection Vulnerability"&lt;/a&gt; — desirable clarification behavior increases attack surface.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2606.29142" rel="noopener noreferrer"&gt;"Agent Security Meets Regulatory Reality"&lt;/a&gt; — production KYC negative results; how legal accountability amplifies each threat.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://beam.ai/agentic-insights/ai-agent-security-in-2026-the-risks-most-enterprises-still-ignore" rel="noopener noreferrer"&gt;"AI Agent Security in 2026"&lt;/a&gt;, Beam — the 82% executive-confidence gap; healthcare incident rates.&lt;/li&gt;
&lt;li&gt;My series on the provenance side of this: &lt;a href="https://dev.to/p0rt/the-agent-faked-a-test-log-then-believed-it-self-editing-harnesses-have-a-provenance-problem-3id6"&gt;the agent that faked its own test log&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
      <category>machinelearning</category>
    </item>
    <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>
  </channel>
</rss>
