Repository navigation
Closed
Labels
plan:donePlanboard statusPlanboard status
Description
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_usdis 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.usageis per turn;modelUsageis 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 everyresult: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,
recordUsageanalytics, andrecordSpend.recordSpendfeeds 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
lastTotalCostUsdon 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
-pprocess per run. Confirm Grok'stotal_cost_usdsemantics, 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.
Metadata
Metadata
Assignees
Labels
plan:donePlanboard statusPlanboard status
Activity
currentbits commentedon Oct 4, 2026
Landed from Solenta thread "P1: Claude spend over-counted — cumulative total_cost_usd is". Closed on merge.
Bill only the cost growth on warm Claude turns (#1435) (#1583)
fix(usage): bill a warm Claude process's cost growth, not its running…
fix(usage): bill a warm Claude process's cost growth, not its running…
Port unpushed local-main fixes (#1435, #1436, #1439); classify setFas…