Skip to main content
Back to blog

I Rebuilt the Counter I Threw Away

10 min read

I've published three counts of my own token usage. Two of them came from a Postgres telemetry lake I built in April — it ingests every session, backs itself up at 03:55 every morning, and has a tab on my dashboard. It's a real instrument and it still runs. Then, for the third count, I went around it: straight to the raw transcript files, with a script I wrote in an afternoon and deleted the moment I was done. I said so at the time, in the closing paragraph, like it was charming: "by a script I wrote in an afternoon."

The lake is why that shortcut was avoidable. Deleting the shortcut is why this post exists.

So this time I wrote the transcript counter properly and kept it: scripts/count_tokens.py. It walks ~/.claude/projects/**/*.jsonl, takes the assistant lines, sums the four usage buckets, groups by model and by project. The difference from the throwaway is that it still exists tomorrow — and, unlike the lake, it sees the nested subagent transcripts, which turns out to matter.

The count

3,503 transcript files. 3,350 of them carry usage. 121,810 assistant turns. 24.69 billion tokens.

Split: 207.5M uncached input, 118.4M output, 1.28B cache writes, and 23.08B cache reads.

One caveat the script prints on every run, because it's a real limit and not a footnote: 295 of those turns are counted twice. Resumed sessions re-read their parent's transcript, so a turn can appear in more than one file. There's a --dedupe flag that drops them. The default counts them, to match how the earlier posts counted, and the number is reported rather than quietly absorbed. It's a rounding error at this scale — 0.2% — but an instrument that hides its known error is the thing I'm trying to stop building.

The three patterns I'd found before all reproduced on the new instrument, which is the only reason re-measuring is interesting:

Pattern Last count This count
Bash is half my tool calls 52.8% 50.3% of 62,604
Cache reads are ~90% of everything 90.5% 93.5%
One project is half the work homelab ≈ half homelab 13.87B of 24.69B

Three findings, two instruments, both agreeing. Those are the ones I'll keep building on. RailMind is the surprise in second place at 7.64B — an iPad track planner is now nearly a third of my entire AI footprint, which tells you what I've actually been doing this summer regardless of what I thought I was doing.

Then I checked it against my own homework

The obvious test for a new instrument is to point it at a window you've already published and see if it agrees. My last post covered April 8 to July 7 and reported 91,908 turns and 15.58 billion tokens, from 2,608 files.

I ran the new counter over exactly that window. It said 78,050 turns. 13.82 billion. From 2,320 files.

Fifteen percent fewer turns. Eleven percent fewer tokens. Roughly 288 files short. That's not rounding.

My first assumption was a bug in my own parser — that I was skipping assistant lines the old script had counted. That one I could check, so I did: of every assistant line on disk inside that window, 78,050 of 78,050 carry a usage block. Zero without. There is nothing there for my parser to have missed.

So one of two things is true. Either there is less on disk than there was on July 7th, or my new script counts differently than the old one did.

And here is the punchline of this entire post: for this particular pair of numbers, I cannot tell which.

I can't, because answering it requires running the July 7th script against today's files, and that script does not exist. It was the shortcut — the afternoon transcript walk I took instead of querying the lake — and I deleted it the day I ran it. The one artifact that could distinguish "the world changed" from "my ruler changed" for this comparison was the one I didn't keep.

Here's what makes that sting rather than shrug. The lake would have answered it. The two earlier counts both came from the lake, which means those two are comparable — same instrument, held still, still queryable for the May and June windows today. I had a fixed ruler. I built it, I back it up nightly, I put it on a dashboard. And then for one count I set it down, picked up a ruler I made that afternoon, took the measurement, and snapped the ruler in half. The number that moves 15% is the one measurement in the series with nothing stable behind it — because it's the one where I stepped around the stable thing.

A number that moves is only information if the instrument held still. For the lake-to-lake counts, mine did. For this one, I made sure it couldn't.

The measurement I got wrong on the way

I nearly published something much worse than uncertainty.

Chasing the shortfall, I broke the corpus down by month — top-level session transcripts against the nested subagent files that parallel work leaves behind — and got a table with a hole in it. April: zero top-level sessions. May: zero. I had a whole section written about it. Every session from those months, gone; only orphaned subagent files left behind, limbs with no torso.

It was an artifact of my own measurement. I'd bucketed the files by mtime — when the file was last written — and called it "when that work happened." Those are not the same question. A session started in April and resumed in July carries a July mtime, and several of mine were bulk-touched on the first and ninth of July by something I haven't identified. Re-cut on the first content timestamp inside each file — when the conversation actually began — and the hole closes:

Month Top-level, by mtime Top-level, by content
2026-04 0 5
2026-05 0 8
2026-06 94 123
2026-07 296 254

Thirteen April and May sessions are sitting on my disk right now. I was about to tell you they'd been deleted.

I want to sit in that for a second, because it's the same bug as everything else I've fixed this week. My briefing reported a hardcoded zero. My health check reports healthy because npx is on PATH. And I, writing the post about those, produced a zero that was an artifact of the field I chose to sort by — and it was such a good zero. It was dramatic. It had a table. I'd have defended it.

What saved it wasn't my judgment. It was handing the draft to something whose only instruction was to refute me, which went and counted the files a different way. The same move that makes the pipeline trustworthy is the one that makes the writing trustworthy, and I don't get to skip it just because I'm the one holding the pen.

The record is thin in April — 5 sessions, 14 subagent files, for a month I know I worked in. Something is missing. But "thin" is what I can prove, and "gone" was a story I told because my sort key told it to me.

The part that actually stung

Two committed pieces of code in my stack read these transcripts: backfill_sessions.py, which feeds the lake on a session-end hook, and thread_indexer_worker.py, which runs hourly. The lake feeder walks the tree correctly. The hourly one does not: its docstring says it globs **/*.jsonl, and line 179 actually calls project_dir.glob("*.jsonl") — no recursion.

So it sees the top-level session files and nothing else. 390 of 3,503. Eleven percent. It has never once read a subagent transcript, which is 89% of everything I've generated, and every fan-out I've ever run.

Then I went looking for what it does with the 390, and it's worse than the glob. The worker is gated behind a CORTEX_WRITE env var, and that variable is set nowhere — not in the LaunchAgent plist, not in my env file, nowhere. Its own live report says would_write_to_cortex: false. It runs every hour, on the hour, reads eleven percent of my transcripts, and writes a JSON file to /tmp that nothing reads and I had forgotten existed.

I've been calling this thing my session indexer all week. It indexes nothing. It's an hourly no-op with a docstring describing a system I never finished, and I'd have kept citing it as infrastructure indefinitely, because it's green, and it's scheduled, and it never errors.

Fixed the glob tonight — one word, rglob, plus the tests the worker never had, which is precisely how a docstring and its implementation disagreed for the worker's entire life without anything noticing. The CORTEX_WRITE question is a different decision — whether I want that pipeline at all — and I'm not going to make it at midnight by flipping a variable.

What the instrument is for

I'm being careful with the dollar figures this time. The counter prints cost, and the arithmetic checks out, but the rates come from a table I maintain by hand — I typed them in from what my own earlier posts asserted. So it's an estimate resting on my memory of a pricing page, and I marked it that way in the code, with a comment telling the next reader to re-check it before quoting a number. The token counts are ground truth from the files. The dollars are a model. Two different epistemic animals, and the script now says which is which.

One thing it does refuse to do: price a model it doesn't know. If an unfamiliar model shows up, the function returns None rather than 0.0, the row prints unpriced, and it's excluded from the total with a loud warning. There's a model in my corpus right now — <synthetic>, 311 turns — that trips exactly this path. A zero would have slid into the total and looked like a fact.

Here's the correction I owe the record, because I nearly wrote the opposite. My first draft of this post said I'd measured my tokens three times with a throwaway script and deleted it every time — that I'd never kept a ruler at all. That's a better story and it's false. I built the ruler. It's called the lake, it's been running since April, and two of the three counts came straight out of it. What I actually did was worse in a quieter way: I had the durable instrument, and for one measurement I set it aside for something faster, and then I didn't even keep the fast thing.

That's the failure, stated honestly. Not "never built the ruler." Built it, back it up nightly, dashboard and all — then stepped around it once and threw away the step. The lake-to-lake numbers hold. The one number I can't stand behind is the one where I bypassed the lake, and I can't stand behind it because of the bypass, not because I never had anything better.

So the transcript counter is committed now, next to the lake instead of instead of it, and that's the actual deliverable — not the 24.69 billion. A count is only worth the ruler behind it, and the lesson I paid for this week is that having a good ruler doesn't help if you put it down for the convenient one and don't write down what you did.

Keep the instrument. And when you reach past it for something quicker, keep that too — because the measurement you can't reproduce is worth exactly nothing, no matter how real the number looked when it printed.