Skip to content

claude adapter: duplicate result usage records are summed, doubling totalCost and forcing a spurious budget_exceeded exit #14

Closed

Description

@sblattj

Summary

When the Claude CLI emits its final result line twice in one run (observed with CLI 2.1.277 in --output-format stream-json --verbose mode, both copies identical and within the same millisecond), the claude adapter pushes one usage event per result line and the driver adds both to the cumulative cost. The run then reports double the real cost, usage.cost.tokens is exactly doubled, and the budget check trips on the doubled total, so a run that finished normally exits as budget_exceeded.

Observed

Three ach run --agent claude --json runs on the same day, same CLI version:

Run tokens[] rows usage events per-model row cost totalCost exitStatus
1 2 (identical, same timestamp) 2 18.89 35.93 budget_exceeded (budget 30)
2 2 (identical, 1 ms apart) 2 23.99 45.82 budget_exceeded (budget 25)
3 1 1 52.89 52.89 success

In runs 1 and 2 every output the agent was asked to write was present and the final result had been emitted, so the budget_exceeded status is an accounting artifact rather than an enforcement decision. usage.cost.reportedCostUsd in those runs is exactly 2x the per-model row, and usage.cost.tokens.inputTokens is exactly 2x the row's input tokens.

The duplicated rows carry the same session_id, the same modelUsage totals and the same total_cost_usd, which is the CLI's cumulative-final figure for the run, not a per-turn delta.

Where

  • src/adapters/claude.ts, the kind === 'result' branch: pushes { type: 'usage' } on every result line, no dedupe.
  • src/core/driver.ts, the budget check (cumulativeCost > budgetUsd): treats each usage event as additive.
  • The NDJSON splitter in the adapter is not the cause; it parses each line once and the close-handler flush only fires on a leftover partial line.

Runs 1 and 2 also carry several step (system init) events for the same session id spread across the run, while run 3 has one. That may be related to the CLI re-emitting init/result lines, or unrelated; noting it as a possible correlate only.

Expected

  • A result line from the Claude CLI is a cumulative-final record for the session. Two identical ones should count once. Suggested: dedupe on (session id, total_cost_usd, token totals, timestamp within a small window), or take the last result per session as authoritative instead of summing.
  • Budget enforcement should use the deduped cumulative figure.
  • When a run finished (result emitted, process exited 0) and the budget was only crossed by the final record, the exit status should reflect completion, or at least a warning should say the budget was exceeded by the final accounting rather than by mid-run enforcement.

Repro sketch

Feed the adapter a stream-json transcript that ends with the same result line twice (identical JSON, or 1 ms apart) and a --budget-usd slightly below 2x the row's cost but above 1x. Expected: totalCost equals the row's cost and exitStatus is success. Actual: doubled cost and budget_exceeded.

A unit test in the adapter suite that asserts a repeated identical result yields one usage record would pin this.

Activity

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 isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions