Note
Grok responding on behalf of Julius.
Triage (main @ b24f0fbb)
Confirmed in code. The reader has no step to drop identical copies of the same call. Response ids are the only thing it merges on:
metadata()returns the entry's main usage plus one usage for every retry entry: field 9 and the field 28 list for steps, field 4 and the field 17 list for generations (L131-L137).readDatabasemakes a record for every usage of every step and then every generation (L202-L253). Ids come only from fields 11/12/7 (L227-L230). With no id, the key falls back toantigravity:${sessionId}:${source}:${index}:${usageIndex}(L244), so every copy gets its own key.readAntigravityUsagemerges candidates only when they share a key from that id list (L325-L333).UsageAggregator.adddrops only repeateddedupeKeys (usageAggregation.ts L176-L180). Copies without an id never collide.
I checked this locally against main's reader with a made-up DB. One call with no id is stored as a step usage, a generation usage, and a generation retry entry, all with the same counters. The reader returns 3 records (step:0:0, generation:0:0, generation:0:1).
Tests. Every Antigravity fixture that has both a step and a generation copy gives them a shared field-11/12 id (usageTranscriptReader.test.ts L531-L590, L615-L658, L696-L739). No fixture covers id-less copies, which is where the duplicates come from. In the one retry fixture, the retry has different counters and its own id (retry-1), and the test expects it to stay a separate record.
Origin. Not a later regression. The multi-source reading, the retry lists and the per-source fallback key have been there since the reader landed in #10409. The only later change to the file, #15101, renamed fast to speed.
Fix direction. Dedupe within each conversation in readDatabase, before the id merge:
- Drop a retry entry whose counters match its entry's main usage, unless it carries a different id. The existing retry test, with different counters and its own id, should still pass.
- Pair id-less step and generation usages that have identical counters, nth with nth, so that two real identical calls are both kept.
- Keep the response-id merge across files as it is.
Add a fixture with id-less step, generation and retry copies.
Caveats:
- Matching on counters is a heuristic. Two genuinely separate calls in one conversation with exactly the same counters (fields 2/3/4/5/9/10) would be collapsed if their copies don't line up nth with nth. That is probably rare, but it can happen.
- It's not clear from the code whether
steps.idxandgen_metadata.idxline up one-to-one. The model fallback appears to assume they do (L238), but pairing by idx isn't verified, so pairing by counters is the safer choice. - Reading only generations would undercount, because step-only stores and step usages with no generation are handled on purpose (L741-L758).
Related.
- Open PRs #13902 (skip unchanged Antigravity DBs on repeat scans) and #13809 (Gemini Flash pricing) also edit
antigravityUsageReader.ts, so a fix here will likely conflict with them. - #16104, #15198 and #15299 are about limits, not token counts. #7858 is double counting from a directory shared across Windows and WSL. They're related but separate.
Unconfirmed:
- Whether real Antigravity DBs store the same usage in all three places, and the reporter's numbers (34 files, 10,592 records vs 3,598 calls, 2.94×; 165 of 10,702 usages with ids). I can't check either without the databases. The code path above counts such copies separately if they're there.