Skip to content

claude session ignores --since/--until in totals (codex session does not) #1609

Description

@jeffrey0070

Summary

ccusage claude session accepts --since / --until but the date window never
reaches the numbers. The flags only decide whether a session row is shown at
all; every row still reports the session's lifetime totals.

ccusage codex session scopes the same flags correctly, so the two providers
disagree under identical arguments.

This is silent and misleading: the row appears under a date filter, so the
figure reads as "usage in this window" when it is actually "usage since the
session was created". For a long-lived session the two differ by an order of
magnitude.

Three defects fall out of one root cause (details below).

Environment

  • ccusage 20.0.20 (npx ccusage@latest)
  • Windows 11, PowerShell 7.6.5
  • Source inspected at 49ad744ccb0a1a1b167a2b6dfa6bbcc9ffe9423f

Defect A — claude session totals ignore the date window

Session ef018fb9, firstActivity 2026-08-04, lastActivity 2026-08-14.
Only --since varies; --timezone UTC throughout:

--since totalCost totalTokens
2026-01-01 1210.75 1,157,847,398
2026-08-13 1210.75 1,157,847,398
2026-08-14 1210.75 1,157,847,398
(no filter) 1210.75 1,157,847,398

--since 2026-08-14 excludes ten days of activity, yet nothing moves.

Not session-specific. Session 8fe7ee09, firstActivity 2026-08-12,
lastActivity 2026-08-14:

--since totalCost totalTokens
2026-01-01 620.11 780,678,594
2026-08-13 620.11 780,678,594
2026-08-14 620.11 780,678,594
(no filter) 620.11 780,678,594

The same test against codex session (session 019ee83f) scopes correctly:

--since costUSD totalTokens
(no filter) 401.15 391,238,184
2026-08-01 95.45 93,425,326
2026-08-15 12.29 11,827,686

Defect B — --until is off by one day

Same session, lastActivity = 2026-08-14T09:38:56.467Z, --timezone UTC:

flags result
--until 2026-08-13 absent
--until 2026-08-14 absent
--until 2026-08-15 present, 1210.75
--since 2026-08-14 present, 1210.75
--since 2026-08-15 absent

--until is documented as inclusive, so --until 2026-08-14 should keep a
session whose last activity is on 2026-08-14. The --since boundary is
correct; only --until is wrong.

Same class as #1390, which was fixed for the pi provider.

Defect C — --timezone does not apply to the session filter

The session filter compares against a UTC timestamp regardless of
--timezone, so the flag has no effect on which sessions are selected.

Root cause

All three follow from one filter block.

rust/crates/ccusage/src/commands/mod.rs:172-188:

if session_shared.since.is_some() || session_shared.until.is_some() {
    rows.retain(|row| {
        let date = row
            .last_activity
            .as_deref()
            .unwrap_or_default()
            .replace('-', "");
        session_shared.since.as_ref().is_none_or(|since| &date >= since)
            && session_shared.until.as_ref().is_none_or(|until| &date <= until)
    });
}

This is the same shape as the daily filter in
rust/crates/ccusage-adapter-all/src/loader.rs:614-623:

fn filter_daily_summaries_by_date(rows: &mut Vec<UsageSummary>, shared: &SharedArgs) {
    if shared.since.is_none() && shared.until.is_none() { return; }
    rows.retain(|row| {
        let date = row.date.as_deref().unwrap_or_default().replace('-', "");
        shared.since.as_ref().is_none_or(|since| &date >= since)
            && shared.until.as_ref().is_none_or(|until| &date <= until)
    });
}

The only difference is the field read: row.date versus row.last_activity.
That single substitution breaks three assumptions at once.

A — wrong layer. In the daily report a row is a day, so retaining rows is
equivalent to filtering by date. In the session report a row is an entire
session, so retaining rows can only include or exclude a session whole. The
filter runs at commands/mod.rs:172, after aggregation completes at lines
168-171, and load_entries at line 152 applies no date bound — the Claude
adapter contains no reference to since or until anywhere
(rust/adapters/claude/src/). By the time the filter runs, the lifetime totals
are already computed.

B — wrong granularity. row.date is a plain YYYY-MM-DD, so
.replace('-', "") yields 20260814, which compares correctly against the
normalized bound. row.last_activity is a full RFC3339 timestamp
(ccusage-core/src/summary.rs:156 → format_rfc3339_millis), so the same
.replace yields 20260814T09:38:56.467Z. String-compared against 20260814:

"20260814T09:38:56.467Z" <= "20260814"   ->  false   (dropped)
"20260814T09:38:56.467Z" <= "20260815"   ->  true    (kept)

The trailing time component makes the value always greater than the bare date
for the same day, so --until on the boundary day always fails. The >= side
is unaffected, which is why only --until is off.

C — wrong timezone. format_rfc3339_millis
(ccusage-core/src/date_utils.rs:344) starts with timestamp.utc_parts() and
is UTC-only, whereas the daily path formats row.date through format_date
with shared.timezone. So the session filter silently ignores --timezone.

For contrast, the Codex adapter filters during parsing and aggregation
(rust/adapters/codex/src/aggregate.rs, parser.rs) rather than after, which
is why codex session scopes correctly.

Suggested fix

Push the date window down to entry loading / aggregation for the Claude path,
matching the Codex adapter. That fixes A, B and C together.

If a narrower fix is preferred, B and C can be addressed on their own by
truncating last_activity to a date and formatting it with shared.timezone
before comparing — but A would remain, and A is the one that produces wrong
numbers rather than a wrong row set.

Happy to test a patch.

Activity

  1. pullfrog commented on Aug 16, 2026

    @pullfrog
    Contributor

    Issue #1609 is actionable and reproducible from the current code. The focused Claude path in rust/crates/ccusage/src/commands/mod.rs loads and aggregates every entry, then filters completed rows using last_activity as an RFC3339 string; this cannot scope totals, rejects the inclusive --until boundary day, and ignores the requested timezone. The unified Claude path in rust/crates/ccusage-adapter-all/src/loader.rs has the same post-aggregation behavior.

    Plan

    1. Change focused Claude session processing in rust/crates/ccusage/src/commands/mod.rs to apply the existing ccusage_adapter_common::filter_loaded_entries_by_date helper to loaded entries before session grouping. Remove the row-level last_activity date comparison. Apply the same entry filtering in the --id lookup path so focused session detail obeys the date window as well.
    2. Change the unified Claude session branch in rust/crates/ccusage-adapter-all/src/loader.rs to filter Claude LoadedEntry values before summarize_entry_sessions. Keep the separate behavior for other sources that use the shared session-summary helper unless their semantics are explicitly changed by a follow-up.
    3. Add fixture-backed regression coverage for a Claude session spanning multiple dates. Assert that --since and --until change token and cost totals, both boundary dates are inclusive, entries outside the window are excluded, and a non-UTC timezone determines the entry date. Cover both focused Claude session output and unified session rows so the two command surfaces stay consistent.
    4. Add coverage for focused --id lookup with a date window, including the no-in-range-entries result, and preserve deduplication, model breakdowns, session identity, and lifetime behavior when no bounds are supplied.
    5. Audit docs/guide/session-reports.md and the Claude session guide wording. Update the documented semantics to explain that date bounds select usage entries and scope session totals, then run the focused Rust tests plus the repository’s standard format, lint, and check recipes.

    Acceptance criteria

    • ccusage claude session --since/--until reports totals calculated only from entries whose localized date is inside the inclusive window.
    • --until YYYYMMDD includes entries on YYYYMMDD, including the last activity boundary.
    • --timezone affects date inclusion consistently with daily reports.
    • ccusage session produces the same Claude session totals for the same window.
    • Unbounded session reports remain unchanged, and Codex session behavior remains unchanged.

    No implementation or tests were started in this planning run.

    Task list (3/3 completed)

    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 16, 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: priority:high. This is a clear, reproducible bug in core usage totals: bounded Claude session reports can show lifetime cost and tokens, which is misleading for budgeting. The issue is unresolved on main.

    I prepared commit c7c305e with the focused fix, regression tests, and documentation updates. The affected tests, clippy, and formatting checks pass, but the PR could not be opened because git push was denied with HTTP 403 for github-actions[bot]; the PR attempt then failed because the branch was not available remotely.

    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, reproducible, repository-scoped bug with a detailed root cause, expected behavior, and regression coverage scope. The issue remains unresolved, and the existing discussion indicates a safe focused fix was prepared but not published.

    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