Skip to content

Claude usage collapses to one entry when sessions share a message id (gateway setups) #1635

Closed

Description

@Fluffy-Bunny-23

I run Claude Code behind a local relay that answers every response with the same message id ("ocgo") and no request id. Usage reports were wrong in a big way: daily and monthly views showed 750K tokens of claude usage while my raw session logs summed to 1.21B across three days.

Cause: push_deduped_entry (lib.rs) and push_deduped_daily_entry (daily.rs) key entries on (message_id, request_id) alone. When those two are identical for every record, which is the normal case behind an LLM gateway, each record after the first scanned one matches the exact hash and gets dropped unless its total is strictly larger. One survivor per relay, everything else gone.

Minimal repro: two jsonl session files where assistant records share message.id and have no requestId. Scanned together, only one file's usage survives. Scanned alone, each reports correctly. I can put this up as a fixture-based test.

Fix that worked for me: add session_id and timestamp to the exact-match hash and matcher. True double-writes still collapse. Claude Code does write some (same session, same timestamp, same usage). Distinct sessions keep their own rows. On my data this took claude all-time from 750K to 1.21B tokens, which now matches an independent sum over the raw logs within 0.03%.

Related: #1459 did the same thing for codex ("scope event dedupe to sessions"), and #1155 introduced this dedup for the sidechain replay case. Fork-copied transcripts keep collapsing under the scoped key because their internal session ids and timestamps still match - verified against two identical copies at different paths before writing this.

Happy to send the patch as a PR with regression tests once approved here.

About how this report was produced

The initial investigation and draft were written by an AI coding agent: pi coding agent running ox-alpha-free via OpenCode Go. It found the bug by bisecting my own session logs against ccusage's output, verified the fix on about 240 real session files, and drafted this text. A human reviewed the findings and this report before posting.

Activity

  1. pullfrog commented on Aug 24, 2026

    @pullfrog
    Contributor

    Assessment

    This issue is actionable. The reported failure is confirmed in the current Rust implementation: rust/adapters/claude/src/lib.rs:147-209 and daily.rs:401-440 use only (message_id, request_id) for the exact lookup, so a gateway that reuses those values can discard usage from independent sessions. The regular loader already has the parsed timestamp and path-derived session context, while the daily loader currently parses both timestamp and sessionId but retains only the date. The unified Claude loader delegates to these two paths (rust/crates/ccusage-adapter-all/src/loader.rs:593-610), so no separate unified dedupe implementation is needed.

    One compatibility detail needs to be handled deliberately: the existing keeps_most_complete_duplicate_usage_entry test relies on two records with the same message/request IDs but different timestamps collapsing to the more complete record. The safest scope is to always include session provenance, and use timestamp as part of the identity for records without a request ID, which fixes gateway records without changing established request-ID replacement behavior.

    Implementation Plan

    1. Define a Claude dedupe identity that includes the record’s effective session identity and parsed TimestampMs when requestId is absent. Prefer the serialized record sessionId so fork-copied transcripts with the same internal identity still collapse; fall back to the path-derived session ID when the field is missing so unrelated files do not collide.
    2. Update push_deduped_entry and push_deduped_daily_entry, including their hash indexes and equality matchers. Apply the session and timestamp scope to the sidechain replay lookup as well, preserving the existing parent-versus-sidechain preference and higher-usage replacement rules.
    3. Carry the required identity through the daily loader. Preserve the effective session ID for direct and AgentProgress records, retain the parsed timestamp on DailyLoadedEntry, and update its unit-test fixture helpers accordingly.
    4. Add fixture-backed regression coverage for two independent session files sharing a message ID with no request ID. Assert that focused entry loading keeps both records and that daily aggregation sums both; also verify exact duplicates in the same session and timestamp still collapse.
    5. Retain coverage for sidechain replay with a different request ID, copied transcripts with matching internal session/timestamp identity, and request-ID duplicates where the more complete record wins. Update the existing duplicate fixture if needed to make the intended timestamp distinction explicit.
    6. Document the session-scoped gateway behavior in rust/adapters/claude/src/README.md, then run the focused Claude/ccusage Rust tests and the relevant workspace checks.

    No implementation has been started.

    Task list (3/3 completed)
    • Read repository guidance and issue context
    • Trace Claude message/session aggregation and relevant tests
    • Assess actionability and publish an implementation plan

    Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Implement plan ➔ | View workflow run | via Pullfrog | Using GPT Luna (free via Pullfrog for OSS) | 𝕏

  2. github-actions commented on Aug 24, 2026

    @github-actions
    Contributor

    This issue was auto-closed. Issues from new contributors are auto-closed by default.

    Maintainers review auto-closed issues and reopen worthwhile ones. Issues that do not meet the quality bar in CONTRIBUTING.md may not be reopened or receive a reply.

    Keep the issue short, concrete, and written in your own voice.

    If a maintainer replies lgtmi, your future issues will stay open. If a maintainer replies lgtm, your future issues and PRs will stay open.

    See CONTRIBUTING.md.

  3. ryoppippi commented on Aug 27, 2026

    @ryoppippi
    Member

    @pullfrog Triage this issue using the repository standing issue-triage instructions. Inspect the full issue body, comments, current main branch, and related issues or pull requests. Choose exactly one priority label. If the issue is clearly unresolved and is priority:critical or priority:high with safe and well-scoped requirements, implement it and open a focused pull request. Otherwise, only triage it. Keep the explanation short and use simple English.

  4. pullfrog commented on Aug 27, 2026

    @pullfrog
    Contributor

    Priority: high. This is a real usage undercount bug: Claude records from independent sessions can collapse when a gateway reuses message.id and omits requestId. Current main still has the affected deduplication, and related Codex work does not fix Claude. I prepared a focused fix with regression tests, but could not open the PR because pushing the branch failed: remote: Permission to ccusage/ccusage.git denied to github-actions[bot].

    Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | View workflow run | via Pullfrog | Using GPT Luna (free via Pullfrog for OSS) | 𝕏

  5. github-actions commented on Aug 28, 2026

    @github-actions
    Contributor

    Pullfrog triage: priority:high

    A maintainer explicitly requested an implementation attempt. Clear, actionable, repository-scoped Claude undercount bug with a concrete reproduction, identified deduplication cause, proposed safe fix, and regression-test plan. Related Codex and Claude deduplication work confirms the scope; it is not a duplicate.

    Decision: kept open; Pullfrog will attempt a focused implementation PR.

  6. ryoppippi commented on Aug 31, 2026

    @ryoppippi
    Member

    Historical audit: this discussion was auto-closed by the legacy contributor gate. That closure did not assess technical importance.

    Audit result: resolved. A later merged change or the current main implementation covers this request. This item is kept for history and does not need to be reopened.

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 workingpriority:highHigh user impact or an important regression.triage:resolvedResolved by a later change or current implementation.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions