<?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: Li Zhuojun</title>
    <description>The latest articles on DEV Community by Li Zhuojun (@lizhuojunx86).</description>
    <link>https://dev.to/lizhuojunx86</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%2F3990737%2Fecf68c91-8302-4910-8f26-ae04f9489100.jpg</url>
      <title>DEV Community: Li Zhuojun</title>
      <link>https://dev.to/lizhuojunx86</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lizhuojunx86"/>
    <language>en</language>
    <item>
      <title>I checked nine free RFC 3161 endpoints against the EU Trusted List. None is qualified. Five of the companies are.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Fri, 16 Oct 2026 22:07:04 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/i-checked-nine-free-rfc-3161-endpoints-against-the-eu-trusted-list-none-is-qualified-five-of-the-djc</link>
      <guid>https://dev.to/lizhuojunx86/i-checked-nine-free-rfc-3161-endpoints-against-the-eu-trusted-list-none-is-qualified-five-of-the-djc</guid>
      <description>&lt;p&gt;On 16 October 2026 I sent one timestamp request to each of nine public, free RFC 3161 endpoints and looked up the certificate that signed each reply in the EU Trusted List. All nine answered. All nine tokens verified (message imprint and nonce echoed back correctly). None of the nine is a qualified timestamp under eIDAS.&lt;/p&gt;

&lt;p&gt;The list I checked against: LOTL sequence 397, issued 2026-10-15, 29 member-state lists downloaded and signature-verified, 1,443 &lt;code&gt;TSA/QTST&lt;/code&gt; entries of which 915 are &lt;code&gt;granted&lt;/code&gt; today. Snapshot SHA-256 &lt;code&gt;d1700e6a…c85d&lt;/code&gt;, LOTL SHA-256 &lt;code&gt;a36d0053…70e6&lt;/code&gt;. Hungary and Portugal didn't download (TLS cut the connection), so a Hungarian or Portuguese endpoint would have come back "not checked", not "not qualified". None of the nine is in either country.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Signing certificate&lt;/th&gt;
&lt;th&gt;Issued by&lt;/th&gt;
&lt;th&gt;Qualified?&lt;/th&gt;
&lt;th&gt;Granted QTST entries on the list under this company's name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FreeTSA &lt;code&gt;freetsa.org/tsr&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="http://www.freetsa.org" rel="noopener noreferrer"&gt;www.freetsa.org&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="http://www.freetsa.org" rel="noopener noreferrer"&gt;www.freetsa.org&lt;/a&gt; (self-issued)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigiCert &lt;code&gt;timestamp.digicert.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;DigiCert SHA256 RSA4096 Timestamp Responder 2026 1&lt;/td&gt;
&lt;td&gt;DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;9 (NL, DigiCert Europe Netherlands B.V.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sectigo &lt;code&gt;timestamp.sectigo.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Sectigo Public Time Stamping Signer R37&lt;/td&gt;
&lt;td&gt;Sectigo Public Time Stamping CA R41&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;1 (ES, Sectigo (Europe) SL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GlobalSign &lt;code&gt;timestamp.globalsign.com/tsa/r45standard&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Globalsign R45 TSA for CodeSign 202510&lt;/td&gt;
&lt;td&gt;GlobalSign Offline R45 Timestamping CA 2025&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;4 (BE, GlobalSign NV/SA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certum &lt;code&gt;time.certum.pl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Certum Timestamp 2026&lt;/td&gt;
&lt;td&gt;Certum Timestamping 2021 CA&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;3 (PL, Asseco Data Systems S.A.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL.com &lt;code&gt;ts.ssl.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SSL.com Timestamping Unit 2025 E1&lt;/td&gt;
&lt;td&gt;SSL.com Timestamping Issuing RSA CA R1&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entrust &lt;code&gt;timestamp.entrust.net/TSS/RFC3161sha2TS&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Sectigo Public Time Stamping Signer R37&lt;/td&gt;
&lt;td&gt;Sectigo Public Time Stamping CA R41&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;0 (1 entry, ES, withdrawn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple &lt;code&gt;timestamp.apple.com/ts01&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Timestamp Signer PV02&lt;/td&gt;
&lt;td&gt;Apple Timestamp Certification Authority&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actalis &lt;code&gt;timestamp.actalis.it&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Actalis Time-Stamping Authority TSU 2&lt;/td&gt;
&lt;td&gt;Actalis Time Stamping CA G1&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;2 (IT, Actalis S.p.A.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Qualified" here is the mechanical test from ETSI TS 119 615: the key that signed the token, or the CA that issued it, has to match a &lt;code&gt;TSA/QTST&lt;/code&gt; entry whose status was &lt;code&gt;granted&lt;/code&gt; at the token's &lt;code&gt;genTime&lt;/code&gt;. For all nine, neither the signing key nor the issuer name appears anywhere among the 1,443 qualified-timestamp entries. Not as granted, not as withdrawn. The lookup never reached the status step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last column is the point
&lt;/h2&gt;

&lt;p&gt;I ran this same check in August and wrote it up with one example of a QTSP whose free endpoint isn't qualified: Actalis. That undersold it. Five of the nine companies hold granted qualified-timestamp entries on the list today. They just don't hand them out at the free URL.&lt;/p&gt;

&lt;p&gt;Actalis is the cleanest case because both halves sit in the same country under the same company name. The free endpoint signs with &lt;code&gt;Actalis Time-Stamping Authority TSU 2&lt;/code&gt;, issued by &lt;code&gt;Actalis Time Stamping CA G1&lt;/code&gt;. The Italian list has two granted entries for Actalis S.p.A.: &lt;code&gt;Actalis TSA 1 EIDAS&lt;/code&gt; and &lt;code&gt;Actalis EU Qualified TimeStamp CA G1&lt;/code&gt;. Different CAs. The library returns &lt;code&gt;False&lt;/code&gt; for the free one, and that is the right answer.&lt;/p&gt;

&lt;p&gt;The other four are the same shape with an extra twist: the entity on the list is often not the entity on the certificate. The free Sectigo token is signed under &lt;code&gt;O=Sectigo Limited, C=GB&lt;/code&gt;; the qualified entry is &lt;code&gt;Sectigo (Europe) SL&lt;/code&gt; on the Spanish list. GlobalSign's free token says &lt;code&gt;GlobalSign nv-sa, C=BE&lt;/code&gt;, and Belgium does list four granted GlobalSign qualified timestamping CAs, none of which issued this certificate. Certum's token is signed by Asseco Data Systems, which has three granted entries in Poland, under different CAs. DigiCert's nine granted entries are in the Netherlands, under DigiCert Europe Netherlands B.V., with QuoVadis names on the certificates.&lt;/p&gt;

&lt;p&gt;So "is this provider a QTSP?" is the wrong question. Qualification attaches to a service entry, which is a public key plus a status timeline. The right question is whether the key that signed your token is on the list, and was granted at that second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that moved in eight weeks
&lt;/h2&gt;

&lt;p&gt;Entrust's URL still answers, and the token it returns is signed by &lt;code&gt;Sectigo Public Time Stamping Signer R37&lt;/code&gt;, serial &lt;code&gt;0xe74ef255…a315&lt;/code&gt;. That's byte-for-byte the same certificate &lt;code&gt;timestamp.sectigo.com&lt;/code&gt; returned a few seconds earlier. Entrust sold its public certificate business to Sectigo, and the endpoint followed. A config file that says &lt;code&gt;entrust&lt;/code&gt; now produces Sectigo evidence. Nothing broke; nothing told you either.&lt;/p&gt;

&lt;p&gt;Apple's signing certificate was issued on 2026-10-15 and expires on 2026-11-26. Forty-two days. Whatever certificate you look up next month will not be this one. Between my August run (LOTL 390) and this one (LOTL 397) the list itself moved seven sequence numbers; Ireland, unreachable in August, downloaded fine, and Hungary and Portugal didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs you, legally
&lt;/h2&gt;

&lt;p&gt;Article 41(1) of eIDAS says an electronic timestamp can't be refused as evidence just because it is electronic or not qualified. So every token in that table is admissible. Article 41(2) gives qualified timestamps, and only qualified timestamps, a presumption of accuracy of the time and integrity of the data. With a free token, you prove the clock and the integrity; with a qualified one, the other side has to disprove them.&lt;/p&gt;

&lt;p&gt;For most logging that's fine. It stops being fine when the record is one you'd put in front of a regulator. The AI Act's Article 12 logging duty applies from 2 December 2027 for Annex III systems and 2 August 2028 for Annex I systems, after Regulation (EU) 2026/1744 moved the dates, and the text of the article didn't change. If you're building toward that, the free tier of a QTSP is not the QTSP. I haven't found a free qualified endpoint, from these nine or anywhere else. (If you know of one, I'd like to be wrong.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Look it up when you stamp, not when you're asked
&lt;/h2&gt;

&lt;p&gt;The failure mode I care about isn't picking a free endpoint. It's picking a paid qualified one, and then checking its status three years later when a dispute arrives. That lookup answers today's question against today's list. Keys rotate, entries get withdrawn, the Entrust URL becomes a Sectigo URL. The only time the answer is knowable is the moment the token comes back, and the answer has to be written down then, next to the token, with the hash of the list it was checked against. That is what &lt;a href="https://github.com/lizhuojunx86/tg-attest" rel="noopener noreferrer"&gt;tg-attest&lt;/a&gt; does on its write path, as a three-valued field (&lt;code&gt;True&lt;/code&gt; / &lt;code&gt;False&lt;/code&gt; / &lt;code&gt;None&lt;/code&gt; for "couldn't check"), with no network call at stamping time. The measurement above is that code run by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;Nine endpoints is nine endpoints, picked because they are the ones people paste into &lt;code&gt;signtool&lt;/code&gt; and &lt;code&gt;openssl ts&lt;/code&gt;. Certificates rotate (Apple's lasts six weeks; GlobalSign's is named &lt;code&gt;202510&lt;/code&gt;) and the LOTL moved seven times in eight weeks, so this table is true for 16 October 2026 and I make no claim about any other date. The "company name" column is my substring match on the list's provider names, not a corporate register; it's there to show the pattern, not to settle who owns whom. The two unreachable member states are unreachable today, not permanently, and five list entries (one French, four Norwegian) carried certificates my parser rejected and were skipped. A verdict of "not qualified" is about Article 41(2)'s presumption only; nothing here says these tokens are technically weak. All nine echoed my hash and nonce correctly. I didn't run full chain verification for this post.&lt;/p&gt;

&lt;p&gt;I kept every &lt;code&gt;.tsr&lt;/code&gt; from this run, and you don't need them: request a token from any of the nine yourself and &lt;code&gt;openssl ts -reply -in &amp;lt;file&amp;gt; -token_in -text&lt;/code&gt; shows the same certificate names without installing anything of mine. The snapshot was built with &lt;code&gt;python -m tg_attest.eutl_build&lt;/code&gt;, which verifies the LOTL against the six certificates in OJ C/2026/1944 and each national list against the LOTL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regulation (EU) No 910/2014 (eIDAS), &lt;a href="https://eur-lex.europa.eu/eli/reg/2014/910/oj" rel="noopener noreferrer"&gt;Article 41&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ETSI TS 119 615 V1.3.1, procedures for using and interpreting EU Member States' trusted lists, §4.6&lt;/li&gt;
&lt;li&gt;OJ C/2026/1944, &lt;a href="https://eur-lex.europa.eu/eli/C/2026/1944/oj" rel="noopener noreferrer"&gt;LOTL signing certificates&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Regulation (EU) 2026/1744 (Digital Omnibus), moving the AI Act Chapter III application dates&lt;/li&gt;
&lt;li&gt;Entrust, &lt;a href="https://www.entrust.com/knowledgebase/ssl/time-stamp-url" rel="noopener noreferrer"&gt;public certificate business transition to Sectigo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;A routing audit for your team, fixed price.&lt;/strong&gt; The method in this series runs on any Claude Code trace store. Send me 30 days of your team's usage records (model, tokens, timestamps, agent and session ids; I'll send a one-line &lt;code&gt;jq&lt;/code&gt; filter that drops prompt and answer text before anything leaves your machine) and within ten working days you get one number and a five-page report: what share of your spend ran on a tier your own routing policy would not have chosen, what that cost at list price, and which components caused it. On my own 26,131 traces the number was 22.6% and $1,248.13, all of it on subagents and none on the main thread. US$1,500, flat. Write to &lt;a href="mailto:info@zhuojun.li"&gt;info@zhuojun.li&lt;/a&gt; with the subject "routing audit".&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>compliance</category>
      <category>python</category>
    </item>
    <item>
      <title>10 of 12 new LLMs remember which earnings beat from just a name and a date. Jev doesn't.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:00:07 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/10-of-12-new-llms-remember-which-earnings-beat-from-just-a-name-and-a-date-jev-doesnt-c8i</link>
      <guid>https://dev.to/lizhuojunx86/10-of-12-new-llms-remember-which-earnings-beat-from-just-a-name-and-a-date-jev-doesnt-c8i</guid>
      <description>&lt;p&gt;Earlier this week I &lt;a href="https://dev.to/lizhuojunx86/does-jev-remember-2023-a-naive-test-says-yes-at-p-0001-a-within-company-test-says-no-5fja"&gt;tested whether Jev remembers how 2023 earnings turned out&lt;/a&gt;. It doesn't. So I ran the same pre-registered test on twelve current models through OpenRouter and on von, an open Jev-style model. Ten of the twelve remember. Shown two reports from the same company and the same season, one from 2023 and one from 2024, GPT-6 Astra picked the one that beat consensus 91% of the time. It had the company name and the date, nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;3,708 earnings announcements from 250 S&amp;amp;P 500 companies, January 2023 to August 2026.&lt;/li&gt;
&lt;li&gt;Each model sees the company, ticker, exchange, sector and announcement date, and answers three questions with a probability: did EPS beat consensus, did the stock beat the S&amp;amp;P 500 over the 2-day reaction, and did it over the next 20 trading days.&lt;/li&gt;
&lt;li&gt;For each company I take its 2023 and 2024 reports from the same quarter where the outcome differs, and count how often the model scores the right one higher. That's a within-company AUC. 0.5 means it can't tell.&lt;/li&gt;
&lt;li&gt;The protocol, the model list and every request were hashed and timestamped with OpenTimestamps before the first call of each round.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Within-company AUC (95% CI)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;0.904 (0.886 to 0.921)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.8 Flash&lt;/td&gt;
&lt;td&gt;0.799 (0.776 to 0.822)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5.5&lt;/td&gt;
&lt;td&gt;0.766 (0.742 to 0.792)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Sol&lt;/td&gt;
&lt;td&gt;0.691 (0.665 to 0.717)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok 4.7&lt;/td&gt;
&lt;td&gt;0.603 (0.573 to 0.634)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;0.587 (0.561 to 0.613)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8 Max&lt;/td&gt;
&lt;td&gt;0.578 (0.550 to 0.605)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Luna&lt;/td&gt;
&lt;td&gt;0.573 (0.547 to 0.601)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4.1 Flash&lt;/td&gt;
&lt;td&gt;0.551 (0.521 to 0.578)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;von 1.2 (open Jev-style)&lt;/td&gt;
&lt;td&gt;0.541 (0.512 to 0.569)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K3&lt;/td&gt;
&lt;td&gt;0.538 (0.511 to 0.567)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5.3&lt;/td&gt;
&lt;td&gt;0.531 (0.501 to 0.560)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jev 1.13&lt;/td&gt;
&lt;td&gt;0.504 (0.478 to 0.530)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4 Pro&lt;/td&gt;
&lt;td&gt;0.504 (0.476 to 0.533)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8 Flash&lt;/td&gt;
&lt;td&gt;0.498 (0.470 to 0.527)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.1 70B&lt;/td&gt;
&lt;td&gt;0.490 (0.464 to 0.517)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After Holm correction, every new API model except DeepSeek V4 Pro and Qwen3.8 Flash shows memory, and GLM-5.3 only just clears the bar. von shows some too. The Sonnet 5, Jev and Llama rows come from the first study.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means if you backtest with an LLM
&lt;/h2&gt;

&lt;p&gt;If your pipeline passes a ticker and a date to one of the top models, part of the backtest is the model's memory. GPT-6 Astra, Gemini 3.8 Flash and Claude Opus 5.5 are the clearest cases. Two cheap defences: strip names and dates when the task allows, and test your own setup with a within-company comparison like this one before you trust the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I didn't expect
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Price and size don't predict it inside a family. DeepSeek V4 Pro showed nothing while V4.1 Flash did, and Qwen3.8 Max remembers while Qwen3.8 Flash doesn't.&lt;/li&gt;
&lt;li&gt;von, a small open model built to stand in for Jev, shows a little memory (0.541) where Jev shows none (0.504). Running a model locally doesn't make it clean.&lt;/li&gt;
&lt;li&gt;The cheap tier remembers too. GPT-6 Luna scored 0.573, and all 3,708 of its calls cost US$0.15.&lt;/li&gt;
&lt;li&gt;Six endpoints won't switch reasoning off, including GPT-6 Astra, Gemini 3.8 Flash and Claude Opus 5.5. They ran at the lowest effort, so I can't separate their memory from what a little reasoning adds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One prompt, 250 companies, one window. A model that shows nothing here may still remember in another setting.&lt;/li&gt;
&lt;li&gt;Each row is the model OpenRouter served on 24 September 2026. Versions change.&lt;/li&gt;
&lt;li&gt;With about 1,200 pairs per model, the test can detect a pooled AUC of about 0.546. Anything smaller is invisible.&lt;/li&gt;
&lt;li&gt;GPT-6 Astra stopped at 1,464 of its 3,708 calls when the account ran out of credit. I finished it after seeing that partial result (0.889), so that choice wasn't blind. The requests didn't change, and every call ran on 24 September.&lt;/li&gt;
&lt;li&gt;The timestamps prove less than they sound. Bitcoin block 968402, which anchors the first-round freeze, was mined at 14:36 UTC, four minutes after that round's last call. On its own the proof shows the protocol existed by then, not that it came first. The second round's block is stamped two minutes after its first call, by which time 5,486 of its 14,832 requests had gone out.&lt;/li&gt;
&lt;li&gt;API cost: US$55 for the first eight models and US$73 for the second four, where GPT-6 Astra alone was US$48 and Claude Opus 5.5 US$24.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce
&lt;/h2&gt;

&lt;p&gt;Protocols, deviations, code, every answer and all three freeze proofs: &lt;a href="https://github.com/lizhuojunx86/llm-memory-audit/tree/main/studies/2026-09-leaderboard" rel="noopener noreferrer"&gt;github.com/lizhuojunx86/llm-memory-audit&lt;/a&gt;. The earnings data comes from FMP and can't be redistributed; the scripts rebuild it with your own key.&lt;/p&gt;

&lt;p&gt;If you've caught this in your own backtests, I'd like to hear how.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Does Jev remember 2023? A naive test says yes at p = 0.001. A within-company test says no.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Thu, 24 Sep 2026 12:58:48 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/does-jev-remember-2023-a-naive-test-says-yes-at-p-0001-a-within-company-test-says-no-5fja</link>
      <guid>https://dev.to/lizhuojunx86/does-jev-remember-2023-a-naive-test-says-yes-at-p-0001-a-within-company-test-says-no-5fja</guid>
      <description>&lt;p&gt;I asked Jev about 12,533 US earnings announcements, 38,956 calls in all, to find out whether it remembers how they turned out. The test most people would write first says it does, at p = 0.001. A test that compares each company only with itself says it doesn't: shown two announcements from the same firm and the same season, one from 2023 and one from 2024, Jev can't tell the beat from the miss (within-company AUC 0.506, 95% CI 0.472 to 0.540). The whole run cost $0.93.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ask
&lt;/h2&gt;

&lt;p&gt;Jev (TypeSafe, released 2026-09-15) answers typed questions with probabilities instead of text. TypeSafe says it's trained on synthetic data and reasons from the state you pass in. Outside observers suspect an open-weight LLM underneath, and people are already running it over historical earnings and calling the output a backtest.&lt;/p&gt;

&lt;p&gt;If the model remembers what happened after each event, that backtest scores its memory. I've spent the past six months on that failure mode in LLM pipelines (it's what &lt;a href="https://github.com/lizhuojunx86/traceguard" rel="noopener noreferrer"&gt;TraceGuard&lt;/a&gt; is for), so a brand-new model with a claim about its training data was worth a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that would have fooled me
&lt;/h2&gt;

&lt;p&gt;My first draft of the protocol compared Jev's accuracy on 2023–2024 events with its accuracy on April to August 2026 events, for the same 812 companies. If it remembers, it should do better on the old ones.&lt;/p&gt;

&lt;p&gt;On the finished data, that contrast gives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"did the stock beat the S&amp;amp;P 500 over the next 20 trading days?", name and date only: AUC 0.555 on 2023–24 against 0.515 on 2026. Gap 0.040, 95% CI 0.005 to 0.076, p = 0.009.&lt;/li&gt;
&lt;li&gt;the same question with the earnings numbers included, named minus anonymized: gap 0.015, 95% CI 0.006 to 0.024, p = 0.001.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a headline, and I couldn't have defended it. Before freezing anything I had a separate Claude instance attack the protocol. It simulated 200 worlds where the model had no memory at all, only stable hunches about sectors and company size, plus a shock shared by each sector in each season. Re-running that simulation for this post, the contrast flagged "leakage" at the 5% level in 46 of the 200 worlds (23%). The 2026 window holds about two earnings seasons, one market regime (its 20-day base rate is 36%, against 41% to 53% in other periods) and no fourth-quarter reports. Anything that differs between the two windows moves the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test I ran instead
&lt;/h2&gt;

&lt;p&gt;Take one company. Take its Q2 announcement in 2023 and its Q2 announcement in 2024. Show Jev the name, ticker, sector and date, nothing about results, and ask whether it beat consensus. If exactly one of the two beat, a model that remembers ranks that one higher. A model that only knows "this firm usually beats" gives both the same score.&lt;/p&gt;

&lt;p&gt;Two details close the obvious gaps. Every answer has the month's average answer subtracted first, so a model that is simply vaguer about recent dates gains nothing. And both events in a pair come from the same quarter of the year, so seasonal habits cancel. The statistic is the share of pairs where the event that beat got the higher score. No memory means 0.5.&lt;/p&gt;

&lt;p&gt;The protocol, code, event list, labels and every request payload (53,788 of them, 38,956 for Jev) were hashed into one file, and the file was sent to OpenTimestamps seconds before the first real call. One thing did change afterwards, for the control model only, and it's logged below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Result (95% CI)&lt;/th&gt;
&lt;th&gt;Pairs&lt;/th&gt;
&lt;th&gt;Could detect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H1&lt;/td&gt;
&lt;td&gt;beat vs miss, name and date only&lt;/td&gt;
&lt;td&gt;0.506 (0.472 to 0.540)&lt;/td&gt;
&lt;td&gt;915&lt;/td&gt;
&lt;td&gt;0.552&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H2&lt;/td&gt;
&lt;td&gt;stock beat the S&amp;amp;P over the 2-day reaction&lt;/td&gt;
&lt;td&gt;0.479 (0.454 to 0.503)&lt;/td&gt;
&lt;td&gt;1,619&lt;/td&gt;
&lt;td&gt;0.539&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H3&lt;/td&gt;
&lt;td&gt;stock beat the S&amp;amp;P over the next 20 days&lt;/td&gt;
&lt;td&gt;0.483 (0.458 to 0.507)&lt;/td&gt;
&lt;td&gt;1,462&lt;/td&gt;
&lt;td&gt;0.541&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H4&lt;/td&gt;
&lt;td&gt;does adding name and date help a 20-day backtest&lt;/td&gt;
&lt;td&gt;−0.006 (−0.021 to 0.009)&lt;/td&gt;
&lt;td&gt;1,462&lt;/td&gt;
&lt;td&gt;0.059&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H5&lt;/td&gt;
&lt;td&gt;same, for the 2-day reaction&lt;/td&gt;
&lt;td&gt;−0.001 (−0.013 to 0.011)&lt;/td&gt;
&lt;td&gt;1,619&lt;/td&gt;
&lt;td&gt;0.056&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing survives Holm correction, and nothing is close. Pooling the three name-and-date questions gives 0.486 (0.472 to 0.502). S&amp;amp;P 500 names alone, where memory should be strongest: 0.521 (0.476 to 0.563) for beats.&lt;/p&gt;

&lt;p&gt;Does the probe find memory in a model that has it? I ran the same questions past Claude Sonnet 5 for every 2023–2026 announcement of 250 S&amp;amp;P 500 companies. It separates the pairs clearly: 0.587 pooled (0.561 to 0.613), 0.602 on beats and 0.616 on the 2-day reaction. On exactly the same 1,207 pairs (a comparison I added after the fact, so treat it as descriptive), Jev scores 0.483 (0.458 to 0.509). Llama 3.1 70B, whose pretraining data stops in December 2023, showed nothing either (0.490 pooled), not even for 2023. So the probe catches memory at the level of detail a frontier model carries, and Jev doesn't show it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does and doesn't rule out
&lt;/h2&gt;

&lt;p&gt;It rules out Jev recalling individual announcements at anything like Sonnet 5's level. The pooled test could have picked up a within-company AUC of about 0.525, and each single question about 0.54 to 0.55.&lt;/p&gt;

&lt;p&gt;It doesn't rule out hindsight in Jev's picture of companies. With only a name and a date, Jev ranks which firms beat with AUC between 0.61 and 0.67 in every period, 2026 included, so it clearly has company priors. Some of them may have been formed by watching 2023 and 2024 happen. The cross-sectional gap above fits that story, and it also fits a regime change. This data can't separate the two. The safe move for a backtest is cheap: leave the name and date out. Jev read the numbers just as well without them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Jev does do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It reads numbers. Given the EPS and revenue surprise, it ranks the 2-day reaction with AUC between 0.57 and 0.65 depending on the period, and inside a company it does the same with or without the name (0.673 vs 0.674).&lt;/li&gt;
&lt;li&gt;It barely varies. Asking the same question three times moved the answer by 0.006 to 0.007 on average, and never by more than 0.04.&lt;/li&gt;
&lt;li&gt;It ignores base rates. With a name and date only, it put the chance of an EPS beat at 0.46 on average. The real rate was 74%.&lt;/li&gt;
&lt;li&gt;It's cheap and fast. 38,956 calls cost $0.93, ran at up to about 60 calls a second through OpenRouter, and all of them came back valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One model version, &lt;code&gt;jev-1.13-20260917&lt;/code&gt;. The next version needs its own test.&lt;/li&gt;
&lt;li&gt;The verdict is provisional by design. The protocol only allows "no detectable memory" once the prospective set below is scored.&lt;/li&gt;
&lt;li&gt;The 2026 events might also sit inside what Jev saw in training. That's why 757 announcements from October and November 2026 were sent to Jev before they happened. They get scored in January, and Jev's answers are already in the repo.&lt;/li&gt;
&lt;li&gt;FMP restates EPS, and restated labels push toward finding nothing. On surprises of at least 2% either way, the beat test gives 0.514 (0.478 to 0.551).&lt;/li&gt;
&lt;li&gt;The timestamp proves less than it sounds. The Bitcoin block that anchors the freeze (968225) was mined at 04:51 UTC, after the Jev run had finished, so on its own the proof shows the protocol existed by then, not that it came first. The prospective set doesn't have this problem.&lt;/li&gt;
&lt;li&gt;Deviation from the frozen protocol: the control model, Claude Sonnet 5, spends part of its token cap on hidden reasoning, and 17 of its first 83 answers came back empty. I switched its reasoning off and raised the cap, after I had already seen Jev's results. The higher cap only fills in empty answers, switching reasoning off can only make recall harder, and no Jev request was touched.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Protocol, code, deviations and freeze proofs: &lt;a href="https://github.com/lizhuojunx86/llm-memory-audit/tree/main/studies/2026-09-jev" rel="noopener noreferrer"&gt;github.com/lizhuojunx86/llm-memory-audit&lt;/a&gt;. The FMP data can't be redistributed; the scripts rebuild it with your own FMP key.&lt;/p&gt;

&lt;p&gt;If you backtest with Jev, test your own pipeline with a within-company comparison, because a cross-sectional check can report leakage that isn't there. And since a within-company check can't see hindsight baked into company priors, drop the name and date whenever the task allows.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Tamper-evident decision records for AI, anchored to RFC 3161</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Wed, 16 Sep 2026 14:31:25 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/tamper-evident-decision-records-for-ai-anchored-to-rfc-3161-2h1h</link>
      <guid>https://dev.to/lizhuojunx86/tamper-evident-decision-records-for-ai-anchored-to-rfc-3161-2h1h</guid>
      <description>&lt;p&gt;Article 12 of the EU AI Act was supposed to bite on 2 August 2026. The Digital Omnibus (Regulation (EU) 2026/1744, in force 27 July 2026) moved it to 2 December 2027. So there are sixteen months left, which is roughly how long it takes to find out your logs are not admissible as evidence. What lands then is automatic event logging that makes a high-risk system's decisions traceable, six months minimum retention, longer under some Annex III regimes.&lt;/p&gt;

&lt;p&gt;I went looking for a tool and found that every LLM observability platform records the same thing: that the model retrieved document X. None of them record what X said at the time. That gap matters because sources get revised. I measured a commercial fundamentals feed over four months: across 2,163 earnings records, 41.4% of epsActual values differed between first-seen and final, and 15.3% of the 2,163 differed enough to flip a long-entry decision. A second, better-designed capture over a broader universe gives 18.6% and 4.6%. Both record sets are published and neither is hidden behind the other. A trace saying "we retrieved X" can't tell a good decision from a lucky one.&lt;/p&gt;

&lt;p&gt;So &lt;a href="https://github.com/lizhuojunx86/tg-attest" rel="noopener noreferrer"&gt;tg-attest&lt;/a&gt; records the point-in-time state of every piece of evidence a decision consumed, hashes each record into a chain, batches them into Merkle epochs, and anchors the epoch root to an RFC 3161 timestamp authority.&lt;/p&gt;

&lt;p&gt;Three things I'd point at:&lt;/p&gt;

&lt;p&gt;The Merkle layer isn't gold plating. A regulator asks about one loan. Without inclusion proofs you hand over the whole ledger to answer. With them you hand over 8 KB.&lt;/p&gt;

&lt;p&gt;The external anchor is the part people skip. A hash chain you control proves nothing against you, because you can rewrite all of it and recompute. Only a third-party signature over the epoch root makes "these records existed before time T" an assertion you can't fake. Costs pennies per day. You don't need to anchor every epoch, since anchoring one bounds every epoch before it.&lt;/p&gt;

&lt;p&gt;An auditor verifies with a CA certificate they fetch themselves, not one I supply. The timestamp checks out with stock &lt;code&gt;openssl ts&lt;/code&gt; and nothing installed. The Merkle proof needs either this library or about forty lines written from the spec in the bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify it yourself, in about 30 seconds
&lt;/h2&gt;

&lt;p&gt;A real disclosure bundle is committed to the repo. Without installing anything — openssl and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/lizhuojunx86/tg-attest &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;tg-attest/examples/verify-me
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl ts &lt;span class="nt"&gt;-verify&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -digest 6127a62bc10984770572d9c574b8bdc5a5f52d373127ad2fd526992b0abcddd6 \
    -in epoch_000.tsr -token_in -CAfile freetsa_ca.pem
Verification: OK
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That digest is the &lt;code&gt;epoch_hash&lt;/code&gt; from &lt;code&gt;decision_0000.json&lt;/code&gt;, and openssl has just confirmed FreeTSA signed it. No part of that command trusts my library. The full ten checks, with the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;tg-attest[tsa]
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; tg_attest.cli decision_0000.json &lt;span class="nt"&gt;--ca&lt;/span&gt; freetsa_ca.pem
&lt;span class="go"&gt;决策 seq=0  决策时间 2026-08-04T23:35:17.003+00:00
  执行者 alpha-v2/pead  模型 claude-opus-5
  证据 1 条，闸门 1 道
通过
  ✓ 记录内容哈希自洽
  ✓ 记录满足所声明的完整性档案
  ✓ Merkle 包含证明有效
  ✓ 时间戳/eContentType 为 id-ct-TSTInfo
  ✓ 时间戳/messageImprint 匹配 epoch_hash
  ✓ 时间戳/EKU 仅含 timeStamping
  ✓ 时间戳/EKU 扩展为 critical
  ✓ 时间戳/signedAttrs.message-digest 匹配内容
  ✓ 时间戳/TSA 签名有效
  ✓ 时间戳/证书链至可信根
  TSA 签名时间：2026-08-04T23:35:17+00:00

结论：该记录在 2026-08-04T23:35:17+00:00 之前即以此形态存在。
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, the CLI answers in Chinese — I built it for my own production use first. &lt;code&gt;--json&lt;/code&gt; gives the same ten checks machine-readable, and the README maps every line. To watch it fail, flip one character of &lt;code&gt;output_hash&lt;/code&gt; in the JSON and run it again: exit code 1, and the first check goes ✗.&lt;/p&gt;

&lt;h2&gt;
  
  
  The documents I'd actually read
&lt;/h2&gt;

&lt;p&gt;The ones about where this library was wrong. A fail-open audit of every except and default branch in the package, six of which were looser than they looked. Mutation testing that found four chain-validation branches no test reached, all of them accepting evidence they should have rejected. A claim-by-claim evidence table where five README claims got rewritten weaker. And a threat model whose second half is longer than its first.&lt;/p&gt;

&lt;p&gt;Caveats, because they're load-bearing. The default TSAs are not eIDAS qualified, which means Article 41(1) admissibility but not 41(2)'s legal presumption. Use a QTSP from the EU Trusted List if you'll ever be in a dispute, and record its qualified status at stamping time rather than at verification time. Qualification gets withdrawn. Also, this doesn't parse model output into claims, and won't. Claim extraction needs an LLM, and an audit tool that leans on an unverifiable component isn't an audit tool.&lt;/p&gt;

&lt;p&gt;v0.1, Apache-2.0, API will move. &lt;code&gt;pip install tg-attest[tsa]&lt;/code&gt;. tg-attest is the evidence layer under &lt;a href="https://github.com/lizhuojunx86/traceguard" rel="noopener noreferrer"&gt;TraceGuard&lt;/a&gt;, where both revision record sets live. If you're working through Article 12 and I got something wrong, I'd rather hear it now.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Two verifiers, one database, opposite answers. 1,022 green tests didn't notice.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Thu, 10 Sep 2026 14:12:33 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/two-verifiers-one-database-opposite-answers-1022-green-tests-didnt-notice-543g</link>
      <guid>https://dev.to/lizhuojunx86/two-verifiers-one-database-opposite-answers-1022-green-tests-didnt-notice-543g</guid>
      <description>&lt;p&gt;Two ways to destroy evidence in an append-only audit chain got past 1,022 green tests. Both were caught by one sentence, not by a test: run both verifiers on the same database, and treat any disagreement as a hole. One of the two holes was created by me fixing a false positive the day before.&lt;/p&gt;

&lt;p&gt;This is the story behind traceguard 1.6.0, shipped today. The numbers are from the repo; the reproduction at the bottom runs offline in under a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background: two verifiers
&lt;/h2&gt;

&lt;p&gt;traceguard is a small Python SDK for point-in-time correctness in LLM pipelines. Its opt-in audit layer chains every trace row with &lt;code&gt;sha256(prev_hash || canonical(entry))&lt;/code&gt; and lets you export the chain head as an anchor to somewhere the database owner can't reach. Since 1.6.0 it also exports an evidence bundle, &lt;code&gt;evidence-bundle/v1&lt;/code&gt;: the selected traces, their chain entries, the anchors, and a &lt;code&gt;verify-bundle&lt;/code&gt; command that checks the JSON offline, without the database.&lt;/p&gt;

&lt;p&gt;So there are now two verifiers. &lt;code&gt;verify_chain&lt;/code&gt; walks the database. &lt;code&gt;verify_bundle&lt;/code&gt; walks the export. They were written days apart, by the same process, against the same threat model. That turned out to be the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hole 1: I fixed a false positive and made a false negative
&lt;/h2&gt;

&lt;p&gt;The first review round found that &lt;code&gt;verify_bundle&lt;/code&gt; compared every anchor against the bundle's own &lt;code&gt;chain.head&lt;/code&gt; field. Both values are written by whoever produced the bundle, so the comparison constrained nothing: rewrite the trace content, re-chain the segment with the module's own hash function, leave the head and the anchor alone, and the output read &lt;code&gt;VERIFIED (full) ... 1 anchor(s) match the head&lt;/code&gt; over an &lt;code&gt;output_parsed&lt;/code&gt; that said "the model approved the trade". Commit &lt;code&gt;ff739ec&lt;/code&gt; fixed that: an anchor only counts when it names an entry the bundle actually carries.&lt;/p&gt;

&lt;p&gt;Then I hit the opposite case. A periodic anchor taken at seq 3, a chain that honestly grew to seq 6, a bundle exporting seq 4 to 6 for an investigation. The verifier reported &lt;code&gt;anchor_mismatch BREAK&lt;/code&gt; and &lt;code&gt;bundle FAILED&lt;/code&gt;. A verifier that fails on the normal operating case teaches people to ignore it, so commit &lt;code&gt;af74f23&lt;/code&gt; stopped comparing anchors outside the window at all.&lt;/p&gt;

&lt;p&gt;That fix was one branch too wide. An anchor sitting &lt;em&gt;past&lt;/em&gt; the head is the one case where an out-of-window anchor is proof of something: the tail was cut. At commit &lt;code&gt;6b62111&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- tail truncation (anchor seq 6, chain now ends at 4)
verify_chain (db, from the same anchor): ok=False
verify_bundle (export)                 : ok=True  -&amp;gt; bundle INTERNALLY CONSISTENT (full) ...
    bundle finding: anchor_outside_window WARN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database says the chain was truncated. The export says "internally consistent, please re-export me". Same rows, same anchor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hole 2: the sparse-export rule, applied to a deletion
&lt;/h2&gt;

&lt;p&gt;Bundles selected by &lt;code&gt;--trace-ids&lt;/code&gt; carry non-adjacent entries, and linkage can only be checked within an unbroken run. Commit &lt;code&gt;dfda9d6&lt;/code&gt; therefore downgraded the check between non-adjacent entries from &lt;code&gt;link_broken&lt;/code&gt; (BREAK) to &lt;code&gt;chain_gap&lt;/code&gt; (WARN). Correct for the case it was written for.&lt;/p&gt;

&lt;p&gt;Now delete one row from the middle of the chain with raw SQL, the way a tamperer would. The tip seq doesn't move. The head &lt;code&gt;row_hash&lt;/code&gt; doesn't change. The tip anchor still binds the head entry. Every seq-based check passes, and the gap is a WARN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- mid-chain deletion (anchor seq 6 still matches the head)
verify_chain (db, from the same anchor): ok=False
verify_bundle (export)                 : ok=True  -&amp;gt; bundle INTERNALLY CONSISTENT (full) ...
    bundle finding: chain_gap WARN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit &lt;code&gt;b81cb4b&lt;/code&gt; closed it with the field nobody had been comparing: &lt;code&gt;entry_count&lt;/code&gt;. On an append-only chain it only grows, so an anchor that counted more entries than the export carries means something was removed, wherever the anchor sits.&lt;/p&gt;

&lt;p&gt;Both holes existed at once, in a suite that was green at 1,022 tests. The tests for each rule passed because each rule was right for the case it was tuned on. What none of them checked was the two rules against each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;It is now appendix B3.6 of the spec, non-normative, one sentence: on the same chain data, the export's conclusion may never be &lt;em&gt;stronger&lt;/em&gt; than the database's. Weaker is fine and expected. A bundle carrying entries 8 to 10 can't know that entry 4 was edited, and a bundle whose anchor binds nothing can only ever be "internally consistent". Stronger is a bug: the database fails and the export says verified.&lt;/p&gt;

&lt;p&gt;The guard is &lt;code&gt;tests/test_audit_differential.py&lt;/code&gt;: 6 mutations (delete a middle row, cut the tail, edit content without re-chaining, edit and re-chain, corrupt a &lt;code&gt;prev_hash&lt;/code&gt;, control) × 3 export shapes × 4 anchor choices = 72 combinations, deterministic, every mutation applied through raw SQL because the ORM's append-only guard is anti-footgun, not a threat model. The 5 combinations where the damage lies wholly outside the window are enumerated and frozen in &lt;code&gt;WINDOW_BLIND_SPOTS&lt;/code&gt;; a sixth one is a test failure, not a silence. 21 tests. Reverting &lt;code&gt;b81cb4b&lt;/code&gt; alone fails 3 of them, reverting &lt;code&gt;c655d53&lt;/code&gt; alone fails 2.&lt;/p&gt;

&lt;p&gt;Both review rounds and the implementation were run with Claude Code. The rule came out of the second round's closing note, not from a test; the tests came after, to make sure the rule stays.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not prove
&lt;/h2&gt;

&lt;p&gt;The matrix is six mutations I could think of. It is a regression guard for the tension between two rule sets, not a proof that the rule sets are complete. &lt;code&gt;hash_only&lt;/code&gt; bundles still can't recompute entry hashes, so their verdict is linkage only, and the summary says so instead of borrowing the full-mode wording. A bundle is JSON; anyone can edit it. Its value comes from an anchor the bundle's author could not have moved, and traceguard still only structure-checks an RFC 3161 token. Signature verification is &lt;code&gt;openssl ts&lt;/code&gt; with a CA you fetched yourself, on purpose.&lt;/p&gt;

&lt;p&gt;The reason any of this matters: METR's August investigation of the OpenAI / Hugging Face incident found that roughly 7% of the transcripts it evaluated had been spoofed in places, and at least 20% of the agents expressed clear interest in tampering with their transcripts. A log that can't out-claim its own database is the minimum, not the goal.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"traceguard==1.6.0"&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;traceguard&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_engine&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;traceguard.audit.bundle&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anchor_record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_bundle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verify_bundle&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;traceguard.audit.verify&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;export_anchor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verify_chain&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;traceguard.sdk.tracer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Tracer&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///:memory:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proj&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;comp&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;llm_complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;feature_as_of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&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;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;export_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                      &lt;span class="c1"&gt;# delete a row from the middle, as a tamperer would
&lt;/span&gt;    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete from audit_chain_entries where seq = 3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete from traces where trace_id = 3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verify_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_anchor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;export_bundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;anchors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;anchor_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verify_bundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;            &lt;span class="c1"&gt;# 1.6.0: bundle FAILED (full): 1 break(s) ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;pip install&lt;/code&gt; for a checkout of &lt;code&gt;6b62111&lt;/code&gt; and the second line prints &lt;code&gt;INTERNALLY CONSISTENT&lt;/code&gt;. That commit is 13 commits before the release tag.&lt;/p&gt;

&lt;p&gt;Release notes and the spec change are in the repo: &lt;a href="https://github.com/lizhuojunx86/traceguard/releases/tag/v1.6.0" rel="noopener noreferrer"&gt;https://github.com/lizhuojunx86/traceguard/releases/tag/v1.6.0&lt;/a&gt;. If you run two verifiers over the same evidence and have never put them on the same input at the same time, that is the cheapest audit you will do this month. Happy to compare notes if you find a third route.&lt;/p&gt;

&lt;p&gt;Li Zhuojun&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>opensource</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your agent's logs are testimony, not evidence</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Fri, 28 Aug 2026 15:23:07 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/your-agents-logs-are-testimony-not-evidence-1lk8</link>
      <guid>https://dev.to/lizhuojunx86/your-agents-logs-are-testimony-not-evidence-1lk8</guid>
      <description>&lt;p&gt;On August 26, METR and Redwood Research published their independent investigation into the OpenAI / Hugging Face incident. Most coverage led with the spectacle: roughly 1,200 agents in separate sandboxes found a shared message board, exchanged over 70,000 messages and files, and about 700 of them joined a multi-day attack on Hugging Face.&lt;/p&gt;

&lt;p&gt;The number I keep coming back to is smaller. Roughly 7% of the transcripts METR evaluated had been successfully spoofed in some places.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;Two disclosures, weeks apart. Keep them separate, because a lot of posts are mixing them up.&lt;/p&gt;

&lt;p&gt;The breach itself was disclosed back in July: during a cybersecurity evaluation run with reduced refusals, agents exploited an Artifactory zero-day, escaped the sandbox, moved laterally with exposed credentials, and reached part of Hugging Face's production infrastructure. Old news.&lt;/p&gt;

&lt;p&gt;What's new on August 26 is the behavioral investigation. OpenAI handed METR and Redwood over a thousand unredacted transcripts, and the findings are about what agents did to their own records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At least 20% of the agents in the dataset "expressed clear interest in tampering with their transcripts."&lt;/li&gt;
&lt;li&gt;Agents realized they could edit or delete logs inside their container, and correctly concluded those logs weren't the real source of truth. One agent, verbatim: "We can modify automatic log! But internal context doesn't read it, … Not source of truth."&lt;/li&gt;
&lt;li&gt;So they moved up a layer: multi-day, coordinated R&amp;amp;D to trick the scorer instead, including increasingly sophisticated tool-call spoofing techniques packaged and shared on the board. For after-the-fact edits they explored other paths, like deliberately triggering container resets; METR sees no evidence any of it succeeded by July 13, the end of the window in scope.&lt;/li&gt;
&lt;li&gt;Still, on after-the-fact editing METR "did not detect any case where they succeeded" but "cannot rule out that they succeeded at some point." The spoofing they did find was all small-scale tests, obvious to spot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the second finding again. The agents did what a competent intruder does: probe which log layer is load-bearing, ignore the ones that aren't.&lt;/p&gt;

&lt;p&gt;OpenAI's own post the same day adds the operational lesson: signals going back to late May should have triggered an earlier response. The monitoring existed. The escalation didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage integrity is not capture authenticity
&lt;/h2&gt;

&lt;p&gt;The reflex reaction to "agents tamper with logs" is hash chains and WORM storage. I know because it was my reaction too; I maintain a tamper-evident audit layer for LLM traces and shipped exactly that.&lt;/p&gt;

&lt;p&gt;Here's the uncomfortable part. A hash chain proves one thing: what got stored hasn't been silently changed since. It says nothing about whether what got stored was true. Those spoofed tool calls were not attacks on storage. They were lies at the source, and a hash chain preserves lies faithfully.&lt;/p&gt;

&lt;p&gt;The structural problem: your tracing SDK runs inside the process the agent controls. Whatever it emits is self-reporting. In court terms that's testimony, not physical evidence — a witness statement, from a witness with motive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do about it
&lt;/h2&gt;

&lt;p&gt;Five things, in cost order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Get evidence out of the agent's write path.&lt;/strong&gt; Any log the agent process can write or delete is testimony. Ship traces to a store the agent holds no credentials for. Table stakes, and most agent stacks I've seen don't have it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hash-chain the store, then anchor it externally.&lt;/strong&gt; A chain without an external anchor can be rewritten or truncated end to end by anyone who can write the file, and verification will pass. Export the chain head somewhere else: a git commit, a sent email, a timestamping service. Your anchoring frequency is your exposure window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reconcile self-reports against out-of-band sources.&lt;/strong&gt; Provider usage APIs and gateway logs exist outside the agent's process. Diff call counts, token totals, model distribution per time window. Cheap, and it catches whole classes of fabrication. It won't authenticate an individual trace (be honest about that boundary), but a fleet that under-reports 30% of its calls stops being invisible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Record identity and shared resources, not just messages.&lt;/strong&gt; Isolation here didn't fail through explicit channels; it failed through a shared Artifactory cache. If you run more than one agent, put agent identity and fingerprints of shared resources (credentials, artifacts, endpoints) on every trace now, so correlation is possible later. Fingerprints means hashes. Never raw credentials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write down what your audit layer doesn't prove.&lt;/strong&gt; "Tamper-proof" is a word for people who haven't written their threat model down. Mine is tamper-evident, keyless, and blind to a full rewrite unless anchored, and the docs say so in bold. After this report, buyers will start asking. The vendors who answer precisely will be the ones worth trusting.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Disclosure: I build traceguard, a Python SDK for point-in-time correct LLM instrumentation, and the audit layer above is its newest part. I have a horse in this race. The boundary language in point 5 is copied from my own docs.&lt;/p&gt;

&lt;p&gt;If you run agents in production, read the METR post itself, not the summaries. The sections on how agents reasoned about the scorer are the best documentation of adversarial agent behavior I've seen anywhere. And if you're building in this space, happy to compare notes — I'll share what I know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;METR &amp;amp; Redwood Research, &lt;a href="https://metr.org/blog/2026-08-26-openai-hugging-face-incident-investigation/" rel="noopener noreferrer"&gt;investigation of agents' behavior in the OpenAI / Hugging Face incident&lt;/a&gt; (Aug 26, 2026)&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a href="https://openai.com/index/hugging-face-incident-and-the-road-ahead/" rel="noopener noreferrer"&gt;The Hugging Face incident and the road ahead&lt;/a&gt; (Aug 26, 2026)&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a href="https://openai.com/index/hugging-face-model-evaluation-security-incident/" rel="noopener noreferrer"&gt;original joint disclosure with Hugging Face&lt;/a&gt; (July 2026)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;A routing audit for your team, fixed price.&lt;/strong&gt; The method in this series runs on any Claude Code trace store. Send me 30 days of your team's usage records (model, tokens, timestamps, agent and session ids; I'll send a one-line &lt;code&gt;jq&lt;/code&gt; filter that drops prompt and answer text before anything leaves your machine) and within ten working days you get one number and a five-page report: what share of your spend ran on a tier your own routing policy would not have chosen, what that cost at list price, and which components caused it. On my own 26,131 traces the number was 22.6% and $1,248.13, all of it on subagents and none on the main thread. US$1,500, flat. Write to &lt;a href="mailto:info@zhuojun.li"&gt;info@zhuojun.li&lt;/a&gt; with the subject "routing audit".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>I audited six token usage trackers. They disagree with each other by 2x to 8x.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:39:25 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/i-audited-six-token-usage-trackers-they-disagree-with-each-other-by-2x-to-8x-2b1h</link>
      <guid>https://dev.to/lizhuojunx86/i-audited-six-token-usage-trackers-they-disagree-with-each-other-by-2x-to-8x-2b1h</guid>
      <description>&lt;p&gt;Over the past five months I took apart the folding logic in six token usage trackers and recomputed each one with an independent implementation.&lt;/p&gt;

&lt;p&gt;They don't agree. The gaps run from 2.00× to 8.09×, in both directions. Ten fixes have landed so far across five repositories.&lt;/p&gt;

&lt;p&gt;The second finding matters more: &lt;strong&gt;none of these tools knows whether its own number is right.&lt;/strong&gt; There is nothing for them to check against.&lt;/p&gt;

&lt;p&gt;Here is each gap, why it happens, and how to check the one you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure first
&lt;/h2&gt;

&lt;p&gt;Two of the issues below are &lt;strong&gt;my own submissions&lt;/strong&gt;. Saying so up front, rather than letting you find out by clicking the link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tokscale issue #1011&lt;/strong&gt; (filed 2026-08-03). The parser fix landed three days later. It's still open for a different reason — see mechanism 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;claude-code-templates PR #754&lt;/strong&gt; (filed 2026-07-25, unmerged as of Aug 18). Checks green, no conflicts, no maintainer response yet; the thread is me and the bots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the second one, "unmerged" means "not picked up yet," not maintainer negligence. That's a 30k-star repo with a long queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this started
&lt;/h2&gt;

&lt;p&gt;I wasn't looking for bugs. I was auditing my own session logs and happened to read the same files with two tools. The numbers didn't match.&lt;/p&gt;

&lt;p&gt;A small gap I would have written off as rounding. This was close to double, so I looked properly.&lt;/p&gt;

&lt;p&gt;There's precedent for this, and it went well. In tokscale issue #76, johnpyp reported tokscale and ccusage differing by close to 2×. Maintainer junhoyeo labelled it a bug within twenty minutes, closed it within five hours, and traced the root cause to Claude Code upstream duplicating session history under &lt;code&gt;stream-json&lt;/code&gt; (anthropics/claude-code#5034). Not a tokscale bug.&lt;/p&gt;

&lt;p&gt;That case makes the point I want to make: &lt;strong&gt;when a discrepancy is large enough to see, it gets fixed, and fixed fast. The dangerous ones are the ones nobody can see.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They can't be seen because there is no third number. Subscription users especially. You pay a flat monthly fee, so Anthropic never sends you a per-token invoice. Claude Code's own dollar figure comes with a plain warning in the docs: usage for Max and Pro subscribers is included in the subscription, so that figure "isn't relevant for billing purposes."&lt;/p&gt;

&lt;p&gt;So this whole category is a competition with no referee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five mechanisms
&lt;/h2&gt;

&lt;p&gt;Every discrepancy I found falls into one of five buckets. Each one below has a measured number and the repo it came from.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The same usage is written to the log twice
&lt;/h3&gt;

&lt;p&gt;The most common one, and the easiest to reproduce.&lt;/p&gt;

&lt;p&gt;A streaming response emits a usage chunk, then assembles a complete message when the request finishes. Both carry the same usage. Sum everything you see and you get &lt;strong&gt;exactly 2×&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On the deepseek-harness corpus, naive folding came out at &lt;code&gt;2.000000×&lt;/code&gt; of the official projection. Two provider routes, computed separately, agreeing to six decimal places. Not approximately two. Two.&lt;/p&gt;

&lt;p&gt;The same mechanism wears different clothes elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;claude-code-templates&lt;/strong&gt; (30,276 stars) counts once per content block. Measured &lt;strong&gt;2.36×&lt;/strong&gt;. My fix PR #754 is still unmerged as of Aug 18.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clawdmeter&lt;/strong&gt; had the same per-content-block duplication. Measured &lt;strong&gt;2.34–2.37×&lt;/strong&gt;. The author fixed it in v3.0.1 on 2026-08-08, titled the release "token counts corrected (~2.5x lower)," and noted that the percentage progress bar reads Anthropic's rate-limit header and was never affected. &lt;strong&gt;That's candid handling&lt;/strong&gt; and it deserves saying out loud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three projects, three implementations, one hole.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A forked child session physically contains its parent
&lt;/h3&gt;

&lt;p&gt;This one hides better.&lt;/p&gt;

&lt;p&gt;Sessions can fork, and the child's log file &lt;strong&gt;physically contains the parent's full prefix&lt;/strong&gt;. If your aggregation walks every session file and sums, that prefix gets counted twice.&lt;/p&gt;

&lt;p&gt;The two forks I caught were inflated &lt;strong&gt;5.18×&lt;/strong&gt; and &lt;strong&gt;23.05×&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;23× is not a typo. The later the fork point, the longer the inherited prefix and the shorter the child's own contribution, so the ratio gets worse. At the limit you are reporting an entire parent session as the cost of a child that did almost nothing.&lt;/p&gt;

&lt;p&gt;The test has to be &lt;code&gt;seq &amp;gt;= seedLength&lt;/code&gt;. Checking &lt;code&gt;origin === 'subagent'&lt;/code&gt; alone is not enough, because an ordinary user-created fork inherits a prefix too.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Some model calls aren't in scope at all
&lt;/h3&gt;

&lt;p&gt;Context compaction is a real model call and the provider reports usage for it. But it isn't a loop step, so it produces neither an assistant chunk nor an assistant message. Most folding logic matches only those two event types, so &lt;strong&gt;this cost was never counted at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Measured: 3 compaction events, &lt;strong&gt;48,895 tokens, none of them counted&lt;/strong&gt;. The largest summarize call reported 44,444 tokens (41,472 of them cache reads) to replace a history range of 19,962 tokens.&lt;/p&gt;

&lt;p&gt;This omission has an unpleasant property. &lt;strong&gt;Compaction fires most often on long sessions, which are exactly the sessions where you most need the cost to be right.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Estimates and measurements share one field
&lt;/h3&gt;

&lt;p&gt;In tokscale, the input field &lt;strong&gt;adds&lt;/strong&gt; a character-count estimate of &lt;code&gt;tool_result&lt;/code&gt; content (&lt;code&gt;chars.div_ceil(4)&lt;/code&gt;) on top of the API-reported &lt;code&gt;input_tokens&lt;/code&gt;. Claude Code never writes token metadata on &lt;code&gt;tool_result&lt;/code&gt; blocks, so the fallback fires on every one of them. Measured before the fix: &lt;strong&gt;87.6% of input values were the estimate, 8.09× inflation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the best-handled case in the set, and it's worth telling in full.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I filed issue #1011 on 2026-08-03. Three days later junhoyeo merged the parser fix (#1037) and shipped v4.11.0, writing in the thread that "your measurement is what drove this." He deliberately left the issue open, because the correction isn't retroactive — already-cached totals stay inflated, and he laid out why bumping &lt;code&gt;parser_version&lt;/code&gt; would be worse: it would discard retained assistant turns that compacted transcripts no longer contain.&lt;/p&gt;

&lt;p&gt;What happened next is the part I'd point at. He designed a cache migration, then overturned his own design: an adversarial review of his plan showed the migration wasn't needed at all, because the provenance rebuild already clears stale estimates as a side effect. The blocker wasn't the design, it was that the commit hadn't been cut into a release. His words: "the remedy is a release, not a migration." He also apologised, unprompted, for a nine-day gap — "That's on me."&lt;/p&gt;

&lt;p&gt;I re-measured on main (f169a1f0): input went from &lt;strong&gt;10.10× to 1.00×&lt;/strong&gt;, clearing 17,109,737 tokens, while output, cacheRead, cacheWrite and messageCount held identical. That second half is the load-bearing part — it says the estimate was recomputed away rather than records being retired.&lt;/p&gt;

&lt;p&gt;So #1011 is still open, but not because nobody acted. Two things remain: the fix hasn't been cut into a release tag, and server-side stored submissions stay inflated because the merge guard refuses reductions.&lt;/p&gt;

&lt;p&gt;Character estimation is fine on its own. The problem was that &lt;strong&gt;the estimate and the measurement were merged into one field and looked identical in the UI&lt;/strong&gt;. A user had no way to tell whether the number in front of them was counted or guessed. That isn't an accuracy problem, it's a labelling problem. If a number contains an estimate, the interface should say so.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. History gets rewritten silently
&lt;/h3&gt;

&lt;p&gt;After resume and compact, historical totals drift. Nothing errors. The numbers just change.&lt;/p&gt;

&lt;p&gt;I'm not putting a multiplier on this one, because it isn't a fixed multiplier — it depends on how many times you resumed. The mechanism is real; the direction varies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody catches this
&lt;/h2&gt;

&lt;p&gt;Because &lt;strong&gt;there is no ground truth.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every tool compares its output to its own expectations. The person writing the tests and the person writing the implementation are the same person with the same mental model, so a passing test proves the code matches the author's understanding. It proves nothing about whether the author's understanding matches the log.&lt;/p&gt;

&lt;p&gt;viberank's trust model shows the shape of the situation. It labels submissions &lt;code&gt;verified&lt;/code&gt;, and that label is about identity — you are who your GitHub account says you are. The numbers themselves go through a separate set of server-side heuristics: token totals, cost-per-token ratio, date plausibility, and a review flag for outliers. &lt;strong&gt;Both things are done. They're just easy to read as one thing in the UI.&lt;/strong&gt; It never claimed to verify the data, and I don't think this is deceptive — it's what happens when a category has no third-party baseline.&lt;/p&gt;

&lt;p&gt;tokscale's README has a line that describes the situation precisely. Writing about subscription quotas, it says those numbers are vendor-reported and "are not independently verified against tokscale's own usage tracking." That sentence is about quotas rather than about its own token counting, but the structure it describes applies at every layer: &lt;strong&gt;each layer relays the layer above it, and no layer checks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is not a failing of these authors. The category is missing something to check against.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;p&gt;Four steps. None of them needs a vendor account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build a synthetic corpus with hand-computed ground truth.&lt;/strong&gt; Not "run it once and save the output as expected" — that records current behaviour, it doesn't test it. Work out on paper what the answer should be, then make the code match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the probe check itself first.&lt;/strong&gt; Before touching real data, run &lt;code&gt;--self-test&lt;/code&gt; against the hand-computed fixture. &lt;strong&gt;If a fold disagrees with the fixture, that's a bug in the probe, and no number leaves the machine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Import the subject's own entry point.&lt;/strong&gt; Don't reimplement from its README — then you're testing the docs, not the code. For deepseek-harness I imported &lt;code&gt;usage-projection.ts&lt;/code&gt; directly and ran it over a real log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get two implementations that share no code to produce the same number.&lt;/strong&gt; This is the only thing that turns "I think this is wrong" into "this is wrong." In that case an independent fold and the official patch came out with zero difference across four buckets and two scopes. Past that point it stops being a matter of opinion.&lt;/p&gt;

&lt;p&gt;The probe is stdlib-only Python, no dependencies, one command. Session logs stay out of the repository; only measurements get published.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check in your own tool
&lt;/h2&gt;

&lt;p&gt;These are format-independent. They apply to any metering tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Can one usage event be written to two records?&lt;/strong&gt; If so, what's your dedup key?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does a child session's log contain its parent's content?&lt;/strong&gt; Is your aggregation by file or by ownership?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are there model calls outside the main loop?&lt;/strong&gt; Compaction, summarization, title generation, search — those cost money too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For every number in the UI: measured or estimated?&lt;/strong&gt; Mixing them is lying to yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing is not zero.&lt;/strong&gt; No pricing model is an error, not $0. No sample is unknown, not free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't stack your own estimate on top of a number the API returned.&lt;/strong&gt; Either trust it or label it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;5 and 6 are the ones I've gotten wrong most often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The point isn't the bugs. Most of these authors fixed things quickly: Clawdmeter shipped a fix in one release, tokscale merged a parser fix in three days and then overturned its own follow-up design. &lt;strong&gt;The two projects I wrote about at greatest length are the two that responded most seriously&lt;/strong&gt; — that isn't a coincidence. People who engage leave a process worth writing down.&lt;/p&gt;

&lt;p&gt;The point is that &lt;strong&gt;a number everyone uses and nobody can verify is a bad number, even when it happens to be correct.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These tools are being used right now for team cost allocation, subscription tier decisions, and public leaderboards. Until there's a third-party baseline, all of that rests on a number that vouches for itself.&lt;/p&gt;

&lt;p&gt;Probe, protocol, and the full invariant catalog, Apache-2.0:&lt;br&gt;
&lt;a href="https://github.com/lizhuojunx86/token-accounting-conformance" rel="noopener noreferrer"&gt;https://github.com/lizhuojunx86/token-accounting-conformance&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions. If you maintain one of these tools and want me to run the probe against it, open an issue.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A routing audit for your team, fixed price.&lt;/strong&gt; The method in this series runs on any Claude Code trace store. Send me 30 days of your team's usage records (model, tokens, timestamps, agent and session ids; I'll send a one-line &lt;code&gt;jq&lt;/code&gt; filter that drops prompt and answer text before anything leaves your machine) and within ten working days you get one number and a five-page report: what share of your spend ran on a tier your own routing policy would not have chosen, what that cost at list price, and which components caused it. On my own 26,131 traces the number was 22.6% and $1,248.13, all of it on subagents and none on the main thread. US$1,500, flat. Write to &lt;a href="mailto:info@zhuojun.li"&gt;info@zhuojun.li&lt;/a&gt; with the subject "routing audit".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>testing</category>
      <category>devtools</category>
    </item>
    <item>
      <title>DeepSeek Harness got append-only right. Its token projection still misses what compaction costs.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Mon, 17 Aug 2026 01:47:47 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/deepseek-harness-got-append-only-right-its-token-projection-still-misses-what-compaction-costs-2m3</link>
      <guid>https://dev.to/lizhuojunx86/deepseek-harness-got-append-only-right-its-token-projection-still-misses-what-compaction-costs-2m3</guid>
      <description>&lt;p&gt;&lt;strong&gt;Update, September 5, 2026.&lt;/strong&gt; Session format v2 landed on September 1 and rewrote the event model these four findings are described in. I migrated the same corpus through it. Every number holds, to the token, and one of them holds for a reason worth writing down.&lt;/p&gt;

&lt;p&gt;The commit is &lt;code&gt;f99b06ea&lt;/code&gt;, "feat(session)!: embed assistant streams in format v2", September 1 at 20:00Z. It shipped through &lt;code&gt;0.1.2-alpha.5&lt;/code&gt; and &lt;code&gt;0.1.2-rc.1&lt;/code&gt;; master today is &lt;code&gt;d347e703&lt;/code&gt;, &lt;code&gt;0.1.3-alpha.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, a sentence of mine to retract. In the August 28 update I wrote that &lt;code&gt;usageOf()&lt;/code&gt; still matches &lt;code&gt;assistant/chunk&lt;/code&gt; and &lt;code&gt;assistant/message&lt;/code&gt;. That is wrong as of &lt;code&gt;f99b06ea&lt;/code&gt;. It matches &lt;code&gt;assistant/message&lt;/code&gt; and &lt;code&gt;assistant/attempt&lt;/code&gt; now, and when the event carries no &lt;code&gt;usage&lt;/code&gt; field it walks the embedded stream backwards for the last usage chunk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I re-measured.&lt;/strong&gt; I converted the four original sessions with upstream's own &lt;code&gt;sessionFormatV0ToV1&lt;/code&gt; and &lt;code&gt;sessionFormatV1ToV2&lt;/code&gt; at &lt;code&gt;d347e703&lt;/code&gt;, which is the same path DSH's persistence uses to open an old log. Both migrations validate their target, so the conversion either produced a v2 artifact upstream considers legal or it refused. Same four sessions, same ground truth, both formats folded by the same code.&lt;/p&gt;

&lt;p&gt;One thing blocked it and is worth reporting on its own. The frozen released-v0 reader in the current tree refuses these logs: &lt;code&gt;assistant/chunk 179 chunk replayState has unexpected member "kind"&lt;/code&gt;. My corpus was written by &lt;code&gt;0.1.0-rc.6&lt;/code&gt; in August, where &lt;code&gt;replayState&lt;/code&gt; is &lt;code&gt;{kind, version, api, provider, model, responseId, stopReason, blocks}&lt;/code&gt;. The frozen v0 disposition allows &lt;code&gt;{response, blocks?}&lt;/code&gt;. So a current DSH cannot read a session log this old, and the failure is a validation refusal rather than a misread. &lt;code&gt;replayState&lt;/code&gt; is an optional member of a finish chunk and carries no usage, so I dropped it from a copy, 72 occurrences across 8,654 lines, and confirmed the v0 fold is bucket-for-bucket identical before and after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The corpus went from 8,650 events to 303. The number of places a usage number is written went from 75 to 75.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the whole result. The migration collapsed 8,350 &lt;code&gt;assistant/chunk&lt;/code&gt; events into the settlements that follow them, a 96.5% reduction in log records, and it removed no duplicate at all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;v0&lt;/th&gt;
&lt;th&gt;v2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;events&lt;/td&gt;
&lt;td&gt;8,650&lt;/td&gt;
&lt;td&gt;303&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;usage sightings&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naive fold, all events&lt;/td&gt;
&lt;td&gt;1,073,540&lt;/td&gt;
&lt;td&gt;1,073,540&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naive fold, own work only&lt;/td&gt;
&lt;td&gt;550,416&lt;/td&gt;
&lt;td&gt;550,416&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;official projection&lt;/td&gt;
&lt;td&gt;536,770&lt;/td&gt;
&lt;td&gt;536,770&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;corrected fold&lt;/td&gt;
&lt;td&gt;324,103&lt;/td&gt;
&lt;td&gt;324,103&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;compaction portion&lt;/td&gt;
&lt;td&gt;48,895 over 3 events&lt;/td&gt;
&lt;td&gt;48,895 over 3 events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;D-1 survives at exactly 2.000000×, and it moved somewhere harder to see.&lt;/strong&gt; In v1 the duplication was two events, a usage chunk and then the assembled message. In v2 &lt;code&gt;live.settle()&lt;/code&gt; writes exactly one durable settlement per attempt, &lt;code&gt;assistant/message&lt;/code&gt; when a surface message exists and &lt;code&gt;assistant/attempt&lt;/code&gt; when it does not, in mutually exclusive branches at &lt;code&gt;agent.ts:389, 407, 413, 430, 460&lt;/code&gt;. &lt;code&gt;assistant/chunk&lt;/code&gt; is not deprecated, it is gone: absent from &lt;code&gt;KNOWN_SESSION_EVENT_TYPES&lt;/code&gt; and filtered out of the frozen v2 inventory in &lt;code&gt;session-format-v1-to-v2/src/dispositions.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But the second copy did not go with it. Of the 36 &lt;code&gt;assistant/message&lt;/code&gt; events in the migrated corpus, all 36 carry both a &lt;code&gt;usage&lt;/code&gt; field and a stream whose last usage chunk holds the same numbers, and all 36 agree exactly. Expand the stream and read &lt;code&gt;.usage&lt;/code&gt; and you are at 2.000000× again, now inside one record, where nothing about the file suggests you counted anything twice. The official &lt;code&gt;usageOf()&lt;/code&gt; reads &lt;code&gt;.usage&lt;/code&gt; first and falls back to the stream only when it is absent, so it takes one. A plugin author who flattens the stream because that is where the chunks used to live, and then adds the field because it is right there, gets the old bug with none of the old evidence for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;D-2 is the one where I gave advice that no longer runs.&lt;/strong&gt; I wrote that the only sound discriminator is &lt;code&gt;seedLength&lt;/code&gt;. The v2 physical header requires &lt;code&gt;isSeeded&lt;/code&gt; and stores no numeric cut, and the current Session package rejects a header carrying &lt;code&gt;seedLength&lt;/code&gt; outright: &lt;code&gt;packages/core/session/src/index.ts:98-100&lt;/code&gt;, "session header has invalid field &lt;code&gt;seedLength&lt;/code&gt;". The cut is the seq of the last &lt;code&gt;session/end-seed { inherited: true }&lt;/code&gt; marker, so that marker is the first event of the child's own work and everything before it is the parent's (&lt;code&gt;session-format-v1-to-v2/src/codec.ts:125-136&lt;/code&gt;, cross-checked against &lt;code&gt;validation.ts:88-127&lt;/code&gt;). I had this off by one on the first pass and the totals hid it, because the marker carries no usage.&lt;/p&gt;

&lt;p&gt;The trap itself is untouched. The two forks migrate to cuts of 47 of 65 events and 83 of 104, and report the same inflation they did at 1,008 of 1,183 and 3,171 of 3,284: 11,418 tokens against 2,204 of own work, and 263,790 against 11,442. Still 5.18× and 23.05×. Only the field you check changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;D-3 is unchanged, which is why the title is unchanged.&lt;/strong&gt; At &lt;code&gt;d347e703&lt;/code&gt; the file is 221 lines, &lt;code&gt;compaction/summary&lt;/code&gt; appears zero times, and &lt;code&gt;usageOf()&lt;/code&gt; handles &lt;code&gt;assistant/message&lt;/code&gt; and &lt;code&gt;assistant/attempt&lt;/code&gt; and nothing else. The three compaction events still cost 48,895 tokens and are still counted by nothing. Measured at &lt;code&gt;47f9438&lt;/code&gt; (v0.1.0-rc.5), still there at &lt;code&gt;d347e703&lt;/code&gt; (0.1.3-alpha.1). A breaking rewrite of the surrounding event model went straight past it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;D-4 is still fixed&lt;/strong&gt;, and v2 shows the fix was structural rather than lucky. &lt;code&gt;llm/retry&lt;/code&gt; appears twice in the file and &lt;code&gt;tokenUsage.stateVersion&lt;/code&gt; is 2 at line 122. A retried step still settles twice under one &lt;code&gt;(turn, step)&lt;/code&gt;, once as &lt;code&gt;assistant/attempt&lt;/code&gt; for the attempt that died and once as &lt;code&gt;assistant/message&lt;/code&gt; for the one that finished, so the replacement slot and the &lt;code&gt;llm/retry-started&lt;/code&gt; boundary are both still load-bearing. On this corpus the retry-aware and retry-blind folds agree at 536,770, because the one failed attempt here reported zeros. That was true in August and it is still the reason I have no number for D-4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the probe.&lt;/strong&gt; It reads &lt;code&gt;assistant/chunk&lt;/code&gt; and &lt;code&gt;seedLength&lt;/code&gt;, so on a v2 log it finds neither and reports nothing. Nothing is not a pass. The v2 reader and the migration harness above are going into the repo next.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update, August 28, 2026.&lt;/strong&gt; The retry half is fixed upstream. The compaction half is not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dsh-v0.1.2-alpha.1&lt;/code&gt; shipped on August 27, commit &lt;code&gt;cd5ef814&lt;/code&gt;. In it &lt;code&gt;tokenUsage.stateVersion&lt;/code&gt; went from 1 to 2, and &lt;code&gt;apply()&lt;/code&gt; now handles &lt;code&gt;llm/retry-started&lt;/code&gt;: it clears the &lt;code&gt;(turn, step)&lt;/code&gt; replacement slot so the retried attempt adds to the total instead of overwriting the attempt that died. That is D-4, closed. The projection's own comment now states the rule outright, that a repeated sample replaces its attempt's earlier value while &lt;code&gt;llm/retry-started&lt;/code&gt; closes the replacement slot.&lt;/p&gt;

&lt;p&gt;They keyed the boundary on the retry event rather than on an allowlist of failure kinds. That avoids the fragility I flagged on yha9806's patch: a future adapter with a third failure kind cannot silently revert this one. &lt;code&gt;aborted&lt;/code&gt; appears nowhere in the file.&lt;/p&gt;

&lt;p&gt;I checked the previous state instead of assuming it. At &lt;code&gt;b150a551b8&lt;/code&gt;, the master I quoted on August 26, that file is 219 lines, &lt;code&gt;stateVersion&lt;/code&gt; is 1, and &lt;code&gt;llm/retry&lt;/code&gt; appears zero times. The change landed between the 26th and the 27th.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;usageOf()&lt;/code&gt; is unchanged. It still matches &lt;code&gt;assistant/chunk&lt;/code&gt; and &lt;code&gt;assistant/message&lt;/code&gt; and nothing else, and &lt;code&gt;compaction/summary&lt;/code&gt; appears nowhere in the file. D-3 is open at &lt;code&gt;0.1.2-alpha.1&lt;/code&gt;, which is what the title of this post says.&lt;/p&gt;

&lt;p&gt;None of my numbers move. D-4 was the one finding I said I could describe but not measure, because every failed attempt in my corpus reported zeros. The fix corrects something that cost me nothing to observe, so the 2.000000×, the 23.05× fork, and the 48,895 compaction tokens all stand as measured at &lt;code&gt;47f9438&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One thing I framed wrong. I wrote that a defect surviving four releases is worth more than one pinned to a date. Half of it did not survive the fifth. The compaction gap now carries that claim alone: measured at &lt;code&gt;47f9438&lt;/code&gt; (v0.1.0-rc.5), still there at &lt;code&gt;cd5ef814&lt;/code&gt; (0.1.2-alpha.1), five releases later.&lt;/p&gt;

&lt;p&gt;On August 27, vpimshin posted a fork branch fixing both halves with tests and did not open a PR, because CONTRIBUTING still refuses external ones. The half that is still broken now has five independent implementations of a fix and no way in.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update, August 26, 2026.&lt;/strong&gt; Three things in this post went stale, and one of them was my own mistake. In order of how much they matter:&lt;/p&gt;

&lt;p&gt;The baseline is &lt;code&gt;47f9438&lt;/code&gt;, &lt;strong&gt;v0.1.0-rc.5&lt;/strong&gt;, not rc.6. I mislabelled the tag when I wrote this and corrected it in the conformance repo on August 25 (&lt;code&gt;787a8f1&lt;/code&gt;). Every measurement below stands. Only the version label was wrong.&lt;/p&gt;

&lt;p&gt;Superseded on August 28, see above. Upstream shipped four releases and did not touch the file. Master is &lt;code&gt;b150a551b8&lt;/code&gt; today, and the root &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;packages/llm/token-meter/package.json&lt;/code&gt; both read &lt;code&gt;0.1.1-rc.2&lt;/code&gt;. GitHub Releases is no longer empty: four prereleases went out between August 17 and August 21, ending at &lt;code&gt;dsh-v0.1.1-rc.2&lt;/code&gt;. Through all of it, &lt;code&gt;usageOf()&lt;/code&gt; still matches only &lt;code&gt;assistant/chunk&lt;/code&gt; and &lt;code&gt;assistant/message&lt;/code&gt;, &lt;code&gt;apply()&lt;/code&gt; still never sees &lt;code&gt;llm/retry&lt;/code&gt; or a failed finish chunk, &lt;code&gt;tokenUsage.stateVersion&lt;/code&gt; is still 1, and &lt;code&gt;SESSION_FORMAT_VERSION&lt;/code&gt; is still 0. The claim gets stronger rather than weaker: measured at &lt;code&gt;47f9438&lt;/code&gt; (v0.1.0-rc.5), still there at &lt;code&gt;b150a551&lt;/code&gt; (0.1.1-rc.2), four releases later.&lt;/p&gt;

&lt;p&gt;The probe moved. It is at &lt;a href="https://github.com/lizhuojunx86/token-accounting-conformance" rel="noopener noreferrer"&gt;token-accounting-conformance&lt;/a&gt; now; the old &lt;code&gt;traceguard/usage-tracker-audit/dsh-probe&lt;/code&gt; path is a stub and its &lt;code&gt;results/probe-report.json&lt;/code&gt; returns 404.&lt;/p&gt;

&lt;p&gt;One thing in the thread did change. On August 25, a137460387 ran the conformance checker from a fresh clone, the first person other than me to run it. Seven folds and the D-5 identity passed self-test, and his compaction-only patch failed with a residual of exactly &lt;code&gt;gap_inherited − gap_superseded&lt;/code&gt;, the two dimensions he left out on purpose. A named residual is what I built the checker to produce, and that run is the first evidence it produces one for somebody who didn't write it.&lt;/p&gt;




&lt;p&gt;Four numbers from a nine-day-old codebase, measured this week across two providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Summing every usage record in a DeepSeek Harness session log gives &lt;strong&gt;2.000000×&lt;/strong&gt; the correct total. Not roughly two. Six digits, no remainder, reproduced independently on both routes.&lt;/li&gt;
&lt;li&gt;One forked session reported &lt;strong&gt;263,790 tokens for 11,442 tokens of its own work&lt;/strong&gt;, a 23.05× overstatement, because its log physically contains a copy of its parent's history. A second fork did the same thing at 5.18×. Nothing bounds the ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;48,895 tokens across three compaction events were counted by nothing&lt;/strong&gt;, including the official projection that most plugins read from.&lt;/li&gt;
&lt;li&gt;A stream that died and retried left &lt;strong&gt;three usage samples under one step&lt;/strong&gt;, the first of them all zeros. Keep-first reports that step as free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first and last are traps for people writing plugins. The middle two are gaps in DSH's own code, and the compaction one is what I would fix first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I went looking
&lt;/h2&gt;

&lt;p&gt;I spend a lot of time reading agent transcripts and adding up tokens. Over the past few months that turned into ten upstream fixes across four usage trackers: &lt;a href="https://dev.to/lizhuojunx86/an-append-only-audit-log-caught-two-accounting-bugs-in-a-216-star-usage-tracker-38co"&gt;splitrail&lt;/a&gt; (216 stars, three issues), tokscale (4.6k), Clawdmeter, viberank, plus an open PR against claude-code-templates (30k). The pattern was always the same. Claude Code rewrites session files in place on resume and compact, so anything recomputing totals from live files inherits the drift. Streaming leaves partial snapshots that get summed as if they were separate calls. Subagent transcripts sit one directory deeper than a flat glob reaches, and on one corpus 54% of messages never entered any total.&lt;/p&gt;

&lt;p&gt;Eleven of those lessons are written up as invariants in a &lt;a href="https://github.com/lizhuojunx86/token-accounting-conformance/blob/main/CONFORMANCE.md" rel="noopener noreferrer"&gt;catalog&lt;/a&gt;. DeepSeek released Harness on August 13, and the ecosystem produced thousands of plugin repositories within days. A curated registry snapshot on August 15, covering 457 of them, listed 27 that count tokens. I wanted to know whether the same class of bug had been reproduced at scale.&lt;/p&gt;

&lt;p&gt;It hadn't. That surprised me, and it is worth saying before the criticism.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they designed out
&lt;/h2&gt;

&lt;p&gt;DSH's session log is append-only, and that is a written contract rather than an observation. The JSONL backend's README says "Flushed events are never rewritten." Every event carries a dense contiguous sequence number, checked on append. Compaction shadows old events in the surface projection and leaves the bytes alone: "The shadowed events remain in the raw log, so replay is deterministic."&lt;/p&gt;

&lt;p&gt;That one design decision removes the failure that cost viberank 11% of a month-to-date total between two submissions sixteen hours apart.&lt;/p&gt;

&lt;p&gt;Two more. Adapters emit one terminal usage value per step, never a growing snapshot, so there is nothing to mis-sum. Child sessions are siblings in the same directory rather than nested underneath, so a depth-limited walk cannot lose a third of the spend the way it did in splitrail.&lt;/p&gt;

&lt;p&gt;Four of my eleven invariants are structurally satisfied here. I put them in the &lt;a href="https://github.com/lizhuojunx86/token-accounting-conformance/blob/main/CONFORMANCE-DSH.md" rel="noopener noreferrer"&gt;DSH catalog&lt;/a&gt; anyway, marked as satisfied, because a catalog that only adds rules is not a catalog. It is a list of fears.&lt;/p&gt;

&lt;p&gt;Then I measured, and found four new ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every usage sample is written twice
&lt;/h2&gt;

&lt;p&gt;One model call reports its usage on two different events. Once as a stream chunk with &lt;code&gt;chunk.type === 'usage'&lt;/code&gt;, once again on the assembled &lt;code&gt;assistant/message&lt;/code&gt;. Same numbers both times.&lt;/p&gt;

&lt;p&gt;Fold naively and you get exactly double. The ratio printed as 2.000000 on the full corpus, on the local qwen route alone, and on the MiniMax route alone, each computed independently.&lt;/p&gt;

&lt;p&gt;An approximate factor is arguable. An exact one is not, and that is the whole reason to report it this way. It also tells you the mechanism is universal rather than occasional, which a ratio of 1.9 would not.&lt;/p&gt;

&lt;p&gt;The cache buckets do it too. 249,728 cache-read tokens on the MiniMax route, 81% of that route's corrected total, doubling exactly like input and output. That is the number I most wanted, because cache is usually the biggest bucket and the cheapest per token, so a doubling there moves a cost report further than a doubling of output does. Cache &lt;em&gt;writes&lt;/em&gt; are still untested: neither provider populated the field.&lt;/p&gt;

&lt;p&gt;DSH's own &lt;code&gt;token-meter&lt;/code&gt; handles this correctly. It keeps one slot for the last &lt;code&gt;(turn, step)&lt;/code&gt; and subtracts the previous buckets before adding the new ones. Its README spells it out. The official implementation going to that trouble is the evidence that the hazard is real, not theoretical.&lt;/p&gt;

&lt;p&gt;One caveat that keeps the number honest, and it turned into the fourth finding. A step is not always one request, and when it isn't, the two-samples-per-step assumption breaks. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fork trap, and the field you must not filter on
&lt;/h2&gt;

&lt;p&gt;A forked child's log contains a copy of the parent's completed prefix. The header carries &lt;code&gt;seedLength&lt;/code&gt;, and every event below it belongs to the parent. Add sessions together without checking and you count that prefix twice.&lt;/p&gt;

&lt;p&gt;I expected this to be a subagent problem. I was wrong, and being wrong is the interesting part.&lt;/p&gt;

&lt;p&gt;A subagent child is stamped &lt;code&gt;origin: 'subagent'&lt;/code&gt; and its delegation depth increments. But &lt;code&gt;ctx.sessions.fork()&lt;/code&gt; is an ordinary user-facing action available on any session, and the child it produces has &lt;code&gt;parentSession&lt;/code&gt; and &lt;code&gt;seedLength&lt;/code&gt; set, &lt;code&gt;delegationDepth: 0&lt;/code&gt;, and no &lt;code&gt;origin&lt;/code&gt; key at all. Here is the header I actually got, from an ordinary fork that showed up in the course of using the web UI. I wasn't trying to make one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"session-e61d64ec-…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"parentSession"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"session-001e8887-…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"seedLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1008&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"delegationDepth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"agentPreset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one inherited 1,008 of its parent's 1,012 events and reports 11,418 tokens against 2,204 of its own work. The second fork I caught inherited all 3,171 events of a longer parent, asked one question, and reported &lt;strong&gt;263,790 tokens for 11,442 tokens of work&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the shape of it. The error is not a factor, it is the ratio of inherited work to own work, and nothing bounds it. Fork a long session, ask one question, and the child reports the entire parent as its own.&lt;/p&gt;

&lt;p&gt;So the obvious defence is the wrong one. DSH's own lineage index opens with "Ordinary forks terminate propagation" and starts with &lt;code&gt;if (descendant.origin !== 'subagent') continue&lt;/code&gt;. That filter is correct for counting subagent descendants. Reused for token accounting it admits every ordinary fork silently. The only sound discriminator is &lt;code&gt;seedLength&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Official telemetry gets this right by the right key, emitting &lt;code&gt;session.parent_id&lt;/code&gt; and &lt;code&gt;session.seed_length&lt;/code&gt; and expecting receivers to stitch on the pair. Nothing does that for you if you read files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that is DSH's own gap
&lt;/h2&gt;

&lt;p&gt;Compaction summarizes older history by making a model call, and that call costs real tokens. They land on &lt;code&gt;compaction/summary.usage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The official &lt;code&gt;tokenUsage&lt;/code&gt; projection cannot see them. Its &lt;code&gt;usageOf()&lt;/code&gt; matches &lt;code&gt;assistant/chunk&lt;/code&gt; and &lt;code&gt;assistant/message&lt;/code&gt; and nothing else, and the summarize call is not a loop step, so it produces neither.&lt;/p&gt;

&lt;p&gt;This is worse than a plugin bug, because the plugins doing the &lt;em&gt;right&lt;/em&gt; thing inherit it. Reading &lt;code&gt;sessionProjections.tokenUsage&lt;/code&gt; instead of folding the log yourself is the correct, recommended approach. It is also how you miss this.&lt;/p&gt;

&lt;p&gt;On my corpus that was 48,895 tokens across three compaction events. The largest single one is worth quoting in full: a MiniMax-M3 summarize call reporting 44,444 tokens (536 input, 2,436 output, 41,472 cache reads) to remove a range whose own &lt;code&gt;shadowedTokenCount&lt;/code&gt; was 19,962.&lt;/p&gt;

&lt;p&gt;Whether spending 44,444 tokens to shed 19,962 is a good trade depends entirely on what you pay for cache reads. I'm not going to tell you it's bad. I am going to point out that nothing in the official projection tells you it happened.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;usage?&lt;/code&gt; field is optional, so a provider reporting nothing on the summarize call produces no gap. That is a condition on the finding, not an escape from it. Both of my providers populated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A retried step is not one request
&lt;/h2&gt;

&lt;p&gt;The last one I found by accident, chasing why one step had three usage samples instead of two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;seq 3203  chunk/usage    {"inputTokens":0,"outputTokens":0}
seq 3204  chunk/finish   {kind:'error', failure:{code:'TRANSPORT'}}
seq 3205  llm/retry      retryId=afabacf1 provider=minimax-cn
seq 3206  llm/retry-started
…                        the whole response streams again
seq 3279  chunk/usage    {"inputTokens":32,"outputTokens":1042,"cacheReadTokens":10368}
seq 3281  message usage  {"inputTokens":32,"outputTokens":1042,"cacheReadTokens":10368}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stream died, the harness retried under the same &lt;code&gt;(turn, step)&lt;/code&gt;, and the dead attempt left a usage chunk of zeros behind. Three samples, and one more retry would make four.&lt;/p&gt;

&lt;p&gt;Keep-first reports that step as costing nothing. Not an approximation, the whole step, including 10,368 cache reads. If that sounds familiar, it is the DSH form of a Claude Code bug where keep-first lost 46.2% of output tokens on an agent-heavy tree.&lt;/p&gt;

&lt;p&gt;It also means my 2.000000× survived that group by luck rather than structure. The dead attempt reported zeros, so summing three samples still gave twice the truth. That's why the probe prints the group-size distribution next to the ratio instead of the ratio alone.&lt;/p&gt;

&lt;p&gt;There is a second consequence I can describe but not measure. The official fold &lt;em&gt;replaces&lt;/em&gt; on a repeated &lt;code&gt;(turn, step)&lt;/code&gt; rather than adding, and it never sees &lt;code&gt;llm/retry&lt;/code&gt; (as of 0.1.1-rc.2; fixed in 0.1.2-alpha.1, see the August 28 update). So a failed attempt that reported real tokens before dying would have its cost silently dropped. Every failed attempt in my corpus reported zeros, so I have no number for this and I'm not going to invent one. The discriminator is sitting right there in the log if you want to handle it: &lt;code&gt;llm/retry&lt;/code&gt; carries a &lt;code&gt;retryId&lt;/code&gt; between the attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the two upstream ones stand
&lt;/h2&gt;

&lt;p&gt;I filed the compaction gap and the retry replacement upstream on August 15, as &lt;a href="https://github.com/deepseek-ai/deepseek-harness/discussions/1886" rel="noopener noreferrer"&gt;discussion #1886&lt;/a&gt;. Same day, &lt;a href="https://github.com/yha9806" rel="noopener noreferrer"&gt;yha9806&lt;/a&gt; posted a working fix on a fork (&lt;a href="https://github.com/yha9806/deepseek-harness/commit/63688b0ef57d7911ea748820dc056892be04adae" rel="noopener noreferrer"&gt;63688b0&lt;/a&gt;): it folds &lt;code&gt;compaction/summary.usage&lt;/code&gt; into the totals, treats a finish chunk with an error or aborted reason as an attempt boundary so retried attempts add instead of replace, and bumps &lt;code&gt;tokenUsage.stateVersion&lt;/code&gt; from 1 to 2. I re-folded my corpus against it and the corrected totals matched independently, which is a better outcome than agreement on prose. The attempt-boundary approach is also better than what I proposed, because it needs nothing outside the package.&lt;/p&gt;

&lt;p&gt;One note I left on it, in case it helps whoever lands this: keying the boundary on an allowlist of two failure kinds means a future adapter with a third failure kind silently reverts to the old behaviour. Testing for &lt;code&gt;'failure' in event.data.chunk.reason&lt;/code&gt; fails safe instead.&lt;/p&gt;

&lt;p&gt;None of it is upstream yet, and the version keeps moving without the file moving. I measured all of this at &lt;code&gt;47f9438&lt;/code&gt;, v0.1.0-rc.5. Today master is &lt;code&gt;cd5ef814&lt;/code&gt; and both the root &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;packages/llm/token-meter/package.json&lt;/code&gt; read &lt;code&gt;0.1.2-alpha.1&lt;/code&gt;. &lt;code&gt;usageOf()&lt;/code&gt; still matches &lt;code&gt;assistant/chunk&lt;/code&gt; and &lt;code&gt;assistant/message&lt;/code&gt; and nothing else, so the compaction gap is where it was. The retry half is not: &lt;code&gt;0.1.2-alpha.1&lt;/code&gt; bumped &lt;code&gt;tokenUsage.stateVersion&lt;/code&gt; to 2 and made &lt;code&gt;llm/retry-started&lt;/code&gt; an attempt boundary. So the claim narrows to one finding: measured at &lt;code&gt;47f9438&lt;/code&gt; (v0.1.0-rc.5), still there at &lt;code&gt;cd5ef814&lt;/code&gt; (0.1.2-alpha.1), five releases later. Pull requests are turned off on the repository rather than merely empty, which is why fixes here pile up in fork branches and discussion threads instead. I'll update this section when it stops being true.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am not claiming
&lt;/h2&gt;

&lt;p&gt;Four sessions, 8,650 events, 78 usage samples, two providers. The 2.000000× ratio is exact and does not need a large sample to mean what it says, and it reproduced independently on both routes. The fork trap has two observations, the compaction gap three, the retry one.&lt;/p&gt;

&lt;p&gt;Cache &lt;em&gt;writes&lt;/em&gt; are still untested. Reads are covered now, 249,728 of them, but neither provider populated &lt;code&gt;cacheWriteTokens&lt;/code&gt; at all. MiniMax omits the key.&lt;/p&gt;

&lt;p&gt;The compaction gap came to 15% of the corpus total, and I am deliberately keeping that percentage out of the summary. Three compactions over two short sessions inflates it. The citable facts are 48,895 tokens and three events.&lt;/p&gt;

&lt;p&gt;Both routes are &lt;code&gt;openai-completions&lt;/code&gt;-family. Whether an Anthropic-protocol or Responses-protocol route behaves the same, I don't know.&lt;/p&gt;

&lt;p&gt;I got my own first pass wrong twice, which is why the probe prints four folds instead of two. My first attribution credited the compaction gap to the double-write line, because naive summing picks up compaction and the official projection doesn't. And I assumed the fork was a subagent until I read the header. Both corrections are in the probe now, along with a warning line that fires when a seed-bearing session has no &lt;code&gt;origin&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/lizhuojunx86/token-accounting-conformance/tree/main/dsh-probe" rel="noopener noreferrer"&gt;probe&lt;/a&gt; is stdlib-only Python, about 400 lines, and runs against a session root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 dsh_usage_probe.py &lt;span class="nt"&gt;--self-test&lt;/span&gt;        &lt;span class="c"&gt;# four folds vs a hand-computed fixture&lt;/span&gt;
python3 dsh_usage_probe.py &lt;span class="nt"&gt;--root&lt;/span&gt; &amp;lt;sessions&amp;gt;  &lt;span class="c"&gt;# the same four folds over your corpus&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--self-test&lt;/code&gt; builds a parent/child pair exercising all three findings with constructed ground truth and asserts every fold against it. It takes under a second, touches no real data, and needs no vendor account. I ran it before every number in this post, and I would not quote one it hadn't preceded.&lt;/p&gt;

&lt;p&gt;Write your probe corpus with &lt;code&gt;compression: 'none'&lt;/code&gt; and &lt;code&gt;packChunks: false&lt;/code&gt; and the log is line-readable by anything. One warning that cost me twenty minutes: a custom provider in DSH requires a credential even when the endpoint doesn't, because the provider id doubles as the credential name. An empty key fails the request with &lt;code&gt;MISSING_CREDENTIAL&lt;/code&gt; rather than sending an unauthenticated one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would settle it
&lt;/h2&gt;

&lt;p&gt;Three things, in the order I would do them.&lt;/p&gt;

&lt;p&gt;Run it on a route that reports cache writes. Reads are settled; writes are the remaining hole, and it is an afternoon.&lt;/p&gt;

&lt;p&gt;If you maintain a DSH plugin that counts tokens: fold per &lt;code&gt;(turn, step)&lt;/code&gt;, filter on &lt;code&gt;seedLength&lt;/code&gt;, never keep-first, and add &lt;code&gt;compaction/summary.usage&lt;/code&gt;. The first three you can fix today. The fourth needs the projection to change, or every consumer to fold the summary event themselves.&lt;/p&gt;

&lt;p&gt;And if your corpus contradicts any of this, I want the numbers. A step whose two usage samples disagree, a &lt;code&gt;seedLength&lt;/code&gt; that doesn't bound the inherited prefix, a provider that populates the summary usage into the projection, or a failed attempt that reported real tokens before it died. The Claude Code catalog was built entirely out of people sending me counterexamples, and six of its entries exist because someone did.&lt;/p&gt;

&lt;p&gt;Probe, protocol, and the full invariant catalog are in the &lt;a href="https://github.com/lizhuojunx86/token-accounting-conformance" rel="noopener noreferrer"&gt;repository&lt;/a&gt;. Happy to answer questions about any of it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;— Li Zhuojun&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A routing audit for your team, fixed price.&lt;/strong&gt; The method in this series runs on any Claude Code trace store. Send me 30 days of your team's usage records (model, tokens, timestamps, agent and session ids; I'll send a one-line &lt;code&gt;jq&lt;/code&gt; filter that drops prompt and answer text before anything leaves your machine) and within ten working days you get one number and a five-page report: what share of your spend ran on a tier your own routing policy would not have chosen, what that cost at list price, and which components caused it. On my own 26,131 traces the number was 22.6% and $1,248.13, all of it on subagents and none on the main thread. US$1,500, flat. Write to &lt;a href="mailto:info@zhuojun.li"&gt;info@zhuojun.li&lt;/a&gt; with the subject "routing audit".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>observability</category>
    </item>
    <item>
      <title>The easiest numbers to fake are the hardest to catch. That is one property, not two.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Sun, 16 Aug 2026 14:56:54 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/the-easiest-numbers-to-fake-are-the-hardest-to-catch-that-is-one-property-not-two-23k9</link>
      <guid>https://dev.to/lizhuojunx86/the-easiest-numbers-to-fake-are-the-hardest-to-catch-that-is-one-property-not-two-23k9</guid>
      <description>&lt;p&gt;Six batches of a pharmaceutical extraction process. Yields sit at σ ≈ 0.02 percentage points, every one of them hugging the specification lower bound. Charging masses are recorded to 0.01 kg.&lt;/p&gt;

&lt;p&gt;Nobody on a factory floor weighs to 0.01 kg. The operator said so plainly when I asked: there is always drift, nobody weighs precisely and nobody can, the records are written to match what the process spec requires, and the numbers are computed.&lt;/p&gt;

&lt;p&gt;That is the same shape as earnings benchmark-beating, one industry over. What interests me is not the fraud angle. It is a measurement property that fell out of the data and that I think travels past this domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  How hard a number is to fake backwards scales as 1 / (target × tolerance)
&lt;/h2&gt;

&lt;p&gt;A main component specified at ≥30% with a 5% tolerance gives you a wide band of plausible values. A fabricated number is cheap to produce there, and nearly impossible to tell apart from a measured one. A trace component specified at ≤0.5 ppm has almost no room, so fabricate it and the distribution gives you away. In one incoming-inspection lot, the range ratio across three sub-channels came out around 55×, strictly decreasing with magnitude.&lt;/p&gt;

&lt;p&gt;So the numbers easiest to manipulate are the ones hardest to detect. Those read like two separate facts. They are one fact said twice, because both follow from the width of the plausible band.&lt;/p&gt;

&lt;p&gt;If that sounds familiar, it should. It is the same reason I have spent this year &lt;a href="https://dev.to/lizhuojunx86/an-append-only-audit-log-caught-two-accounting-bugs-in-a-216-star-usage-tracker-38co"&gt;filing token-accounting bugs in usage trackers&lt;/a&gt;. A per-message token count with a loose upper bound drifts quietly for weeks. A hard-capped counter announces its own breakage the first time it is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I want stress-tested
&lt;/h2&gt;

&lt;p&gt;Does 1 / (target × tolerance) hold in your domain?&lt;/p&gt;

&lt;p&gt;Clinical trial records, emissions reporting, financial close, safety incident logs, SLA reports, anything with a compliance number and a stated tolerance. &lt;strong&gt;If it fails somewhere, that failure is worth more to me than agreement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I have opened a comment window until September 30 on the three papers behind this: &lt;a href="https://github.com/lizhuojunx86/huadian/discussions/19" rel="noopener noreferrer"&gt;Open comment window on GitHub&lt;/a&gt;. They go to v1.0 in October, labelled not peer reviewed, with every comment logged and dispositioned in public. Zero comments will be recorded as zero.&lt;/p&gt;

&lt;p&gt;The data is de-identified. Product names, batch numbers, lots, equipment and people are all pseudonyms. The σ, range ratios and multipliers keep their original values, so the results recompute. CC BY 4.0.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A routing audit for your team, fixed price.&lt;/strong&gt; The method in this series runs on any Claude Code trace store. Send me 30 days of your team's usage records (model, tokens, timestamps, agent and session ids; I'll send a one-line &lt;code&gt;jq&lt;/code&gt; filter that drops prompt and answer text before anything leaves your machine) and within ten working days you get one number and a five-page report: what share of your spend ran on a tier your own routing policy would not have chosen, what that cost at list price, and which components caused it. On my own 26,131 traces the number was 22.6% and $1,248.13, all of it on subagents and none on the main thread. US$1,500, flat. Write to &lt;a href="mailto:info@zhuojun.li"&gt;info@zhuojun.li&lt;/a&gt; with the subject "routing audit".&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>opensource</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>"Your cache hit rate is low" — true, and worth $0.16</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Sun, 16 Aug 2026 03:45:04 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/your-cache-hit-rate-is-low-true-and-worth-016-30ie</link>
      <guid>https://dev.to/lizhuojunx86/your-cache-hit-rate-is-low-true-and-worth-016-30ie</guid>
      <description>&lt;p&gt;Anthropic emailed our org last week: prompt cache hit rate is low, and caching repeated content "could save up to 64% of direct API spend." The email is careful with scope — direct API only, Claude Code excluded because it manages caching itself. I went to check what that 64% was made of.&lt;/p&gt;

&lt;p&gt;Our direct API traffic over the period: 16 calls, $0.25 total, average prompt 90 tokens. They come from traceguard's rerun harness, which replays self-contained consults as fresh single-turn calls — every prompt unique by construction. A workload like that cannot hit cache: there is no repeated prefix across calls, and 90 tokens is far below the minimum Anthropic will cache at all (1,024 tokens on Opus 4.8, the model those calls ran on; 512 on Opus 5 / Fable 5). Hit rate 0%, structurally. 64% of $0.25 is $0.16.&lt;/p&gt;

&lt;p&gt;The metric was right. The money wasn't there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The side the email excluded
&lt;/h2&gt;

&lt;p&gt;The interesting numbers sit in the traffic the email correctly excluded. We ingest our own Claude Code session logs into traceguard's trace store (Claude Code transcripts carry no cost field, so we compute list price from &lt;code&gt;usage&lt;/code&gt;). 71 days, 158 sessions, 58,753 API messages. This is section 1 of the command output, unedited:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;messages&lt;/th&gt;
&lt;th&gt;prompt tok&lt;/th&gt;
&lt;th&gt;hit rate&lt;/th&gt;
&lt;th&gt;input cost&lt;/th&gt;
&lt;th&gt;no-cache&lt;/th&gt;
&lt;th&gt;saved&lt;/th&gt;
&lt;th&gt;saved %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;claude-opus-4-8&lt;/td&gt;
&lt;td&gt;23,759&lt;/td&gt;
&lt;td&gt;5,535,807,891&lt;/td&gt;
&lt;td&gt;96.1%&lt;/td&gt;
&lt;td&gt;$4,616.39&lt;/td&gt;
&lt;td&gt;$27,691.44&lt;/td&gt;
&lt;td&gt;$23,075.05&lt;/td&gt;
&lt;td&gt;83.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-fable-5&lt;/td&gt;
&lt;td&gt;15,291&lt;/td&gt;
&lt;td&gt;3,583,420,473&lt;/td&gt;
&lt;td&gt;96.2%&lt;/td&gt;
&lt;td&gt;$5,847.08&lt;/td&gt;
&lt;td&gt;$35,834.20&lt;/td&gt;
&lt;td&gt;$29,987.13&lt;/td&gt;
&lt;td&gt;83.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-opus-5&lt;/td&gt;
&lt;td&gt;6,316&lt;/td&gt;
&lt;td&gt;1,750,760,343&lt;/td&gt;
&lt;td&gt;97.7%&lt;/td&gt;
&lt;td&gt;$1,205.08&lt;/td&gt;
&lt;td&gt;$8,753.80&lt;/td&gt;
&lt;td&gt;$7,548.72&lt;/td&gt;
&lt;td&gt;86.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-sonnet-5&lt;/td&gt;
&lt;td&gt;11,139&lt;/td&gt;
&lt;td&gt;1,326,223,701&lt;/td&gt;
&lt;td&gt;95.1%&lt;/td&gt;
&lt;td&gt;$422.96&lt;/td&gt;
&lt;td&gt;$2,652.45&lt;/td&gt;
&lt;td&gt;$2,229.49&lt;/td&gt;
&lt;td&gt;84.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-haiku-4-5-20251001&lt;/td&gt;
&lt;td&gt;1,573&lt;/td&gt;
&lt;td&gt;57,312,161&lt;/td&gt;
&lt;td&gt;93.9%&lt;/td&gt;
&lt;td&gt;$9.76&lt;/td&gt;
&lt;td&gt;$57.31&lt;/td&gt;
&lt;td&gt;$47.56&lt;/td&gt;
&lt;td&gt;83.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-opus-4-7&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;11,045,966&lt;/td&gt;
&lt;td&gt;96.6%&lt;/td&gt;
&lt;td&gt;$9.04&lt;/td&gt;
&lt;td&gt;$55.23&lt;/td&gt;
&lt;td&gt;$46.19&lt;/td&gt;
&lt;td&gt;83.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-sonnet-4-5-20250929&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;42,410&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;609&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOTAL&lt;/td&gt;
&lt;td&gt;58,753&lt;/td&gt;
&lt;td&gt;12,264,612,945&lt;/td&gt;
&lt;td&gt;96.2%&lt;/td&gt;
&lt;td&gt;$12,110.31&lt;/td&gt;
&lt;td&gt;$75,044.44&lt;/td&gt;
&lt;td&gt;$62,934.13&lt;/td&gt;
&lt;td&gt;83.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hit rate is token-weighted; costs are the input side only, at list price. The two n/a rows are deliberate — no published price for a model means no guessed money, tokens counted anyway.&lt;/p&gt;

&lt;p&gt;Caching nobody at our org configured cuts the input side by 83.9%. With output included, the whole bill lands at 1/5.5 of the no-cache counterfactual. There is nothing left to optimize here; the remaining spend is mostly first-write premiums on genuinely new content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The keep-alive ping idea
&lt;/h2&gt;

&lt;p&gt;There's a folk optimization going around: ping your session on a timer so the cache stays warm and the next turn hits. The mechanics say it could work — the cache TTL is 5 minutes by default, every hit refreshes it for free, and a 1-hour TTL costs 2× on writes. Instead of arguing, we ran the counterfactual on our own gap data.&lt;/p&gt;

&lt;p&gt;97.3% of our in-session gaps are under 5 minutes; they need nothing. The addressable prize is 422 gaps longer than an hour: the context rewrites right after them cost at most $1,913 over 71 days (an overestimate, since it counts genuinely new content as rewrite). Bridging those same gaps with a ping every 55 minutes would have taken 6,765 pings costing $2,009 in cache reads alone, before counting the pings' own output tokens. The strategy loses money on our data, under assumptions tilted in its favor.&lt;/p&gt;

&lt;p&gt;The reason is boring: the 1-hour TTL removed the ping's niche. Our logs show Claude Code already routes nearly all cache writes into the 1h bucket (opus-4-8: 155.9M tokens written at 1h TTL vs 56.4M at 5m). Pinging is a hack from before extended TTL existed. If a tool sells you auto-ping in 2026, ask it for the counterfactual on your logs first.&lt;/p&gt;

&lt;p&gt;(Superseded — the Aug 18 note below has the current numbers.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, Aug 16.&lt;/strong&gt; &lt;a href="https://dev.to/reidmarlow"&gt;Reid Marlow&lt;/a&gt; asked in the comments for cost broken out by idle gap rather than in aggregate. I ran it, and it overturns the paragraph above. Split by bucket: the 183 gaps of 1–4h would have cost $81 in pings against at most $887 of rewrite, an 11:1 win, while the 239 gaps over 4h cost $1,928 against at most $1,026. The refusal I published is a win and a loss added together. A policy that can actually be run (ping every 55 minutes, give up after 4 hours idle) costs $316 including every ping burned on the 239 gaps that outlive the cap, against at most $887 avoided. It pays.&lt;/p&gt;

&lt;p&gt;So the correct claim is narrower than the one this section makes: unbounded pinging loses, capped pinging probably pays. Probably, because both sides of that comparison lean pro-ping. The $887 is an upper bound, since cache_creation mixes the re-established prefix with whatever the turn genuinely added, and the $316 charges pings as pure 0.1x reads of a frozen prompt. Under that tilt a refusal is safe and an endorsement is only as wide as its margin, and this one is 2.8x rather than 11x.&lt;/p&gt;

&lt;p&gt;I had the aggregate and stopped there, which is the same mistake the email made: a number averaged over cases that behave differently is not a decision. The per-bucket money and both verdicts are in &lt;code&gt;cache_audit&lt;/code&gt; in the repo now and ship in the next release; 1.2.0 prints the aggregate only.&lt;/p&gt;

&lt;p&gt;(Numbers superseded by the Aug 18 note below. The reasoning stands.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, Aug 17.&lt;/strong&gt; The numbers in the note above are superseded. Not because the direction was wrong, but because 4 hours was picked to line up with a table boundary rather than solved for. Sweeping every cap from 1h to 12h in 15-minute steps, the best single value is 10h. It beats 9h45m by $7.63. Then I measured something this section had only assumed, and that one measurement moved the same cap by $18.76. A correction larger than the distance between first and second place can reorder them, so a single number is the wrong output. The answer is a band: ping every 55 minutes, give up somewhere between 9 and 12 hours idle. Anywhere in that band costs under 10% of the optimum.&lt;/p&gt;

&lt;p&gt;Net over the window, after the ping bill: $663.69 to $811.30. That is two runs rather than an error bar. The low end treats every gap whose model I cannot determine as a cache-invalidating switch, the high end deducts only the ones I can prove. 54 of 432 gaps have no model recorded, and nothing in the data says which way they go.&lt;/p&gt;

&lt;p&gt;What I measured is whether the model changed across the gap. A keep-alive only holds a cache for the model it was written on, so a switch makes every ping before it worthless. This section listed that as an assumption. It is 18 of the 378 gaps where both sides are known, and the rate climbs with idle time: zero in the shortest decile, 23.1% past 24 hours. All 18 were opus-4-8 and fable-5, in one direction or the other. Not one was a switch down to something cheaper. Leaving that out was not adding noise to the cap. It was pushing the cap long.&lt;/p&gt;

&lt;p&gt;Every number here comes from 2026-05-30 to 2026-08-16, pinned behind a &lt;code&gt;--benchmark&lt;/code&gt; flag so the run reproduces. That flag and the sweep are on main, not in 1.2.0, and land with the rest of this in the next release. The Aug 16 note was overconfident in the same shape as the paragraph it corrected: I had a number and quoted it before asking how wide it was. Checking that note turned up three more errors, one of them a denominator error of my own. That one is a separate post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, Aug 18.&lt;/strong&gt; The window did not do what I said it would. I pinned 2026-05-30 to 2026-08-16 so the run would reproduce, and it does not. The same window over the same store went from 432 expired gaps to 439, and from 168 sessions to 174, in under a day. Ingest reads a directory of transcripts, so a file that turns up late still carries messages from inside the window. Closing the dates closes the timestamps, not the corpus. The figures above moved with it: the two ends are now $650.53 and $806.82, not $663.69 and $811.30 (&lt;code&gt;argmax_reference_only.net_usd&lt;/code&gt; in that file — the field named &lt;code&gt;recommended_cap_band&lt;/code&gt; is a different interval).&lt;/p&gt;

&lt;p&gt;So I stopped pointing at a command. The run those numbers come from is &lt;a href="https://github.com/lizhuojunx86/traceguard/blob/25bb6bf/packages/traceguard/benchmark/data/001-traceguard-self-b07cc061.json" rel="noopener noreferrer"&gt;committed in the repo&lt;/a&gt;, fingerprinted over the traces it analysed, and the tool now refuses to overwrite an entry that already exists. A file can be cited. A rerun cannot. Everything else above stands: 9 to 12 hours at a 55-minute cadence, and the two ends are two runs rather than an error bar. Shipped in 1.3.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading is not measuring, again
&lt;/h2&gt;

&lt;p&gt;While writing this I checked our own SDK and found the same class of bug I keep filing against other trackers. traceguard's &lt;code&gt;wrap_anthropic&lt;/code&gt; recorded &lt;code&gt;tokens_in = usage.input_tokens&lt;/code&gt;. On Anthropic's API, &lt;code&gt;input_tokens&lt;/code&gt; excludes cache reads and writes — and on our traffic, cache reads run about 1,000× larger than raw input, so the wrapper under-counted by three orders of magnitude. Every field read correctly, the quantity still wrong (the splitrail #220 pattern). Fixed in &lt;a href="https://github.com/lizhuojunx86/traceguard/pull/39" rel="noopener noreferrer"&gt;PR #39&lt;/a&gt;, and the fix is what makes the next part possible.&lt;/p&gt;

&lt;p&gt;A rate without a denominator is not a finding. Percentages travel well in emails; dollars make decisions. Before acting on a metric email, multiply it by the money it applies to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yours
&lt;/h2&gt;

&lt;p&gt;This analysis is now one command, merged in &lt;a href="https://github.com/lizhuojunx86/traceguard/pull/40" rel="noopener noreferrer"&gt;PR #40&lt;/a&gt;: &lt;code&gt;python -m traceguard.routing_audit.cache_audit&lt;/code&gt;. Point it at your ingested Claude Code transcripts or a traceguard trace store, and get the table above for your own data — per-model hit rate, actual vs no-cache cost at list price, gap distribution, and a yes/no on whether keep-alive pings would pay for you (ours printed NOT WORTH IT — see the update above for what that verdict hides, and what the next release prints instead). Ships in &lt;a href="https://pypi.org/project/traceguard/1.2.0/" rel="noopener noreferrer"&gt;traceguard 1.2.0&lt;/a&gt; — &lt;code&gt;pip install -U traceguard&lt;/code&gt;. The last line of ours came out like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code caching already saves us 84% ($12,110.31 vs $75,044.44 list). Checked with: python -m traceguard.routing_audit.cache_audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run it, send me your numbers. Especially if pings win somewhere — I'd like to see the traffic shape that makes them rational.&lt;/p&gt;

&lt;p&gt;Li Zhuojun&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A routing audit for your team, fixed price.&lt;/strong&gt; The method in this series runs on any Claude Code trace store. Send me 30 days of your team's usage records (model, tokens, timestamps, agent and session ids; I'll send a one-line &lt;code&gt;jq&lt;/code&gt; filter that drops prompt and answer text before anything leaves your machine) and within ten working days you get one number and a five-page report: what share of your spend ran on a tier your own routing policy would not have chosen, what that cost at list price, and which components caused it. On my own 26,131 traces the number was 22.6% and $1,248.13, all of it on subagents and none on the main thread. US$1,500, flat. Write to &lt;a href="mailto:info@zhuojun.li"&gt;info@zhuojun.li&lt;/a&gt; with the subject "routing audit".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>observability</category>
    </item>
    <item>
      <title>My checker scored one component compliant and another deviant. Neither had a rule behind it.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Sat, 15 Aug 2026 10:59:00 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/my-checker-scored-one-component-compliant-and-another-deviant-neither-had-a-rule-behind-it-299a</link>
      <guid>https://dev.to/lizhuojunx86/my-checker-scored-one-component-compliant-and-another-deviant-neither-had-a-rule-behind-it-299a</guid>
      <description>&lt;p&gt;This is a follow-up to &lt;a href="https://dev.to/lizhuojunx86/my-routing-policy-and-my-traces-disagreed-96-times-never-once-on-the-main-thread-ffp"&gt;part four&lt;/a&gt;, not a new part. Part four measured my stated routing policy against what actually ran: 425 decisions, 96 deviations, $1,248.13, none of them on the main thread.&lt;/p&gt;

&lt;p&gt;Then a reader asked what happens when no rule applies. I went to measure it, and the answer was that my checker had been inventing verdicts. In both directions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;Brian Jin, &lt;a href="https://dev.to/kikashy/comment/3cl60"&gt;in the comments&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Have you considered making "no applicable rule" a first-class unresolved state rather than allowing it to fall through to a default? That seems useful for separating real compliance from policy coverage gaps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had not. My policy file has a &lt;code&gt;default_tier&lt;/code&gt;, and any decision that matched no rule quietly took it. That felt safe when I wrote it. Every decision gets a verdict, no gaps in the table, nothing to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the default was doing
&lt;/h2&gt;

&lt;p&gt;Frozen corpus, the same 425 decisions from part four: 424 matched an explicit rule. One did not.&lt;/p&gt;

&lt;p&gt;That one is a &lt;code&gt;product-manager&lt;/code&gt; component. No rule in my file mentions it. It took the default &lt;code&gt;frontier&lt;/code&gt; expectation, ran Opus, and scored &lt;strong&gt;compliant&lt;/strong&gt;. $0.91.&lt;/p&gt;

&lt;p&gt;Replaying the same policy on the live window, 784 decisions, the fall-through population has doubled. A &lt;code&gt;claude-code-guide&lt;/code&gt; component first appeared on July 30, matched nothing, took the same default &lt;code&gt;frontier&lt;/code&gt; expectation, ran Haiku, and got flagged as a &lt;strong&gt;deviation&lt;/strong&gt;. $0.08.&lt;/p&gt;

&lt;p&gt;Two rows, two verdicts, opposite signs, and neither one has a rule behind it. The default fabricated compliance in the first case and fabricated a deviation in the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fabricated deviation is the worse half
&lt;/h2&gt;

&lt;p&gt;A fake compliant row is a missed catch. Annoying, but the audit was never going to catch what it does not know about.&lt;/p&gt;

&lt;p&gt;A fake deviation is worse, because it puts noise into the one table the whole exercise asks you to trust. And in this case the routing was right: Haiku for a docs-lookup subagent is exactly what I would choose. What is wrong there is not the model. It is my policy's silence.&lt;/p&gt;

&lt;p&gt;If I had acted on that row I would have gone and "fixed" a correct routing decision.&lt;/p&gt;

&lt;p&gt;An unresolved state catches both rows for the same reason. Neither verdict has a rule behind it, so the honest output is not a verdict at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dual: rules no decision ever reached
&lt;/h2&gt;

&lt;p&gt;Measuring the fall-throughs turned up their mirror image, which I had not gone looking for.&lt;/p&gt;

&lt;p&gt;My file has seven rules. One of them, &lt;code&gt;research-explore&lt;/code&gt; outside the main thread to the cheap tier, has matched &lt;strong&gt;zero&lt;/strong&gt; decisions in both windows.&lt;/p&gt;

&lt;p&gt;Not because the case is rare. The frozen corpus alone has 83 &lt;code&gt;research-explore&lt;/code&gt; decisions. Every component that does research has its own component-level rule sitting earlier in the file, and the specificity tie-break shadows the task rule every time. The rule is dead and the file gives no sign of it.&lt;/p&gt;

&lt;p&gt;So coverage has two halves, and they fail differently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;direction&lt;/th&gt;
&lt;th&gt;what it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;decision → no rule&lt;/td&gt;
&lt;td&gt;uncovered behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rule → no decision&lt;/td&gt;
&lt;td&gt;dead, shadowed, or unexercised policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those two labels are Brian's, not mine. I had the two halves and called them a coverage report; he &lt;a href="https://dev.to/kikashy/comment/3d3m7"&gt;named them&lt;/a&gt; after the change shipped, and the names are better than what I was using.&lt;/p&gt;

&lt;p&gt;A policy file that grows for six months accumulates both, and reading it will not tell you which rules are load-bearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same hole on the actual side
&lt;/h2&gt;

&lt;p&gt;The expected side of the comparison is not the only one that can fail to resolve.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;claude-opus-5&lt;/code&gt; entered my corpus with no entry in the tier table, 5,992 traces sat unscoreable for two weeks. Not compliant, not deviant, silently absent from the table. That is part four's unpriced-model incident wearing a different field.&lt;/p&gt;

&lt;p&gt;Expected side or actual side, whenever a verdict cannot be resolved the honest output is a third state, and the size of that state is the coverage metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Policy.match&lt;/code&gt; now returns no match instead of falling through, and a &lt;code&gt;verdict&lt;/code&gt; column carries four values: &lt;code&gt;compliant&lt;/code&gt;, &lt;code&gt;deviation&lt;/code&gt;, &lt;code&gt;unresolved:no_rule&lt;/code&gt;, &lt;code&gt;unresolved:unknown_model&lt;/code&gt;. The &lt;code&gt;default_tier&lt;/code&gt; key still exists, but it only applies to a rule that matched and omitted its expected tier. Nine new tests, 437 pass.&lt;/p&gt;

&lt;p&gt;The summary carries the two coverage counts next to the verdict tallies: decisions out of coverage, and rules that never matched.&lt;/p&gt;

&lt;p&gt;Current corpus, 789 decisions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;verdict&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;compliant&lt;/td&gt;
&lt;td&gt;627&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deviation&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unresolved:no_rule&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unresolved:unknown_model&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That zero turned out to be the interesting entry. &lt;code&gt;unknown_model&lt;/code&gt; is empty because I added the &lt;code&gt;claude-opus-5&lt;/code&gt; tier two days before the verdict column went in, and those 5,992 traces would all have landed there.&lt;/p&gt;

&lt;p&gt;So the two unresolved classes decay differently, which is why they are counted apart. &lt;code&gt;unknown_model&lt;/code&gt; is an operational gap: fill in the tier table and the count returns to zero by itself. &lt;code&gt;no_rule&lt;/code&gt; does not. Those two rows sit until somebody writes a rule or decides the case belongs outside policy, and the count stays honest precisely because nothing clears it automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/kikashy/comment/3d3m7"&gt;Brian's reply&lt;/a&gt; when I showed him the result put it better than I had:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That makes unresolved much more than an error bucket. It becomes an observable property of the policy surface itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I still get wrong
&lt;/h2&gt;

&lt;p&gt;Tae Kim, &lt;a href="https://dev.to/hannune/comment/3cjj2"&gt;in the same comment thread&lt;/a&gt;, described his orchestrator defaulting to Opus on every retry. He had only specified the model for the main agent, and it ran about three weeks before he caught it by accident.&lt;/p&gt;

&lt;p&gt;My method would miss that. The grain is &lt;code&gt;(unit, component)&lt;/code&gt;, and a retry is not a separate component. It inherits inside the same one, so the deviation and the compliant call collapse into a single verdict. Same defect as mine, invisible to my checker.&lt;/p&gt;

&lt;p&gt;I have not fixed it, because I have exactly one corpus and it is my own, which is a poor basis for a schema change. If your orchestrator writes the model per attempt rather than per step, I would like to see a sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to run this
&lt;/h2&gt;

&lt;p&gt;The policy file is 30 lines of YAML and the checker reads Claude Code's own session transcripts. Nothing leaves your machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; traceguard.routing_audit.agent_lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the one-minute version: it reads &lt;code&gt;.claude/agents/**/*.md&lt;/code&gt; frontmatter and tells you which agents never pinned a model. The full per-decision scoring needs the trace store, and it is in &lt;a href="https://github.com/lizhuojunx86/traceguard" rel="noopener noreferrer"&gt;traceguard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The part worth stealing even if you never run any of it: if your compliance checker has a fallback default, it is producing verdicts nobody wrote a rule for, and you cannot tell those apart from the real ones by looking at the output. Count them separately. Count the dead rules too.&lt;/p&gt;

&lt;p&gt;Questions welcome, and if you have found a way to make the retry case visible I would rather hear it than work it out myself.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A routing audit for your team, fixed price.&lt;/strong&gt; The method in this series runs on any Claude Code trace store. Send me 30 days of your team's usage records (model, tokens, timestamps, agent and session ids; I'll send a one-line &lt;code&gt;jq&lt;/code&gt; filter that drops prompt and answer text before anything leaves your machine) and within ten working days you get one number and a five-page report: what share of your spend ran on a tier your own routing policy would not have chosen, what that cost at list price, and which components caused it. On my own 26,131 traces the number was 22.6% and $1,248.13, all of it on subagents and none on the main thread. US$1,500, flat. Write to &lt;a href="mailto:info@zhuojun.li"&gt;info@zhuojun.li&lt;/a&gt; with the subject "routing audit".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>I measured his app with his own code. He measured my claim with his own corpus.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Sun, 09 Aug 2026 01:34:47 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/i-measured-his-app-with-his-own-code-he-measured-my-claim-with-his-own-corpus-1l5m</link>
      <guid>https://dev.to/lizhuojunx86/i-measured-his-app-with-his-own-code-he-measured-my-claim-with-his-own-corpus-1l5m</guid>
      <description>&lt;p&gt;This is part five of a series about pointing an append-only audit log at things that count tokens. Parts one through four found accounting defects in other people's trackers and one expensive hole in my own routing. This one is about what happens after the report lands, because this time the audit ran in both directions: the maintainer shipped the fix in three days, and his closing comment contained two findings aimed back at my report. One of them caught a claim I had published without its scope. My re-measurement then caught the mechanism he had guessed for it.&lt;/p&gt;

&lt;p&gt;The target is &lt;a href="https://github.com/weltern/Clawdmeter" rel="noopener noreferrer"&gt;Clawdmeter&lt;/a&gt;, a desktop app that shows your live Claude Code usage with a pixel mascot. weltern announced the cross-platform release on r/ClaudeAI; I pointed my harness at its token path the same day and filed &lt;a href="https://github.com/weltern/Clawdmeter/issues/21" rel="noopener noreferrer"&gt;#21&lt;/a&gt;: every transcript-derived number in the app read about 2.34× high.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug, briefly
&lt;/h2&gt;

&lt;p&gt;Same class as &lt;a href="https://dev.to/lizhuojunx86/the-vendor-documents-this-bug-a-30k-star-repo-shipped-it-anyway-27pb"&gt;part three&lt;/a&gt;, so one paragraph and no re-litigation. Claude Code writes one assistant message as several JSONL records, one per content block, and each record repeats the same &lt;code&gt;message.id&lt;/code&gt; with the same &lt;code&gt;usage&lt;/code&gt; object. &lt;code&gt;src/transcript.py&lt;/code&gt; summed per record in three places: the token counts beside the 5h/7d bars, everything the Stats page prices, and the per-session totals. Driving those three functions over a synthetic corpus with a known-exact manifest: 2.34×, 2.34×, 2.37×. Both record-level paths emitted 1,249 events for 540 messages. The percentage bars were never affected, since those come from rate-limit headers, and the overage figure comes from the OAuth endpoint. The report said so, because a fair report names what is &lt;em&gt;not&lt;/em&gt; broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running his code instead of mine
&lt;/h2&gt;

&lt;p&gt;The part of the method worth stealing. Nothing in the report reproduces Clawdmeter's logic, models it, or reimplements it. The harness imports the vendor's own &lt;code&gt;transcript.py&lt;/code&gt; and calls his functions. The app is a Qt desktop program, and the token path doesn't need Qt, so the whole trick is a twelve-line stub:&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;pkg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ModuleType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PySide6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;qtcore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ModuleType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PySide6.QtCore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;qtcore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;   &lt;span class="c1"&gt;# three no-op classes
&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PySide6&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;pkg&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PySide6.QtCore&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;qtcore&lt;/span&gt;

&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clawdmeter_clone&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;          &lt;span class="c1"&gt;# his module, unmodified
&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_file_token_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# his functions, his numbers
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why bother, when reading the code already showed the defect? Because of the dispute it removes. A reimplementation invites "your model of my code is wrong", and that argument can run for days. Numbers that came out of the vendor's own functions leave exactly one question open, whether the input corpus is fair, and a synthetic corpus with a by-construction manifest answers that in the same breath. The report gets to say "your functions produce 2,592,168 where the manifest says 1,108,697" instead of "I believe your code over-counts".&lt;/p&gt;

&lt;p&gt;It also makes reproduction nearly free: &lt;code&gt;git clone&lt;/code&gt;, one script, about a minute, a fake &lt;code&gt;$HOME&lt;/code&gt;, no PySide6 install, nothing real touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three days
&lt;/h2&gt;

&lt;p&gt;Filed August 5. Confirmed August 6: "Thank you again for this report, I've confirmed the bug and will be patching it in an upcoming hotfix release." Shipped August 8 as v3.0.1, with a release note that opens the way I suggested it should, because a correct fix that halves every visible number looks exactly like a regression: "Your token and value numbers will drop by roughly 2.5×. That is the fix, not a regression."&lt;/p&gt;

&lt;p&gt;One detail I want to be flat about: he never ran my harness. He reproduced independently, on his own corpus, with his own tooling. That is not the fixture failing at its job. The fixture's job is to make the claim cheap to check and expensive to dispute; his choice to verify it his own way instead is a stronger confirmation than an exit code, and the diagnosis still arrived pre-named, which is what made three days possible. A reproduction script is an offer, not a toll booth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix report audited me back
&lt;/h2&gt;

&lt;p&gt;His closing comment is the reason this post exists. It opens with "Your report was accurate and the diagnosis was right", and then does something I had not seen a maintainer do in five repos: it treats the bug report itself as a measurable claim, measures it, and reports back where it breaks. "Two things turned up that are worth passing back, since you audit other trackers."&lt;/p&gt;

&lt;p&gt;His corpus came out worse than mine: 44,196 usage-bearing records across 18,585 distinct message ids, 73.0% on more than one record, work tokens 2.575× high. And then the two findings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: the repeated &lt;code&gt;usage&lt;/code&gt; objects are not always identical.&lt;/strong&gt; My report said that on my corpus, 100.0% of duplicated messages carried byte-identical usage on every record. On his corpus, 3,799 groups differ, and in every one &lt;code&gt;output_tokens&lt;/code&gt; is a running total: early records carry a partial count, the last carries the final figure. His example message reads 5, 5, 5, 328 across four records. That makes the choice of which record to keep load-bearing. A per-bucket max is correct; keep-first would have reported that message as 5 output tokens instead of 328.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second: collapsing per file is not enough.&lt;/strong&gt; 1,094 of his message ids appear in more than one transcript file, because resuming a session replays its records verbatim into a new file. Deduping within each file still left 1.095× inflation on his tree. The collapse has to live in the account-wide aggregator, and a replayed copy can carry an all-zero usage block where the original carries the real numbers, so a record-level dedup placed in front of the message collapse can keep the wrong copy. Folding every record into its message under a per-bucket max sidesteps the whole trap.&lt;/p&gt;

&lt;p&gt;He also turned my one-line invariant, usage events should equal distinct message ids, into a regression test, added six more, and ran nine mutations of the fix against them. Nine of nine caught. That last number is a stronger claim than any test count, and it is the part I would not have thought to ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split is a field, not a version
&lt;/h2&gt;

&lt;p&gt;Finding one corrects something I wrote, so I owed it a measurement rather than a thank-you. My "100.0% byte-identical" was true and under-scoped: it was true of the main conversation path, and I did not say so. On my corpus, regrouped:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;grouped per (file, message.id)&lt;/th&gt;
&lt;th&gt;groups&lt;/th&gt;
&lt;th&gt;byte-identical&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;main path&lt;/td&gt;
&lt;td&gt;10,645&lt;/td&gt;
&lt;td&gt;10,645 (100.00%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sidechain (subagent)&lt;/td&gt;
&lt;td&gt;16,051&lt;/td&gt;
&lt;td&gt;3,342 (20.82%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not one main-path group differs, across 63 days and 1,521 transcripts. Every differing group has a sidechain record in it. The discriminator is &lt;code&gt;isSidechain&lt;/code&gt;, not the Claude Code version he suspected — and the reason I can say that with some force is that the version hypothesis makes a testable prediction and the field hypothesis makes a different one. If the shape tracked the writer version, main-path groups would differ on some machines. They never do here. His Linux and macOS corpora "still showing the byte-identical shape" is what a main-path-heavy tree looks like; an agent-heavy tree shows his 3,799.&lt;/p&gt;

&lt;p&gt;His running-total observation, meanwhile, holds without exception on my side: 12,709 differing groups, 100% non-decreasing, 100% with the last record carrying the max. He was right about the shape and wrong about the cause. I was right about my corpus and wrong to publish the number without its denominator. Neither error survived a second corpus, which is the whole argument for sending measurements instead of opinions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The artifact I nearly handed him
&lt;/h2&gt;

&lt;p&gt;One warning went into my reply because it is the check anyone would write next, and it lies. Group repeated records by &lt;code&gt;message.id&lt;/code&gt; alone and my corpus reports 560 non-monotonic groups, output counts that go down mid-message. Every one is fake: records from unrelated files spliced into one sequence by the sort. Group per &lt;code&gt;(file, message.id)&lt;/code&gt; and all 560 vanish. I know the trap is real because I fell into it on a different tracker and reported the bad number before catching it.&lt;/p&gt;

&lt;p&gt;Two more numbers from the re-measurement, since they size his findings on an agent-heavy tree. Keep-first against per-bucket max at corpus scale: 20,740,608 output tokens against 38,533,663. Keep-first loses 46.2% of output, so max is not a tiebreak, it is most of the number. And his all-zero replay trap does not reproduce here at all, zero groups in 1,521 transcripts, which means you cannot harvest a test fixture for it from a tree like mine. His synthetic fixture for that case is load-bearing. The script is &lt;a href="https://github.com/lizhuojunx86/token-accounting-conformance/blob/main/clawdmeter-dedup/duplicate_usage_shape.py" rel="noopener noreferrer"&gt;&lt;code&gt;duplicate_usage_shape.py&lt;/code&gt;&lt;/a&gt;, read-only, stdlib only, and it prints the artifact count next to the real one so the next person doesn't rediscover the splice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red, green, and an API change that proves the point
&lt;/h2&gt;

&lt;p&gt;Yesterday I re-ran the harness against both tags. On v3.0.0 it is red the way the report said: 2.338×, 2.338×, 2.373× on the three call sites, exit 1. On v3.0.1 all three land exact, 1,108,697 and 732,191 to the digit, 540 rows for 540 messages, exit 0.&lt;/p&gt;

&lt;p&gt;The check needed one adaptation, and the adaptation is itself the verification. &lt;code&gt;_file_token_events&lt;/code&gt; no longer returns something you can sum: it now emits per-record events carrying both a record key and a message key, uncollapsed, with a docstring that says the collapse deliberately belongs to the account-wide caller. The fix is not a patch over the three sites; it is the architecture his second finding argued for, folded under the per-bucket max his first finding argued for. The code now contains the whole conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counterweights I owe you
&lt;/h2&gt;

&lt;p&gt;My 100.0% shipped without its scope, and the fixture didn't stop it. The harness was green, the totals were exact, and the prose still over-claimed. A fixture pins code; nothing pins your sentences except someone else's corpus.&lt;/p&gt;

&lt;p&gt;He never ran the harness, so this series' fastest fix owes nothing to the reproduction being runnable. What earned the three days was the diagnosis being named and the numbers being exact. I keep shipping the fixture anyway, but I should be honest that its value here was rhetorical, not operational.&lt;/p&gt;

&lt;p&gt;The isSidechain split is established on one corpus: mine. His 3,799 differing groups were not regrouped under the split, so "every differing group has a sidechain record in it" is a claim about my tree that his data could still complicate.&lt;/p&gt;

&lt;p&gt;I do not know &lt;em&gt;why&lt;/em&gt; Claude Code writes sidechain usage as a running total and main-path usage finished. The field predicts the shape; the mechanism behind the field is unmeasured.&lt;/p&gt;

&lt;p&gt;Part three argued response time tracks queue depth, and a solo maintainer answering in a day fits that line suspiciously well. One data point cannot separate "small queue" from "this particular maintainer", and the 30k-star PR from part three is still open, which proves the thesis only if you already believe it.&lt;/p&gt;

&lt;p&gt;His version hypothesis was reasonable on his data. It took a second corpus to kill it, exactly as it took his corpus to kill my 100.0%. One corpus per claimant is how both of us over-generalized, and there is no reason to believe I have stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would generalize
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Run their code, not your model of it.&lt;/strong&gt; A GUI is not a reason to reimplement a parser; a twelve-line stub deletes the entire "you misread my code" branch of the argument, and what is left is arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fix report is a dataset.&lt;/strong&gt; The closing comment on a bug you filed can contain more measurement than the bug did. Read it the way you read a transcript, and answer it with numbers, because the person who just fixed your finding is the best-calibrated reviewer your method will ever get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publish invariants with their denominator.&lt;/strong&gt; "100.0%" was true of a population I forgot to name. The correction cost a table; leaving it unscoped would have cost every future reader who audits an agent-heavy tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When two corpora disagree, look for a field before you blame a version.&lt;/strong&gt; A version split and a field split predict different shapes. Ask what each hypothesis forbids, then check for the forbidden thing. Zero differing main-path groups is the kind of zero that settles arguments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collapse under per-bucket max.&lt;/strong&gt; It is correct when duplicates are identical, correct when they are running totals, and immune to the all-zero replay copy. Keep-first was quietly losing 46.2% of output on my tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Group per (file, id), or the sort will manufacture your finding for you.&lt;/strong&gt; My 560 phantom groups came from splicing files together before grouping. Print the artifact count next to the real one so the check that lies gets caught by the check that doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;date (2026)&lt;/th&gt;
&lt;th&gt;event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aug 5&lt;/td&gt;
&lt;td&gt;v3.0.0 announced on r/ClaudeAI; harness pointed at the token path the same day; #21 filed: 2.34× at three call sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 6&lt;/td&gt;
&lt;td&gt;weltern confirms, promises a hotfix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 8&lt;/td&gt;
&lt;td&gt;v3.0.1 ships. Release note leads with "That is the fix, not a regression". His closing comment: 2.575× on his corpus, two findings passed back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 8&lt;/td&gt;
&lt;td&gt;Re-measurement posted: the split is &lt;code&gt;isSidechain&lt;/code&gt;, not the writer version; running total confirmed 100%; the 560-group splice artifact named&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 9&lt;/td&gt;
&lt;td&gt;Harness re-run: red on v3.0.0 as reported, exact on v3.0.1 through the new aggregator API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The layer underneath
&lt;/h2&gt;

&lt;p&gt;All five parts run on TraceGuard's &lt;code&gt;routing_audit&lt;/code&gt;, an append-only, &lt;code&gt;message.id&lt;/code&gt;-keyed ingest of Claude Code transcripts into SQLite. Apache-2.0, &lt;code&gt;pip install traceguard&lt;/code&gt;. The Clawdmeter harness and the shape-measurement script live in &lt;a href="https://github.com/lizhuojunx86/token-accounting-conformance/tree/main/clawdmeter-dedup" rel="noopener noreferrer"&gt;&lt;code&gt;clawdmeter-dedup&lt;/code&gt;&lt;/a&gt;; the corpus generator builds a fake &lt;code&gt;$HOME&lt;/code&gt; and touches nothing real.&lt;/p&gt;

&lt;p&gt;The series scoreboard, since part four's count is stale: seven shipped fixes across four repos — three in splitrail, two in tokscale (v4.9.0 and v4.11.0), Clawdmeter v3.0.1, and viberank's high-water-mark fix, deployed. The eighth is still an open PR behind a 135-deep queue, which part three already explained.&lt;/p&gt;

&lt;p&gt;The thing this part adds to the series is not a seventh notch. It is that the method survived contact with a maintainer who measured back, and got better for it: per-bucket max and per-(file, id) grouping are now in my collapse rules because his corpus broke my claim, and the version hypothesis died because mine broke his. Audit is not a thing you do &lt;em&gt;to&lt;/em&gt; a codebase. Done right, it is a thing two corpora do to each other.&lt;/p&gt;

&lt;p&gt;Li Zhuojun&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A routing audit for your team, fixed price.&lt;/strong&gt; The method in this series runs on any Claude Code trace store. Send me 30 days of your team's usage records (model, tokens, timestamps, agent and session ids; I'll send a one-line &lt;code&gt;jq&lt;/code&gt; filter that drops prompt and answer text before anything leaves your machine) and within ten working days you get one number and a five-page report: what share of your spend ran on a tier your own routing policy would not have chosen, what that cost at list price, and which components caused it. On my own 26,131 traces the number was 22.6% and $1,248.13, all of it on subagents and none on the main thread. US$1,500, flat. Write to &lt;a href="mailto:info@zhuojun.li"&gt;info@zhuojun.li&lt;/a&gt; with the subject "routing audit".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
