Skip to content

v1.14.0 ·

The cost report that was billing another project

aidlc cost <instance> printed numbers nobody believed. For the interactive-cli-menu instance it said $485.99 and 49.7 hours, with all of it in one phase. Implementation held every dollar. Deployment, design, ideation, requirements and testing showed "no data".

Three things were wrong, and the roadmap item that started this found only one.

The time column measured waiting, not working. 49.7 hours was the claim window, and the largest single gap in it was 24.4 hours, overnight, because a gate asked a question and the answer came the next day.

The phase breakdown was broken by a field name. phaseWindows() in packages/cli/src/cost/recorder.ts read rec.timestamp from each transition record, but the field is called at. An as unknown as cast hid the mismatch. Every transition was skipped, one window covered all time, and all four call sites seeded it with 'implementation'. Eighteen of the 19 instances on the current transition format had all of their priced usage under the wrong phase. The three older instances were the only ones reporting correctly. The code worked only where the data was old.

And some instances reported nothing. aidlc cost sync refused with 5 candidate records, ambiguous, not guessing, because it picked transcript files by modification time. A long session spread over several rotated transcripts is normal, and it was being treated as a puzzle.

The dollars were also wrong, in a different way

The roadmap item said cost was 28 times too high because it was measured over a wide window. That part was not true. Each usage record is priced from its own token counts, so an idle gap adds nothing. I checked that before writing requirements, and it removed the biggest piece of planned work.

But I then found the real contamination. The code that locates a session walked every project under the Claude projects directory with no check that the entry belonged to this repository. A function that already did the check correctly, and explained the hazard in its own comment, sat 200 lines above and was never called. 711 entries, 262.5M tokens, had been attributed from an unrelated repo. packages/cli/src/cost/provenance.ts now classifies each entry, and packages/cli/src/cost/purge.ts removed the foreign ones, only on positive evidence. The cast is gone, so a renamed field is now a compile error. interactive-cli-menu now reports $146.91, spread across all seven phases.

Fixing the phase labels meant re-deriving history. 4,134 entries were relabelled from ledger data alone. Six were split from transcripts into 1,868 per-record entries. 917 records moved to unattributed because they fell inside no claim, or two. Each split stays inside the entry's own coverage window. One entry's cursor sits in a 633-record transcript while the entry covers 80 of them, and splitting the whole file would have inflated it 23 times.

Why active time uses turns, not a gap cutoff

The obvious fix for the time column is a gap threshold: count gaps under some cutoff as work. I measured it and rejected it. Across 13,516 gaps the distribution decays smoothly with no natural break, and the total swings 2.6 times across the plausible range, from 24.15 hours at 60 seconds to 62.06 hours at 1800 seconds. Any number I picked would be described as derived. This project has been bitten by that before.

Instead I used the turn boundaries the platform already records. Walking parentUuid links, 13,574 of 13,638 usage records (99.5%) resolve to one of 270 turns, with no heuristic. Summing first-to-last time per turn alone gives 198.91 hours, more than any threshold, because one turn can stay open across a long pause. So each entry now carries active_ms, the union of record spans within a turn.

What it does not fix

This leans on fields the platform does not document. If they change, fidelity degrades visibly instead of silently. Candidate selection now uses record timestamps, and aidlc cost sync emits zero ambiguous skips.