Skip to content

fix(claude): group token accounting must not depend on line adjacency#123

Open
akesling wants to merge 2 commits into
mainfrom
fix/claude-group-usage-interleave
Open

fix(claude): group token accounting must not depend on line adjacency#123
akesling wants to merge 2 commits into
mainfrom
fix/claude-group-usage-interleave

Conversation

@akesling

@akesling akesling commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Root cause

Two holes in the message-level usage dedup, both the same class as the #106 over-counting bug:

  1. Interleave double-count. canonicalize_message_usage and sum_usage assumed a group_id's JSONL lines are contiguous. docs/agents/formats/claude-code/known-issues.md documents multi-terminal interleaved writes that split a group's lines — each contiguous fragment then contributed a full message total, over-counting the session.
  2. Id-less messages. group_id came only from message.id, which is Option. An id-less assistant message split across content-block lines repeated its full usage on every line with group_id = None — each line became its own accounting unit and the usage was summed N times.

Fix

  • canonicalize_message_usage now groups globally by group_id in one O(n) pass (field-wise max across all occurrences, total placed on the group's last occurrence), instead of walking contiguous runs.
  • sum_usage dedups via a precomputed last-occurrence map instead of next-line adjacency.
  • group_id falls back to the entry-level requestId for assistant entries when message.id is absent (verified against docs/agents/formats/claude-code/jsonl-envelope.md: requestId is per-API-request, assistant-only, documented for deduping streamed messages). User entries never group.
  • docs/agents/formats/claude-code/usage.md and the CLAUDE.md token-accounting bullet updated to match.

The projector needed no change: it re-expands group totals via its own group-keyed map, independent of which turn carries the total.

Tests

Three new tests (TDD, red before the fix): interleaved groups count once (was 250, now 150 for A(m1)/B(m2)/C(m1)); id-less assistant message groups by requestId (was 20, now 10); user entries never group by requestId. All existing group/projector round-trip tests unchanged and green. Full workspace suite + clippy green. toolpath-claude bumped 0.12.0 → 0.12.1.

Notes

  • Part of a six-PR consistency series from a cross-harness audit; CHANGELOG/version lines may conflict with sibling fix PRs — resolve by keeping both entries.
  • No path-cli/toolpath-cli bump here; one release bump lands after the series merges.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

akesling added 2 commits July 8, 2026 13:51
canonicalize_message_usage and sum_usage assumed a group_id's lines
were contiguous. Multi-terminal writers can interleave a split
message's lines with another message's (docs/agents/formats/claude-code/
known-issues.md), splitting a group into two contiguous runs and
double-counting the message total. Separately, group_id was None
whenever message.id was absent, turning every content-block line of
an id-less assistant message into its own accounting unit.

Both functions now group by group_id globally instead of by
adjacency, and group_id falls back to the entry-level request_id for
assistant entries when message.id is missing (Anthropic's request ID
identifies one API request per assistant message; user entries never
use this fallback).

toolpath-claude 0.12.0 -> 0.12.1.
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

🔍 Preview deployed: https://51cab5d6.toolpath.pages.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant