Skip to content

Usage after local midnight vanishes when a turn spans days (turn-level range filter drops post-midnight calls) #852

Description

@KENSHI601

Summary: In parseProviderSources (src/parser.ts), turns are range-filtered by turn.calls[0]?.timestamp. For long-running sessions whose turn spans midnight (common with autonomous Codex runs lasting many hours), all calls after midnight are dropped from the new day — today shows nothing until the turn ends, and daily history attributes the whole turn's cost to the turn-start day.

Repro (synthetic rollout):

{"timestamp":"2026-07-28T15:55:00.000Z","type":"session_meta","payload":{"session_id":"x","originator":"codex_cli_rs","cwd":"/tmp/t","model":"gpt-5.5"}}
{"timestamp":"2026-07-28T15:57:00.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"work through midnight"}]}}
{"timestamp":"2026-07-28T15:58:00.000Z","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":100000,"cached_input_tokens":50000,"cache_write_input_tokens":0,"output_tokens":2000,"reasoning_output_tokens":0,"total_tokens":102000},"last_token_usage":{"input_tokens":100000,"cached_input_tokens":50000,"cache_write_input_tokens":0,"output_tokens":2000,"reasoning_output_tokens":0,"total_tokens":102000}}}}
{"timestamp":"2026-07-28T16:10:00.000Z","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":200000,"cached_input_tokens":100000,"cache_write_input_tokens":0,"output_tokens":4000,"reasoning_output_tokens":0,"total_tokens":204000},"last_token_usage":{"input_tokens":100000,"cached_input_tokens":50000,"cache_write_input_tokens":0,"output_tokens":2000,"reasoning_output_tokens":0,"total_tokens":102000}}}}

In timezone UTC+8: at local 2026-07-29 00:30, codeburn today reports "No usage data found", although the second token_count (2026-07-29 00:10 local) is squarely inside today. export --from 2026-07-29 likewise returns 0 calls. Same-day control case (user message repeated before the second event) works fine, which isolates the cause to turn-level filtering.

Real-world case: a Codex session running autonomously for 30+ hours (one turn, last user message at 23:57 local) — all post-midnight usage (~150 calls) vanished from today.

Suggested fix: filter calls inside the turn instead of dropping the whole turn:

if (dateRange) {
  const inRangeCalls = turn.calls.filter((c) => {
    const ts = new Date(c.timestamp);
    return !Number.isNaN(ts.getTime()) && ts >= dateRange.start && ts <= dateRange.end;
  });
  if (inRangeCalls.length === 0) continue;
  turn = { ...turn, calls: inRangeCalls };
}

(applies to both the servedSources loop and the durableSources loop in parseProviderSources)

Optionally, in aggregateProjectsIntoDays, attribute per-call cost/token columns to dateKey(call.timestamp) rather than the turn day, so multi-day turns distribute cost across actual days in daily history. Turn-level stats (categories, one-shot) can stay turn-based.

I've verified both changes locally against the repro and a real 51MB rollout: post-midnight usage shows up in today correctly, monthly totals unchanged.

Happy to open a PR with this if you prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions