Skip to content

[Bug]: Usage counts most Antigravity calls three times (step, generation and retry copies are all added) #17071

Description

@cornishandy

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Have some Antigravity history: conversations under ~/.gemini/antigravity/conversations (the Antigravity app) and/or T3 Code's Antigravity profile, <state dir>/providers/antigravity/<sha256 of instance id>/antigravity-acp/conversations.
  2. Open Usage and compare Antigravity's requests and tokens with the number of model calls actually made. Or run main's reader directly from the repo root (Node ≥ 23.6; prints aggregates only):
// repro.mts: node repro.mts <conversations dir>...
import { readAntigravityUsage } from "./apps/server/src/usage/antigravityUsageReader.ts";

const { files, errors } = await readAntigravityUsage(process.argv.slice(2), 0);
const records = files.flatMap((file) => file.records);
const tokens = (rs) => rs.reduce((n, { totals: t }) => n + t.uncachedInputTokens + t.cachedInputTokens + t.cacheCreationTokens + t.outputTokens, 0);
// Identical counters within one conversation = one call stored several times.
const calls = new Map(records.map((r) => [JSON.stringify([r.sessionId, r.totals]), r]));
console.log({ files: files.length, errors: errors.length, records: records.length, dedupeKeys: new Set(records.map((r) => r.dedupeKey)).size, tokens: tokens(records), distinctCalls: calls.size, distinctTokens: tokens([...calls.values()]) });

Expected behavior

Each Antigravity model call is counted once.

Actual behavior

Most Antigravity calls are counted three times, so Antigravity's requests, tokens and API-equivalent cost on the Usage page are about 3× too high.

Antigravity writes the same usage message to up to three places in a conversation database. Counts of non-zero usage messages across 34 conversation files on one Mac:

Where Field path Usages
steps.metadata 9 3,598
gen_metadata.data 1 → 4 3,506
gen_metadata.data retry list 1 → 17 → 2 3,506, each with the same counters as its generation's main usage
steps.metadata retry list 28 → 2 92, each with the same counters as its step's main usage

All 3,506 generation usages have exactly the same counters (fields 2, 3, 4, 5, 9 and 10) as a step usage in the same conversation; the other 92 step usages have no generation entry. Only 165 of the 10,702 usages carry a response id (fields 11/12/7): 55 calls whose three copies share their ids, so only those are merged today.

Where the copies get through:

  • metadata() returns an entry's main usage plus every retry entry (L131–L136).
  • readDatabase makes a record for every usage of every step and every generation (L202–L244). Without a response id, the fallback dedupeKey (…:${source}:${index}:${usageIndex}, L244) is different for each copy.
  • readAntigravityUsage merges only records that share a response id (L325–L331), and UsageAggregator drops only repeated dedupe keys (usageAggregation.ts L176–L177), so every id-less copy is counted.

The script above on those 34 files (9 from the Antigravity app, 25 from T3 Code's Antigravity profile):

{ files: 34, errors: 0, records: 10592, dedupeKeys: 10592, tokens: 924376492, distinctCalls: 3598, distinctTokens: 312264011 }

That's 10,592 records for 3,598 calls (2.94×), and 924M tokens for 312M.

Impact

Minor bug or occasional failure

Nothing breaks, but every Antigravity figure on the Usage page (requests, tokens, API-equivalent dollars) is about 3× too high.

Version or commit

T3 Code Nightly 0.0.46-nightly.20261008.2801; the reader is unchanged on main @ b24f0fb

Environment

macOS 27.2, desktop app (Nightly). Antigravity provider (gemini-3.8-flash-high, gemini-pro-agent) plus the Antigravity app's own history (app 2.21.1).

Workaround

None in the app.

Possible fix

Within one conversation, count each call once:

  • a retry entry with the same counters as its entry's main usage (and no different response id) is the same usage;
  • a step usage and a generation usage with identical counters are the same call. Pair them nth with nth, so a conversation that really made two identical calls keeps both, each with its own time and model;
  • keep merging by response id across files, as now.

Reading only generations wouldn't be enough: 92 step usages here have no generation entry.

Activity

  1. juliusmarminge commented on Oct 8, 2026

    @juliusmarminge
    Member

    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).
    • readDatabase makes 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 to antigravity:${sessionId}:${source}:${index}:${usageIndex} (L244), so every copy gets its own key.
    • readAntigravityUsage merges candidates only when they share a key from that id list (L325-L333). UsageAggregator.add drops only repeated dedupeKeys (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:

    1. 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.
    2. Pair id-less step and generation usages that have identical counters, nth with nth, so that two real identical calls are both kept.
    3. 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.idx and gen_metadata.idx line 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.

    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.
  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions