<?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>The vendor documents this bug. A 30k-star repo shipped it anyway.</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Tue, 04 Aug 2026 13:03:18 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/the-vendor-documents-this-bug-a-30k-star-repo-shipped-it-anyway-27pb</link>
      <guid>https://dev.to/lizhuojunx86/the-vendor-documents-this-bug-a-30k-star-repo-shipped-it-anyway-27pb</guid>
      <description>&lt;p&gt;This is part three of a series about pointing an append-only audit log at things that count tokens. &lt;a href="https://dev.to/lizhuojunx86/a-missing-model-line-was-half-my-ai-agent-overspend-auditing-34-days-of-multi-model-claude-code-mc7"&gt;Part one&lt;/a&gt; found that a single missing model line was half my AI agent overspend. &lt;a href="https://dev.to/lizhuojunx86/an-append-only-audit-log-caught-two-accounting-bugs-in-a-216-star-usage-tracker-38co"&gt;Part two&lt;/a&gt; found two accounting bugs in splitrail, both of which undercounted — one hid 54% of my messages.&lt;/p&gt;

&lt;p&gt;This one points the other way. The target is &lt;a href="https://github.com/davila7/claude-code-templates" rel="noopener noreferrer"&gt;claude-code-templates&lt;/a&gt; — 30.1k stars, 3.3k forks, the largest Claude Code template repo — and its analytics &lt;strong&gt;over&lt;/strong&gt;-counted my tokens by 2.35×.&lt;/p&gt;

&lt;p&gt;The part that changed how I wanted to write this up: while drafting the report I went looking for whether the behaviour was documented anywhere. It is. Anthropic's own SDK documentation has a warning box about it, and ships a reference implementation of the fix. The bug was not hiding. Four separate mechanisms surrounded this code and none of them closed the loop, and the reason they didn't is the actual subject of this post.&lt;/p&gt;

&lt;p&gt;The fix has now been open for ten days. Two AI reviewers approved it. No human has replied. More on that near the end, because the silence turned out to carry information too.&lt;/p&gt;

&lt;p&gt;(Part four is the stated-vs-revealed routing analysis I promised at the end of part two. It got bumped. This was worth the detour.)&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;calculateRealTokenUsage()&lt;/code&gt; summed &lt;code&gt;message.usage&lt;/code&gt; over every parsed record.&lt;/p&gt;

&lt;p&gt;Claude Code writes a single assistant &lt;em&gt;message&lt;/em&gt; as several JSONL &lt;em&gt;records&lt;/em&gt; — one per content block (thinking / text / tool_use) — and every one of those records repeats the same &lt;code&gt;message.id&lt;/code&gt; and a &lt;strong&gt;byte-identical&lt;/strong&gt; &lt;code&gt;usage&lt;/code&gt; object. So each message's tokens were counted once per block, and everything downstream inflated by the average block count: &lt;code&gt;conversation.tokens&lt;/code&gt;, &lt;code&gt;summary.totalTokens&lt;/code&gt;, the per-project rollups, and the Total Tokens figure on the analytics dashboard.&lt;/p&gt;

&lt;p&gt;Measured against my frozen ~50-day corpus:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;property&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;distinct assistant &lt;code&gt;message.id&lt;/code&gt;s&lt;/td&gt;
&lt;td&gt;8,123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ids appearing on exactly one record&lt;/td&gt;
&lt;td&gt;2,533&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ids appearing on more than one record&lt;/td&gt;
&lt;td&gt;5,590 (&lt;strong&gt;68.8%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;of those, ids where &lt;em&gt;every&lt;/em&gt; record's &lt;code&gt;usage&lt;/code&gt; is byte-identical&lt;/td&gt;
&lt;td&gt;5,590 (&lt;strong&gt;100.0%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;records-per-id histogram&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1: 2533, 2: 1351, 3: 3553, 4: 415, 5: 58, 6: 138, 7: 24, 8: 33}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;records ÷ messages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈2.36×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three records per message is the mode — thinking, then text, then a tool call.&lt;br&gt;
Two-thirds of my assistant messages were being counted more than once, and on&lt;br&gt;
every single one of them the repeated &lt;code&gt;usage&lt;/code&gt; was byte-identical, which is the&lt;br&gt;
detail that makes the over-count a multiplication rather than a rounding error.&lt;/p&gt;

&lt;p&gt;Then I drove the analyzer over a synthetic corpus with known-exact totals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;th&gt;ground truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;input_tokens&lt;/td&gt;
&lt;td&gt;27,678&lt;/td&gt;
&lt;td&gt;11,447&lt;/td&gt;
&lt;td&gt;11,447&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;output_tokens&lt;/td&gt;
&lt;td&gt;2,632,674&lt;/td&gt;
&lt;td&gt;1,115,321&lt;/td&gt;
&lt;td&gt;1,115,321&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache_creation&lt;/td&gt;
&lt;td&gt;19,609,374&lt;/td&gt;
&lt;td&gt;8,130,148&lt;/td&gt;
&lt;td&gt;8,130,148&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache_read&lt;/td&gt;
&lt;td&gt;59,626,489&lt;/td&gt;
&lt;td&gt;24,447,923&lt;/td&gt;
&lt;td&gt;24,447,923&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;messagesWithUsage&lt;/td&gt;
&lt;td&gt;1,308&lt;/td&gt;
&lt;td&gt;540&lt;/td&gt;
&lt;td&gt;540 (distinct ids)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The line that turns this from a hunch into a diagnosis: &lt;strong&gt;before the fix, every field equalled the per-record sum exactly.&lt;/strong&gt; Not approximately. Exactness is what pins the cause to record-level summation rather than to anything in parsing, and it is why the report could name the defect instead of reporting a discrepancy.&lt;/p&gt;

&lt;p&gt;There is also a check that needs no corpus, no fixture and no golden file, which is the part I'd want you to take away: &lt;strong&gt;&lt;code&gt;messagesWithUsage&lt;/code&gt; should equal the number of distinct &lt;code&gt;message.id&lt;/code&gt;s.&lt;/strong&gt; It equalled the number of records. One assertion, available from day one, would have failed loudly.&lt;/p&gt;

&lt;h2&gt;
  
  
  It was in the docs the whole time
&lt;/h2&gt;

&lt;p&gt;Here is what I found when I went to check whether this was known. From Anthropic's &lt;a href="https://code.claude.com/docs/en/agent-sdk/cost-tracking" rel="noopener noreferrer"&gt;Agent SDK cost-tracking guide&lt;/a&gt;, in a callout box:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Parallel tool calls produce multiple assistant messages whose nested &lt;code&gt;BetaMessage&lt;/code&gt; shares the same &lt;code&gt;id&lt;/code&gt; and identical usage. &lt;strong&gt;Always deduplicate by ID&lt;/strong&gt; to get accurate per-step token counts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And in the prose above it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When Claude uses multiple tools in one turn, all messages in that turn share the same ID, so deduplicate by ID to avoid double-counting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The page ships a working &lt;code&gt;seenIds&lt;/code&gt; implementation. Its explanatory diagram is captioned, in part, &lt;em&gt;"Step 1 has four assistant messages sharing the same ID and usage (count once)."&lt;/em&gt; There is a public issue on the Claude Code repo with the symptom in its title — &lt;a href="https://github.com/anthropics/claude-code/issues/6805" rel="noopener noreferrer"&gt;#6805, "Token Usage Statistics Duplicated in stream-json Mode Causing Massive Cost Inflation"&lt;/a&gt;. And &lt;a href="https://github.com/getagentseal/codeburn" rel="noopener noreferrer"&gt;codeburn&lt;/a&gt;, another tracker in this space, documents its own global &lt;code&gt;seenMsgIds&lt;/code&gt; set doing precisely what the SDK page recommends.&lt;/p&gt;

&lt;p&gt;So this is not an obscure edge case. The SDK that produces the data tells consumers, in a warning box, to deduplicate by ID. A widely used consumer of that data summed per record instead, for as long as the function existed.&lt;/p&gt;

&lt;p&gt;I want to be exact about what my audit log did and did not contribute here, because the distinction is the whole point and I would rather make it myself than have it made at me: &lt;strong&gt;an append-only log was not necessary to discover this. Reading the documentation would have done it.&lt;/strong&gt; What the log contributed was three things the documentation cannot give you — whether &lt;em&gt;this particular codebase&lt;/em&gt; had the defect, what it &lt;em&gt;cost&lt;/em&gt; (2.36×, on 8,123 real messages), and proof that the fix was &lt;em&gt;exact&lt;/em&gt; rather than merely different.&lt;/p&gt;

&lt;p&gt;Documentation describes hazards. Measurement finds instances. Those are different verbs and the gap between them is where this bug lived.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the two AI reviewers said
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/davila7/claude-code-templates/pull/754" rel="noopener noreferrer"&gt;The PR&lt;/a&gt; went through two commercial AI code reviewers. Both cleared it. Neither mentioned the over-count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Greptile&lt;/strong&gt; posted a summary and a verdict:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Confidence Score: 5/5 — The PR appears safe to merge. No blocking failure remains.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It reviewed both commits, and left the only 👍 the PR has received.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cubic&lt;/strong&gt; ran twice. Its &lt;a href="https://github.com/davila7/claude-code-templates/pull/754/checks?check_run_id=89641027044" rel="noopener noreferrer"&gt;first pass&lt;/a&gt; reported &lt;em&gt;"AI review completed with 2 reviews. Found 2 issues across 3 files"&lt;/em&gt; — a maintainability note about the function being duplicated across two files, and this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;P3: Distinct records can still be merged when an &lt;code&gt;id&lt;/code&gt; equals a generated positional key or another record's &lt;code&gt;uuid&lt;/code&gt;, causing token undercounting for that conversation. Namespace persisted identifiers and use a non-colliding generated key for id-less records.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;That is a fair catch and it deserves credit.&lt;/strong&gt; It is a real hazard: &lt;code&gt;ConversationAnalyzer.js:297&lt;/code&gt; sets &lt;code&gt;id: item.message.id || item.uuid || null&lt;/code&gt;, so a uuid can already arrive in the &lt;code&gt;id&lt;/code&gt; field, and my fallback chain put &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt; and a positional key in a single namespace. On real Claude Code data the probability is approximately zero. The cost to eliminate it was four lines — namespace each source (&lt;code&gt;id:&lt;/code&gt; / &lt;code&gt;uuid:&lt;/code&gt; / &lt;code&gt;idx:&lt;/code&gt;) — plus a test that goes red before the change and green after. cubic's second pass dropped to &lt;em&gt;"1 issue found across 3 files."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now the observation, which I want to state as flatly as I can.&lt;/p&gt;

&lt;p&gt;Both tools read this function closely enough to reason about a hypothetical key collision in a line I had written minutes earlier. Neither remarked that the function's output had been wrong by a factor of two since the day it was written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is not a miss.&lt;/strong&gt; By the standard each was applying, both answers were correct. A diff-scoped reviewer answers &lt;em&gt;"is this change safe and self-consistent?"&lt;/em&gt; The pre-existing error was not in the diff, and nothing in a review context contains an independent measurement of what the right number is.&lt;/p&gt;

&lt;p&gt;The asymmetry is worth naming precisely, because it generalizes past these two products: &lt;strong&gt;cubic's finding was a hypothesis about the shape of code. The bug was a discrepancy between a number and reality.&lt;/strong&gt; Only the second kind of claim requires evidence from outside the repository. A reviewer holding the entire codebase in context still would not catch this one, because the code is internally consistent — it does exactly what it says it does, and what it says is wrong. You cannot read your way to that conclusion. Somebody has to measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hole nobody is looking at
&lt;/h2&gt;

&lt;p&gt;While I was in there: this repo runs &lt;strong&gt;no test suite on pull requests&lt;/strong&gt;. The only check on #754 is the AI reviewer. "Eight unit tests pass" is a sentence I had to type into a comment, because nothing in the pipeline can demonstrate it.&lt;/p&gt;

&lt;p&gt;With 135 open pull requests and 86 open issues, that is capacity, not negligence. But it is exactly the condition under which a silent numerical defect lives indefinitely, and it reorders the priorities: &lt;strong&gt;two AI reviewers and no test CI is a weaker correctness story than no AI reviewers and one test that asserts a total against a known input.&lt;/strong&gt; Reviewers are additive. They are not a ground truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten days later
&lt;/h2&gt;

&lt;p&gt;As of publication, &lt;a href="https://github.com/davila7/claude-code-templates/pull/754" rel="noopener noreferrer"&gt;#754&lt;/a&gt; has been open ten days. Every check is green, the branch merges cleanly, and the two AI-reviewer verdicts haven't moved since day one. Human words on the thread: zero. No label, no assignee, no milestone. The one open review question — which of the two duplicated copies of the function should survive — can only be answered by a maintainer, and hasn't been. In those same ten days the repo gained about 200 stars and its open-PR queue grew from 129 to 135.&lt;/p&gt;

&lt;p&gt;The same ten days, elsewhere in this series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;splitrail, 216 stars.&lt;/strong&gt; I filed &lt;a href="https://github.com/Piebald-AI/splitrail/issues/220" rel="noopener noreferrer"&gt;#220&lt;/a&gt;, a streaming-snapshot over-count, with the inflation ratios predicted from my corpus before any fix existed. A fix matching the prediction on every field was &lt;a href="https://github.com/Piebald-AI/splitrail/pull/222" rel="noopener noreferrer"&gt;merged within two hours&lt;/a&gt;. Third shipped fix in that repo for this series.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tokscale, 4.6k stars.&lt;/strong&gt; My drift report &lt;a href="https://github.com/junhoyeo/tokscale/issues/994" rel="noopener noreferrer"&gt;#994&lt;/a&gt; was fixed and released in &lt;a href="https://github.com/junhoyeo/tokscale/releases/tag/v4.9.0" rel="noopener noreferrer"&gt;v4.9.0&lt;/a&gt;. From the maintainer's closing note: "The measurement in this report is what made it actionable — the before/after table landing exactly on the drifted prediction pinned it to persistence rather than parsing, and saved us looking in the wrong place."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;viberank.&lt;/strong&gt; In &lt;a href="https://github.com/sculptdotfun/viberank/issues/83" rel="noopener noreferrer"&gt;an issue I filed about leaderboard drift&lt;/a&gt;, yoo-minho (he maintains the submission tool for a rival leaderboard, clauderank) confirmed the same class at a scale I can't generate: a month of roughly $15.5k list-price usage whose month-to-date total fell 11% between two submissions 16 hours apart. His words: "A cumulative month-to-date total went down, which shouldn't be possible." He then wrote the countermeasure up as &lt;a href="https://github.com/m1kapp/claude-rank/blob/main/docs/usage-drift-log.md" rel="noopener noreferrer"&gt;usage-drift-log&lt;/a&gt;, a six-field append-only record, and traceguard now &lt;a href="https://github.com/lizhuojunx86/traceguard/pull/30" rel="noopener noreferrer"&gt;implements it verbatim&lt;/a&gt;. One page of spec, two independent implementations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the method is not slow, and maintainers are not indifferent. Response time tracked queue depth, nothing else: the 216-star repo answered in hours, the 4.6k-star repo in days, and the 30.1k-star repo has 135 pull requests ahead of mine.&lt;/p&gt;

&lt;p&gt;Which corrected something I had wrong when I started this series. I treated the merge as the finish line. It isn't — a merge is a statement about maintainer bandwidth, and only the fixture is a statement about the code. It runs in a minute on anyone's machine, red on upstream main, green on the branch, and it means the same thing whether or not anyone at the repo ever reads it. If the Total Tokens figure on that dashboard matters to you, you don't need to trust me and you don't need to wait for the queue: &lt;code&gt;./run_check.sh&lt;/code&gt; settles it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The documentation already said it.&lt;/strong&gt; Worth repeating, because it is the strongest argument against the framing I originally wanted for this post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other trackers get this right.&lt;/strong&gt; ccusage, opcode and ccseva key on &lt;code&gt;messageId:requestId&lt;/code&gt;; codeburn keys on a global &lt;code&gt;seenMsgIds&lt;/code&gt; set. On my corpus the two strategies are equivalent — distinct &lt;code&gt;(message.id, requestId)&lt;/code&gt; pairs came out equal to distinct &lt;code&gt;message.id&lt;/code&gt;s, so &lt;code&gt;message.id&lt;/code&gt; alone was sufficient here. There is no "trackers are broken" story to sell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both signs occur.&lt;/strong&gt; splitrail undercounted. This one over-counted. No directional rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hypothesis of mine was false.&lt;/strong&gt; After part two I expected the subagent-directory blind spot to be an industry-wide pattern. I read thirteen trackers from source. It wasn't — only one unmaintained project had it, and a third-party had already reported the same class of gap in tokscale eighteen days before I filed mine. Saying so is more useful than the story I was hoping for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In tokscale, my proposed fix lost the argument, and should have.&lt;/strong&gt; I suggested the append-only store this series runs on. The maintainer turned it down: retention that never deletes would resurrect sessions a user deliberately removed, so he scoped retention to dedup keys that stay stable across files instead. I tested his rule — deleting a transcript dropped exactly that file's contents, 40 messages, 78,770 output tokens, nothing resurrected — and conceded in the thread. The audit finds the discrepancy. It has no special authority over the fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cubic found something my method wouldn't have.&lt;/strong&gt; A latent hazard in code that had not yet met hostile data leaves no trace in any measurement, because it hasn't happened yet. The two approaches cover different failure classes, and I'd run both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd generalize
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reading is not measuring.&lt;/strong&gt; Documentation, stars, code review, and more code review are all mechanisms for reading. Stacking more of them does not asymptotically produce a measurement. If nothing in your pipeline compares an output to an independently derived number, that comparison is not happening, however many eyes are on the diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assert an invariant, not a total.&lt;/strong&gt; Totals need fixtures, corpora and maintenance. &lt;code&gt;messagesWithUsage == distinct message.ids&lt;/code&gt; needs none of those and would have failed on the first commit. Nearly every accounting path has an invariant like this hiding in it; the work is noticing which one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exactness is the evidence.&lt;/strong&gt; "Roughly double" is a shrug that invites a debate about whose numbers are right. "Exactly the per-record sum, to the digit, on every field" is a diagnosis that ends the debate before it starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship the mechanism with the report.&lt;/strong&gt; Four upstream fixes across two projects so far, three in splitrail and one in tokscale, the fastest merged two hours after the report — all because a runnable red-green fixture arrived attached to the claim rather than after someone asked for one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When the same hazard hits several tools, converge on a contract, not more reports.&lt;/strong&gt; The rewrite-drift class now has a one-page spec with two independent implementations behind two different leaderboards. That does more than a third bug report would have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When the vendor documents a hazard, treat it as a test case, not as trivia.&lt;/strong&gt; Every warning box in an SDK's docs is a bug someone has already shipped. That is a free list of things to go measure.&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;Jul 24&lt;/td&gt;
&lt;td&gt;corpus reconciliation flags the ratio: 8,123 distinct message ids spread across ~2.36× as many records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 25&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/davila7/claude-code-templates/pull/754" rel="noopener noreferrer"&gt;PR #754&lt;/a&gt; filed with fixture, fix and measurements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 25&lt;/td&gt;
&lt;td&gt;Greptile: 5/5, safe to merge. cubic: 2 issues found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 25&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;7793bba8&lt;/code&gt; — namespaced dedup key, two added tests, answering cubic's P3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 25&lt;/td&gt;
&lt;td&gt;cubic re-review: 1 issue. All checks pass, no conflicts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 31&lt;/td&gt;
&lt;td&gt;splitrail merges the series' third fix there (&lt;a href="https://github.com/Piebald-AI/splitrail/issues/220" rel="noopener noreferrer"&gt;#220&lt;/a&gt; → &lt;a href="https://github.com/Piebald-AI/splitrail/pull/222" rel="noopener noreferrer"&gt;#222&lt;/a&gt;) two hours after the report; &lt;a href="https://github.com/sculptdotfun/viberank/issues/83" rel="noopener noreferrer"&gt;yoo-minho confirms rewrite drift&lt;/a&gt; on a $15.5k month, −11% silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 1&lt;/td&gt;
&lt;td&gt;his six-field &lt;a href="https://github.com/m1kapp/claude-rank/blob/main/docs/usage-drift-log.md" rel="noopener noreferrer"&gt;usage-drift-log&lt;/a&gt; spec adopted verbatim into traceguard (&lt;a href="https://github.com/lizhuojunx86/traceguard/pull/30" rel="noopener noreferrer"&gt;PR #30&lt;/a&gt;) — second independent implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 3&lt;/td&gt;
&lt;td&gt;tokscale ships &lt;a href="https://github.com/junhoyeo/tokscale/releases/tag/v4.9.0" rel="noopener noreferrer"&gt;v4.9.0&lt;/a&gt;, closing &lt;a href="https://github.com/junhoyeo/tokscale/issues/994" rel="noopener noreferrer"&gt;#994&lt;/a&gt; with credit to the measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 4&lt;/td&gt;
&lt;td&gt;this post. #754: checks green, two bot approvals, zero human replies, 135 open PRs in the queue&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 three parts of this series run on &lt;a href="https://github.com/lizhuojunx86/traceguard" rel="noopener noreferrer"&gt;TraceGuard&lt;/a&gt;'s &lt;code&gt;routing_audit&lt;/code&gt; module — an append-only, &lt;code&gt;message.id&lt;/code&gt;-keyed ingest of Claude Code transcripts into a SQLite trace store. It is a few hundred lines and a database file, and everything above is downstream of one design decision: never let the source rewrite history.&lt;/p&gt;

&lt;p&gt;Since part two, that layer grew a public edge: after every scheduled ingest it appends the six-field usage-drift-log record (frozen at first sight, never recomputed), so the drift class yoo-minho measured shows up as a warning line instead of a silent change.&lt;/p&gt;

&lt;p&gt;Stable totals are the substrate, not the product. The thing worth building on top is stated-vs-revealed routing analysis, priced per decision — which model you &lt;em&gt;said&lt;/em&gt; you'd route to, which one actually ran, and what the difference cost. That's part four, and this time I mean it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;pip install traceguard&lt;/code&gt; — Apache-2.0. The reproduction harness for this post is self-contained, runs in about a minute, and touches no real &lt;code&gt;~/.claude&lt;/code&gt; data — it builds a synthetic &lt;code&gt;$HOME&lt;/code&gt; and hands the upstream analyzer nothing but a directory: &lt;a href="https://github.com/lizhuojunx86/traceguard/tree/main/usage-tracker-audit/cct-dedup-check" rel="noopener noreferrer"&gt;&lt;code&gt;usage-tracker-audit/cct-dedup-check&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>codereview</category>
      <category>opensource</category>
    </item>
    <item>
      <title>An append-only audit log caught two accounting bugs in a 216-star usage tracker</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Sat, 25 Jul 2026 02:17:08 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/an-append-only-audit-log-caught-two-accounting-bugs-in-a-216-star-usage-tracker-38co</link>
      <guid>https://dev.to/lizhuojunx86/an-append-only-audit-log-caught-two-accounting-bugs-in-a-216-star-usage-tracker-38co</guid>
      <description>&lt;p&gt;Last month I wrote about &lt;a href="https://dev.to/lizhuojunx86/a-missing-model-line-was-half-my-ai-agent-overspend-auditing-34-days-of-multi-model-claude-code-mc7"&gt;auditing 34 days of multi-model Claude Code usage&lt;/a&gt; and finding that a single missing model line was half my overspend. The core of that piece was a boring engineering decision: ingest every assistant message from Claude Code's session files into an &lt;strong&gt;append-only log keyed by &lt;code&gt;message.id&lt;/code&gt;&lt;/strong&gt;, and never let the source rewrite history.&lt;/p&gt;

&lt;p&gt;This is the follow-up I didn't plan to write. The same log just caught two accounting bugs in &lt;a href="https://github.com/Piebald-AI/splitrail" rel="noopener noreferrer"&gt;splitrail&lt;/a&gt;, a 216-star Rust usage tracker for agentic CLIs — and within twelve days of the first report, both were fixed in an official release: one through my PR, one through a maintainer-authored patch that adopted the report's analysis. Here's the play-by-play, with the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: your history quietly rewrites itself
&lt;/h2&gt;

&lt;p&gt;Claude Code rewrites session JSONL files in place when you resume or compact a conversation. Between two scans of the &lt;em&gt;same file&lt;/em&gt;, five of my assistant messages vanished — usage that was already incurred, gone from the record. Any tracker that recomputes totals from live files inherits this drift: yesterday's numbers change while you sleep.&lt;/p&gt;

&lt;p&gt;I filed it as &lt;a href="https://github.com/Piebald-AI/splitrail/issues/200" rel="noopener noreferrer"&gt;#200&lt;/a&gt;. The maintainer confirmed the mechanism the same day — splitrail re-read live files, so messages removed by resume/compaction disappeared from historical totals — and shipped a fix in 3.6.0: a local SQLite history store that persists normalized usage and merges it with current session data before deduplication. Fast, clean work.&lt;/p&gt;

&lt;p&gt;One wrinkle: he suggested validating by comparing 3.5.9 vs 3.6.0 totals, expecting "3.6.0 should be higher." That's subtly wrong, and the distinction matters for anyone testing this class of fix: &lt;strong&gt;on a cold start the two versions are equal&lt;/strong&gt; — the history store has nothing to restore yet. The divergence only appears &lt;em&gt;across&lt;/em&gt; drift events. So I built the test to create one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The validation protocol
&lt;/h2&gt;

&lt;p&gt;Everything ran against a frozen snapshot (APFS clone) of &lt;code&gt;~/.claude/projects&lt;/code&gt;, so both versions scanned identical bytes, with every invocation under an isolated &lt;code&gt;$HOME&lt;/code&gt; — own config, own history store, no upload path. Then:&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;assertion&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;cold-start parity: fresh 3.5.9 == fresh 3.6.0 on identical input&lt;/td&gt;
&lt;td&gt;✅ identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;3.5.9 across a simulated resume/compact rewrite&lt;/td&gt;
&lt;td&gt;✅ drops by exactly the removed usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;3.6.0 across the same rewrite&lt;/td&gt;
&lt;td&gt;✅ totals unchanged — history store restores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;3.6.0 restart stability, 3 consecutive runs&lt;/td&gt;
&lt;td&gt;✅ byte-identical output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The simulated rewrite removes the last five assistant message-groups from the largest transcript — the same shape as the original drift event. The whole thing is now a self-contained regression fixture (&lt;a href="https://github.com/Piebald-AI/splitrail/pull/208" rel="noopener noreferrer"&gt;PR #208&lt;/a&gt;, since merged): it fails red on 3.5.9 and passes green on 3.6.0, which is exactly what you want a regression test to do.&lt;/p&gt;

&lt;p&gt;The part I care most about: on the transcripts splitrail scans, 3.6.0 agreed with my append-only log &lt;strong&gt;token-exact — 18,548,947 output tokens on both sides&lt;/strong&gt; across 13.5k messages. Two independent implementations, different languages, different dedup strategies, same number to the digit. (The tiny message-count delta was zero-usage api-error records splitrail intentionally skips.) When two systems reconcile exactly, every remaining discrepancy is a &lt;em&gt;finding&lt;/em&gt;, not noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: the discrepancy that was a finding
&lt;/h2&gt;

&lt;p&gt;Because the log records where every message came from, I could classify all of them against the live tree:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;files&lt;/th&gt;
&lt;th&gt;messages&lt;/th&gt;
&lt;th&gt;output tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;live, main transcript (what splitrail scans)&lt;/td&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;td&gt;13,704&lt;/td&gt;
&lt;td&gt;18,548,947&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;live, &lt;code&gt;subagents/**&lt;/code&gt; transcript&lt;/td&gt;
&lt;td&gt;1,423&lt;/td&gt;
&lt;td&gt;16,160&lt;/td&gt;
&lt;td&gt;13,738,324&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vanished (file exists, message.id gone)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deleted (session file gone)&lt;/td&gt;
&lt;td&gt;874&lt;/td&gt;
&lt;td&gt;11,821&lt;/td&gt;
&lt;td&gt;14,238,006&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Row two is the second bug. Claude Code writes subagent transcripts (Task tool: Explore, general-purpose, custom agents) under &lt;code&gt;projects/&amp;lt;slug&amp;gt;/&amp;lt;sessionId&amp;gt;/subagents/&lt;/code&gt;, at directory depth ≥ 4. Splitrail's discovery hard-caps at depth 2 in three places (&lt;code&gt;WalkDir…min_depth(2).max_depth(2)&lt;/code&gt;, a &lt;code&gt;components() == 2&lt;/code&gt; path check, and the glob pattern). Those files are structurally invisible — &lt;strong&gt;54% of my live messages, and roughly a third of the dollars, never entered any total&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The model mix makes the blind spot vivid: splitrail saw 22 of my 5,059 Sonnet messages and 0 of 1,566 Haiku messages. Those models run almost exclusively inside subagents. If you delegate heavily to cheap models — which is exactly what cost-conscious agent users do — your tracker undercounts the most, on the workflows you optimized hardest.&lt;/p&gt;

&lt;p&gt;Filed as &lt;a href="https://github.com/Piebald-AI/splitrail/issues/207" rel="noopener noreferrer"&gt;#207&lt;/a&gt; with the layout, a two-line repro, and a suggested fix. Two days later it was closed — and not by my patch. The maintainer wrote the fix himself: &lt;a href="https://github.com/Piebald-AI/splitrail/pull/209" rel="noopener noreferrer"&gt;#209&lt;/a&gt; "Include Claude Code subagent transcripts" (+428/−100 across seven files), whose description restates the issue's analysis and adopts its central caution — simply lifting the depth cap isn't enough; subagent records have to flow through the same &lt;code&gt;local_hash&lt;/code&gt; dedup semantics as main transcripts, or you trade an undercount for a double-count. Both fixes shipped together in &lt;strong&gt;splitrail 3.6.1&lt;/strong&gt;, which hit Homebrew within a day. On my corpus, the newly visible class is row two of the table above: 16,160 messages and 13.7M output tokens finally entering the totals. A fresh 3.6.1-vs-log reconciliation of the live tree is the obvious next check — part 3 starts there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd generalize
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live mutable files are not an audit trail.&lt;/strong&gt; If your source can rewrite history, recomputation is not accounting. Append-only ingest with a stable per-record identity (&lt;code&gt;message.id&lt;/code&gt;, last-write-wins on partials) is cheap insurance — mine is a few hundred lines and a SQLite file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconcile token-exact or you know nothing.&lt;/strong&gt; "Close enough" totals hide entire bug classes. It was exact agreement on the scanned subset that turned the remaining gap into two nameable, fixable defects instead of a shrug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decompose the gap before you blame anyone.&lt;/strong&gt; "Your numbers are lower than mine" is an accusation; &lt;em&gt;"the gap is exactly deleted-files + an unscanned directory class, here's the table"&lt;/em&gt; is a bug report a maintainer can act on in hours. Both turnarounds here — #200→#204 in eight days, #207→#209 in two — happened because the mechanism arrived with the report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frozen snapshot + isolated &lt;code&gt;$HOME&lt;/code&gt; is the whole trick for tracker A/B.&lt;/strong&gt; Clone the data, pin &lt;code&gt;HOME&lt;/code&gt; (and &lt;code&gt;XDG_STATE_HOME&lt;/code&gt;/&lt;code&gt;XDG_DATA_HOME&lt;/code&gt; — on Linux, state dirs honor XDG and will escape your sandbox; CodeRabbit's review caught that one in my own fixture, fair is fair), and two binaries scanning identical bytes become a controlled experiment.&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;Jul 11&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Piebald-AI/splitrail/issues/200" rel="noopener noreferrer"&gt;#200&lt;/a&gt; filed — resume/compact rewrites session JSONL in place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 19&lt;/td&gt;
&lt;td&gt;#204 merged; &lt;strong&gt;3.6.0&lt;/strong&gt; ships the SQLite history store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 20&lt;/td&gt;
&lt;td&gt;4-assertion A/B validation on a frozen corpus; token-exact reconciliation vs the audit log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 21&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Piebald-AI/splitrail/issues/207" rel="noopener noreferrer"&gt;#207&lt;/a&gt; filed (subagent blind spot); &lt;a href="https://github.com/Piebald-AI/splitrail/pull/208" rel="noopener noreferrer"&gt;PR #208&lt;/a&gt; opened; CodeRabbit review round addressed same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 22&lt;/td&gt;
&lt;td&gt;PR #208 merged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 22–23&lt;/td&gt;
&lt;td&gt;Maintainer authors and merges &lt;a href="https://github.com/Piebald-AI/splitrail/pull/209" rel="noopener noreferrer"&gt;#209&lt;/a&gt;; #207 closed; &lt;strong&gt;3.6.1&lt;/strong&gt; released, Homebrew bumped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twelve days, two bugs, zero arguments about whose numbers were right — the log settled every question before it became a debate.&lt;/p&gt;

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

&lt;p&gt;All of this ran on &lt;a href="https://github.com/lizhuojunx86/traceguard" rel="noopener noreferrer"&gt;TraceGuard&lt;/a&gt;'s &lt;code&gt;routing_audit&lt;/code&gt; module (v1.1.0, "audit evidence layer") — the append-only, message.id-keyed ingest of Claude Code transcripts into a SQLite trace store that part 1 was built on. Stable totals are the substrate; the point of the layer above is &lt;strong&gt;stated-vs-revealed routing analysis, priced per decision&lt;/strong&gt; — which model you &lt;em&gt;said&lt;/em&gt; you'd route to, which one actually ran, and what the difference cost. That's part 3, and my totals finally sit still long enough to write it. (Apparently not a niche itch, either — the splitrail maintainer starred TraceGuard during this exchange and said he'd be interested in the audit part.)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;pip install traceguard&lt;/code&gt; — Apache-2.0. The regression fixture is ~600 lines of stdlib Python + bash, merged in &lt;a href="https://github.com/Piebald-AI/splitrail/pull/208" rel="noopener noreferrer"&gt;PR #208&lt;/a&gt;; the subagent fix shipped in splitrail &lt;a href="https://github.com/Piebald-AI/splitrail/releases" rel="noopener noreferrer"&gt;3.6.1&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>observability</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A missing `model:` line was half my AI agent overspend — auditing 34 days of multi-model Claude Code</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Sun, 05 Jul 2026 16:41:59 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/a-missing-model-line-was-half-my-ai-agent-overspend-auditing-34-days-of-multi-model-claude-code-mc7</link>
      <guid>https://dev.to/lizhuojunx86/a-missing-model-line-was-half-my-ai-agent-overspend-auditing-34-days-of-multi-model-claude-code-mc7</guid>
      <description>&lt;p&gt;I'm a solo developer running 10+ projects with a multi-model agent setup: Opus 4.8 as the main thread, Fable 5 (2× Opus price) as a decision advisor, Sonnet/Haiku for mechanical subagent work. The question I couldn't answer: &lt;strong&gt;which tasks actually deserve the expensive tier, and where was I overpaying?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built a routing audit as an opt-in module of my tracing SDK, backfilled ~26,000 traces (34 days) from Claude Code's local session logs, priced them at official list prices, and diffed my &lt;em&gt;stated&lt;/em&gt; routing policy against my &lt;em&gt;revealed&lt;/em&gt; routing behavior. Everything below is list-price equivalent (I'm on a subscription; API-paying teams see these numbers for real).&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 0: your usage history is quietly rewriting itself
&lt;/h2&gt;

&lt;p&gt;Claude Code's resume/compact rewrites session JSONL files. Between two ingest runs, 5 messages vanished from a source file. If your usage numbers come from re-reading live session files (as most usage dashboards do), they drift. An append-only ingest log fixed it — and gave an unexpected validation: my Fable traces reproduce June's export-control suspension window to the hour (1,599 traces on Jun 10, exactly zero from Jun 14–30, resuming Jul 1).&lt;/p&gt;

&lt;p&gt;Takeaway: &lt;strong&gt;snapshot your telemetry into an immutable store, or you're auditing sand.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: one config line was most of the waste
&lt;/h2&gt;

&lt;p&gt;I wrote my routing policy down as a declarative YAML (frontier/mid/cheap tiers × agent role × task type), then diffed it against 425 observed routing decisions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;22.6% deviated from my own policy — $1,248 of list-price spend.&lt;/li&gt;
&lt;li&gt;Root cause of the single biggest cluster ($657, 53% of deviation cost): &lt;strong&gt;subagents inherit the parent thread's model unless the agent definition pins one.&lt;/strong&gt; When my main thread ran the advisor model, mechanical subagents silently ran on it too — 2× Opus price for grep-tier work. Verified 19/19 against parent sessions, zero counterexamples.&lt;/li&gt;
&lt;li&gt;The fix is one &lt;code&gt;model:&lt;/code&gt; line per agent file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bonus honesty: my first audit pass "found" 28.2% deviation. A third of that was my &lt;em&gt;policy file&lt;/em&gt; being wrong, not my behavior (I'd written rules I never actually intended). &lt;strong&gt;The first thing a routing audit fixes is your policy statement.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: counterfactuals cost pennies; protocols are the hard part
&lt;/h2&gt;

&lt;p&gt;"Was the 2× advisor model worth it?" I replayed 12 real advisor consults on the cheaper tier. Predicted cost: $22.81. Actual: &lt;strong&gt;$0.21&lt;/strong&gt; — my estimator was 110× off, because replays are cold-start prompts without the original ~660k-token cached context. Quality counterfactuals are essentially free; nobody skips them because of cost.&lt;/p&gt;

&lt;p&gt;Then I blind-judged the 12 pairs myself, and the protocol failed in three instructive ways: I &lt;em&gt;recognized&lt;/em&gt; my own original conversations in 4/12 pairs (33% leak); my deterministic position assignment put 9/12 originals in slot B (so "B wins" was confounded with "original wins"); and originals had full context while replays didn't. Net result: the advisor-premium question is &lt;strong&gt;still open&lt;/strong&gt;. Protocol v2: third-party judge, position-balanced, longer delay. I'm publishing the failure because an audit tool's dev log should look like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you run agent fleets
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin &lt;code&gt;model:&lt;/code&gt; in every subagent definition.&lt;/strong&gt; Inheritance is silent and expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot usage logs immutably.&lt;/strong&gt; Live session files rewrite themselves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write your routing policy down.&lt;/strong&gt; The diff between stated policy and revealed routing is where the money hides — and half of what you'll find is that your policy was never real.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The module is contract-external, local-first (nothing leaves your machine), built on &lt;a href="https://github.com/lizhuojunx86/traceguard" rel="noopener noreferrer"&gt;traceguard&lt;/a&gt; — a point-in-time-correct LLM instrumentation SDK originally built for quant pipelines, where "which model knew what, when" is an audit requirement, not a nice-to-have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask:&lt;/strong&gt; I'm doing a handful of free routing audits for teams running multi-model agent fleets (especially where quality is measurable — trading, data pipelines, eval-gated workflows), in exchange for anonymized learnings. DM or open an issue.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>claude</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>The `epsActual` That Wasn't: 15% of an LLM Backtest's Trades Were Decided on Data That Didn't Exist Yet</title>
      <dc:creator>Li Zhuojun</dc:creator>
      <pubDate>Thu, 18 Jun 2026 11:16:59 +0000</pubDate>
      <link>https://dev.to/lizhuojunx86/the-epsactual-that-wasnt-15-of-an-llm-backtests-trades-were-decided-on-data-that-didnt-exist-17k3</link>
      <guid>https://dev.to/lizhuojunx86/the-epsactual-that-wasnt-15-of-an-llm-backtests-trades-were-decided-on-data-that-didnt-exist-17k3</guid>
      <description>&lt;p&gt;We were backtesting an LLM-driven earnings signal against a field called &lt;code&gt;epsActual&lt;/code&gt; — the kind of field everyone treats as ground truth. It isn't.&lt;/p&gt;

&lt;p&gt;About &lt;strong&gt;41.4%&lt;/strong&gt; of those "actual" values were &lt;em&gt;different&lt;/em&gt; from what the vendor had first reported. About &lt;strong&gt;15.3%&lt;/strong&gt; differed enough to flip a tradeable decision. When we re-ran the backtest using only the values that actually existed at each decision date, the strategy kept ~&lt;strong&gt;73%&lt;/strong&gt; of its returns and ~&lt;strong&gt;82%&lt;/strong&gt; of its Sharpe. The rest was look-ahead bias — and it rode in through a field whose name promised it was final.&lt;/p&gt;

&lt;p&gt;This is a writeup of how we found it, how we measured it honestly, and the one-line invariant that turns it from a silent inflation into a loud test failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The signal is a post-earnings drift play: at each earnings print, an LLM scores the release and we take a position. To backtest it you replay history — for every past print, reconstruct what the model &lt;em&gt;would&lt;/em&gt; have decided, then check what happened next.&lt;/p&gt;

&lt;p&gt;That reconstruction needs one obviously-trustworthy input: what the earnings number actually &lt;em&gt;was&lt;/em&gt;. Our vendor exposes exactly that, in a field named &lt;code&gt;epsActual&lt;/code&gt;. "Actual." Final. Settled. You query a print from two years ago and get a number back. What could go wrong?&lt;/p&gt;

&lt;h2&gt;
  
  
  The invisible killer
&lt;/h2&gt;

&lt;p&gt;Vendor "actuals" are not frozen at print time. They get backfilled, corrected, and restated — sometimes the next day, sometimes months later. Restatements, late filings, parser fixes, standardization passes: all of them quietly rewrite history. &lt;strong&gt;The value you query today for a 2023 print is not, in general, the value that was available the day after that print.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is textbook look-ahead bias, and it's especially dangerous here because it doesn't &lt;em&gt;look&lt;/em&gt; like leakage. Nobody fed the model future data on purpose. It rode in on a field everyone trusts — and "actual" is about the most trustworthy-sounding name a field can have. A backtest built on today's &lt;code&gt;epsActual&lt;/code&gt; is quietly asking the model to react to numbers that, on the decision date, did not yet exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we measured it honestly
&lt;/h2&gt;

&lt;p&gt;You can't detect this from a single snapshot of the database — by definition the revision has already overwritten the original. So we built a &lt;strong&gt;forward-polling harness&lt;/strong&gt;: poll the vendor on a schedule, snapshot every value we care about, and watch for changes over time. It had accumulated ~&lt;strong&gt;1,400 snapshots&lt;/strong&gt; in the first day of polling.&lt;/p&gt;

&lt;p&gt;The decision that mattered most:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Detect revisions by the value itself, not by the vendor's &lt;code&gt;lastUpdated&lt;/code&gt; timestamp.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;lastUpdated&lt;/code&gt; is unreliable — it doesn't reliably fire on silent backfills, and trusting it would have hidden exactly the revisions we were hunting. So change detection keys on the &lt;strong&gt;value-tuple&lt;/strong&gt;: if any tracked field changes between two snapshots, that's a revision, regardless of what the metadata claims.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Revision = the tracked value-tuple changed between snapshots,
# NOT "the vendor bumped lastUpdated".
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_revision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev_snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;curr_snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tracked_fields&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev_snapshot&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tracked_fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;curr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;curr_snapshot&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tracked_fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To quantify the &lt;em&gt;trading&lt;/em&gt; impact, we compared two backtests over a four-month point-in-time window: a &lt;strong&gt;naive&lt;/strong&gt; one using today's revised &lt;code&gt;epsActual&lt;/code&gt;, and an &lt;strong&gt;as-of&lt;/strong&gt; one using only each value as first seen on (or before) the decision date.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;41.4%&lt;/strong&gt; of &lt;code&gt;epsActual&lt;/code&gt; values (896/2163) differed between first-seen and final.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15.3%&lt;/strong&gt; of cases (332/2163) differed enough to flip a tradeable decision — a sign change or a threshold crossing in the signal.&lt;/li&gt;
&lt;li&gt;Over the four-month window, the as-of backtest retained ~&lt;strong&gt;73%&lt;/strong&gt; of the naive backtest's returns and ~&lt;strong&gt;82%&lt;/strong&gt; of its Sharpe. (The FINAL leg keeps drifting as the vendor keeps revising, so treat the &lt;em&gt;ratio&lt;/em&gt; as more stable than the levels.)&lt;/li&gt;
&lt;li&gt;Read inversely: roughly a quarter of the headline returns, and a fifth of the Sharpe, were look-ahead artifacts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The encouraging half: most of the strategy survives honest data. The sobering half: a naive backtest overstated it by a wide margin, and a meaningful fraction of "winning" trades were decided on numbers that did not exist at decision time. A 15% decision-flip rate is not noise you can wave away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is structural, not a one-off
&lt;/h2&gt;

&lt;p&gt;The natural reaction is "okay, we'll be careful with that field." That doesn't hold. The risk is reintroduced by every new feature, every new vendor, every rerun, every teammate who reaches for "the actual value." Carefulness is a property of a person on a good day; &lt;strong&gt;as-of correctness has to be a property of the pipeline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So treat the question &lt;em&gt;"could this value have been known at the decision time we're simulating?"&lt;/em&gt; as an invariant the code enforces and CI checks. A vendor "actual" is &lt;strong&gt;time-versioned reference data&lt;/strong&gt;: it only becomes valid at the instant you first observed it. Use it to decide &lt;em&gt;before&lt;/em&gt; that instant and you're using a value from the future.&lt;/p&gt;

&lt;p&gt;That's exactly what the look-ahead invariant below checks — it requires &lt;code&gt;valid_from &amp;lt;= feature_as_of&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;traceguard.validators.lookahead&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;validate_reference_timing&lt;/span&gt;

&lt;span class="c1"&gt;# The eps "actual" is time-versioned reference data: valid_from is when this
# specific value first existed (first-seen in our snapshots), feature_as_of is
# the decision moment we are simulating.
&lt;/span&gt;&lt;span class="nf"&gt;validate_reference_timing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;eps_first_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# when this value actually existed
&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="n"&gt;decision_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# the moment we're simulating
&lt;/span&gt;    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vendor_eps_actual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# raises InvariantViolation if eps_first_seen &amp;gt; decision_date
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a value is used before its availability timestamp, the run fails loudly rather than silently inflating a Sharpe ratio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of look-ahead — don't conflate them
&lt;/h2&gt;

&lt;p&gt;It's worth being precise about scope. There are &lt;strong&gt;two&lt;/strong&gt; distinct kinds of look-ahead bias in LLM pipelines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Training contamination&lt;/strong&gt; — the model itself was pre-trained on the future you're predicting, so it "recalls" rather than reasons. That's a separate research problem (membership-inference tests, point-in-time LLMs, claim-level temporal verification), and it needs different tooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness / pipeline leakage&lt;/strong&gt; — your code uses a value, prompt, or model that didn't exist at the simulated time. &lt;em&gt;This story is entirely about this kind&lt;/em&gt;, and it's the kind a pipeline can be made to refuse structurally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both matter. They are not the same problem, and conflating them is how teams "fix" one and ship the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist you can apply today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat every &lt;code&gt;actual&lt;/code&gt; / &lt;code&gt;final&lt;/code&gt; / &lt;code&gt;reported&lt;/code&gt; vendor field as a &lt;strong&gt;moving target&lt;/strong&gt; until you've proven otherwise with your own snapshots.&lt;/li&gt;
&lt;li&gt;Detect revisions by &lt;strong&gt;value&lt;/strong&gt;, not by the vendor's update timestamp.&lt;/li&gt;
&lt;li&gt;Backtest on &lt;strong&gt;as-of (first-seen)&lt;/strong&gt; data, and explicitly measure the gap against revised data. That gap is your look-ahead tax — quantify it instead of assuming it's zero.&lt;/li&gt;
&lt;li&gt;Encode "known at decision time?" as a &lt;strong&gt;CI invariant&lt;/strong&gt;, so the failure mode is a red test, not a flattering backtest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;One vendor, one field, a four-month window. The exact percentages are dataset-specific and should not be read as universal constants — your numbers will differ. And again: this addresses harness leakage only, not whether the model itself has seen the future.&lt;/p&gt;




&lt;p&gt;The validators and point-in-time instrumentation here are part of &lt;a href="https://github.com/lizhuojunx86/traceguard" rel="noopener noreferrer"&gt;&lt;strong&gt;traceguard&lt;/strong&gt;&lt;/a&gt; — an open-source Python library for point-in-time-correct LLM instrumentation: a model registry that refuses anachronistic picks, a git-tracked prompt registry, canonical input hashing, and look-ahead invariants you call in CI. It's not a dashboard — it exports OpenTelemetry spans into Langfuse / Phoenix, so it sits &lt;em&gt;underneath&lt;/em&gt; your observability stack and keeps the timeline honest.&lt;br&gt;
&lt;/p&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;traceguard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've been burned by a backtest that looked great and meant nothing, I'd genuinely like to hear how it happened — that's the failure mode this is built to catch.&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>finance</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
