mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-20 00:57:09 +00:00
A turn that straddles midnight (user typed at 23:58, assistant responded at 00:30) was bucketed and filtered inconsistently across call sites. parseSessionFile filtered entries by timestamp, producing orphan assistant calls that groupIntoTurns pushed as turns with empty timestamp. Some downstream code counted those (buildPeriodData summing project totals) and other code dropped them (renderStatusBar's empty-timestamp skip). The menubar showed today = $32 while the terminal status showed today = $27 for the same dataset; each was internally consistent but used a different turn-bucket rule. Fix both: parseSessionFile now builds all turns first, then filters each turn by its first assistant call timestamp (the moment cost was incurred). renderStatusBar buckets the same way. day-aggregator.ts already bucketed on assistant time, so it is now consistent too. Net effect: a turn is counted in the day the API call actually ran in.
59 lines
2.6 KiB
TypeScript
59 lines
2.6 KiB
TypeScript
import chalk from 'chalk'
|
|
import type { ProjectSummary } from './types.js'
|
|
|
|
// Re-exported from currency.ts so existing imports from './format.js' keep working.
|
|
// The currency-aware version applies exchange rate and symbol automatically.
|
|
// Imported locally too since renderStatusBar below uses it directly.
|
|
import { formatCost } from './currency.js'
|
|
export { formatCost }
|
|
|
|
export function formatTokens(n: number): string {
|
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
|
|
return n.toString()
|
|
}
|
|
|
|
/// Returns YYYY-MM-DD for the given date in the process-local timezone. Cheaper than shelling
|
|
/// out to Intl.DateTimeFormat for every turn in a loop and avoids the UTC drift that bites
|
|
/// `Date.toISOString().slice(0,10)` whenever the user runs this between local midnight and
|
|
/// UTC midnight.
|
|
function localDateString(d: Date): string {
|
|
const y = d.getFullYear()
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${day}`
|
|
}
|
|
|
|
export function renderStatusBar(projects: ProjectSummary[]): string {
|
|
const now = new Date()
|
|
const today = localDateString(now)
|
|
const monthStart = `${today.slice(0, 7)}-01`
|
|
|
|
let todayCost = 0, todayCalls = 0, monthCost = 0, monthCalls = 0
|
|
|
|
for (const project of projects) {
|
|
for (const session of project.sessions) {
|
|
for (const turn of session.turns) {
|
|
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 }
|
|
if (day >= monthStart) { monthCost += turnCost; monthCalls += turnCalls }
|
|
}
|
|
}
|
|
}
|
|
|
|
const lines: string[] = ['']
|
|
lines.push(` ${chalk.bold('Today')} ${chalk.yellowBright(formatCost(todayCost))} ${chalk.dim(`${todayCalls} calls`)} ${chalk.bold('Month')} ${chalk.yellowBright(formatCost(monthCost))} ${chalk.dim(`${monthCalls} calls`)}`)
|
|
lines.push('')
|
|
|
|
return lines.join('\n')
|
|
}
|