diff --git a/src/format.ts b/src/format.ts index 3905048..ee44619 100644 --- a/src/format.ts +++ b/src/format.ts @@ -34,12 +34,15 @@ export function renderStatusBar(projects: ProjectSummary[]): string { for (const project of projects) { for (const session of project.sessions) { for (const turn of session.turns) { - if (!turn.timestamp) continue - // Bucket by the session timestamp's local date so the user's "today" and "this month" - // match the wall clock on their machine. Session timestamps are stored as UTC ISO - // strings; naively slicing `timestamp.slice(0,10)` bucketed them by UTC date, which - // showed `Today $0` during the UTC-midnight-to-local-midnight window. - const day = localDateString(new Date(turn.timestamp)) + if (turn.assistantCalls.length === 0) continue + // Bucket by the first assistant call's local date -- the moment the cost was + // incurred. Bucketing by `turn.timestamp` (the user message time) drops turns + // that straddle midnight (user asked at 23:58, response arrived at 00:30) and + // disagrees with parseAllSessions' dateRange filter which is also on assistant + // time. + const bucketTs = turn.assistantCalls[0]!.timestamp + if (!bucketTs) continue + const day = localDateString(new Date(bucketTs)) const turnCost = turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0) const turnCalls = turn.assistantCalls.length if (day === today) { todayCost += turnCost; todayCalls += turnCalls } diff --git a/src/parser.ts b/src/parser.ts index ba0d31c..6bc81a7 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -287,18 +287,23 @@ async function parseSessionFile( if (entries.length === 0) return null - let filteredEntries = entries + const sessionId = basename(filePath, '.jsonl') + let turns = groupIntoTurns(entries, seenMsgIds) if (dateRange) { - filteredEntries = entries.filter(e => { - if (!e.timestamp) return e.type === 'user' - const ts = new Date(e.timestamp) + // Bucket a turn by the timestamp of its first assistant call (when the cost was + // actually incurred). Filtering entries directly produced orphan assistant calls + // when a user message sat in one day and the response landed in another -- those + // got pushed as turns with empty timestamps, which some code paths counted and + // others dropped, producing inconsistent Today totals. + turns = turns.filter(turn => { + if (turn.assistantCalls.length === 0) return false + const firstCallTs = turn.assistantCalls[0]!.timestamp + if (!firstCallTs) return false + const ts = new Date(firstCallTs) return ts >= dateRange.start && ts <= dateRange.end }) - if (filteredEntries.length === 0) return null + if (turns.length === 0) return null } - - const sessionId = basename(filePath, '.jsonl') - const turns = groupIntoTurns(filteredEntries, seenMsgIds) const classified = turns.map(classifyTurn) return buildSessionSummary(sessionId, project, classified)