Skip to content

P1: Claude spend over-counted — cumulative total_cost_usd is added again on every turn of a warm process #1435

Closed

Description

@currentbits

Found during the third T3 Orchestrator V2 research pass (report: reports/T3 orchestrator ACP UX and repros.md; evidence: research_notes/T3 orchestrator ACP UX and repros/cli_experiments.md, both on branch coder/i-want-you-to-research-t3-codes-orchestr-fae77a).

Bug

  • Claude's total_cost_usd is a running total per process, not a per-turn figure. The CLI bundle documents it as "cumulative across turns in streaming-input sessions — each result carries the running total so far, so read the latest result rather than summing across results". Measured on Claude Code 2.1.283, successive results in one process reported 0.008809, 0.0115976 and 0.0138752. usage is per turn; modelUsage is cumulative too.
  • Solenta treats each result's total as that turn's own cost. It reuses warm Claude processes across turns: claudeSessions, up to 30 idle minutes and 3 idle processes (electron/runner.js:825-845). Then, on every result:
    const costDelta = Number(ev.total_cost_usd) || 0;   // runner.js:4624
    runUsage.costUsd += costDelta;
    ...
    store.setUsage(threadId, {... costUsd: prev.costUsd + costDelta ...});
    if (costDelta > 0) store.recordSpend(costDelta);    // runner.js:4651-4653
    store.recordUsage({ ..., costUsd: costDelta, ... }); // runner.js:4654+
  • The effect: turn N on a reused process records the cost of turns 1..N. For k similar turns on one process, recorded spend is about (k+1)/2 times the real cost. In measured run s1, two results would have been recorded as $0.0338 against a real $0.0182.
  • What it inflates: per-thread usage, recordUsage analytics, and recordSpend. recordSpend feeds the daily and orchestration budget gates (services.js:5123-5133), so those gates trip early.
  • The store's totals were not reconciled against billing, so the size of the real-world inflation is unmeasured.

Fix (~20 lines + test)

  • Keep lastTotalCostUsd on the kept-alive session entry. Set it to 0 when a process is spawned, and reset it whenever a new process replaces the old one.
  • On each result: costDelta = Math.max(0, total - session.lastTotalCostUsd); session.lastTotalCostUsd = total.
  • One-shot processes keep today's behaviour (their delta equals the total). That covers Grok, which goes through this same stream path but runs one -p process per run. Confirm Grok's total_cost_usd semantics, or that it's absent, before relying on that.
  • Don't apply the same diff to usage.input_tokens/output_tokens, which are already per turn.

Test

Extend a runner test that uses a fake Claude binary (CODER_CLAUDE_BIN + electron/test/support/fakeBin.js). Keep one fake process alive for two turns emitting cumulative totals 0.01 then 0.03. The thread's costUsd must be 0.03, not 0.04, and recordSpend must receive 0.01 then 0.02.

Activity

  1. added and removed on Oct 4, 2026
  2. currentbits commented on Oct 4, 2026

    @currentbits
    OwnerAuthor

    Landed from Solenta thread "P1: Claude spend over-counted — cumulative total_cost_usd is". Closed on merge.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions