Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
## Unreleased

### Added
- **Credit-metered ChatGPT workspaces (Business / Edu / Enterprise) now show their limit.** These plans report no rate-limit windows, so the admin-set monthly allowance from `spend_control.individual_limit` is shown as a "Monthly usage limit" bar in the desktop app and the menubar.
- **Credit-metered ChatGPT workspaces (Business / Edu / Enterprise) now show their limit.** These plans report no rate-limit windows, so the admin-set monthly allowance from `spend_control.individual_limit` is shown as a "Monthly usage limit" bar in the desktop app and the menubar. (#833)
- **Combined-device scope in the desktop Dashboard**, mirroring the menu bar. A Local / Combined toggle aggregates paired-device usage in the Overview hero and the menu bar badge, degrading gracefully to the local figure when a peer is unreachable; the badge then shows a dimmed `reachable/total` marker so a momentary drop to the local number reads as "a peer is unreachable" rather than a glitch. (#866, #867, thanks @marcreynolds)

### Added (CLI)
- **Codex throughput tracking**: per-model Tok/s in the dashboard and report, active time excludes tool wait. (#805, thanks @ihearttokyo)
Expand All @@ -13,6 +14,11 @@
- **`--project` / `--exclude` now apply to the headline totals, not just the detail panels.** The durable headline unions the carry-forward daily cache with today's live parse, and the cached days were sliced to the requested provider but never to the requested project — so the Overview panel counted excluded projects while By Project / By Activity / By Model (built from the name-filtered parse) left them out, and the two could not be reconciled. Cost, calls, sessions and savings are now sliced out of the per-project day stats the cache has carried since v15. Tokens, models and categories have no per-project split in the cache, so under a project filter they come from the (project-filtered) live parse instead; cached days — or provider slices — carried from before v15 have no project split at all, so they cannot be attributed to a filtered project, and the terminal overview now states how much was set aside rather than folding it into the total. (#864)
- **Codex parser corrections**: fork-replay no longer double-counts `patch_apply_end` and `mcp_tool_call_end`; `exec` is normalized to Bash; `custom_tool_call` events are handled; token_count lines larger than 32 KiB now parse exact token counts instead of estimating. Codex session cache bumps from v7 to v8 for a one-time re-parse. Only tool attribution changes for ordinary sessions, leaving their cost identical; sessions that logged an oversized token_count line are repriced from exact counts instead of an estimate. (#805)

- **Midnight-straddling turns keep both halves.** A turn whose calls span local midnight was attributed whole to its start day, so `codeburn today` under-reported until the turn ended and multi-day totals mis-split it. Calls are now range-filtered inside the turn so each day gets the calls that belong to it, and By Activity and the daily turn counts reconcile with the headline. (#853, thanks @KENSHI601)
- **`--provider <x>` no longer leaks Claude spend into the detail panels.** A provider-filtered run still ran the Claude scan, whose orphan pass re-injected every cached Claude session, so By Project / By Model / By Activity showed Claude usage under, e.g., `--provider cursor` while the headline was correct. (#872, thanks @ozymandiashh)
- **A degraded session parse no longer freezes daily history.** A read-only parse that served a stale or missing session file was treated as complete and finalized days it never covered, freezing warm-cache ingestion; a corrupt refresh lock is now recovered rather than ending ingestion, and a legitimately idle tail is no longer re-derived on every launch. (#856, thanks @avs-io)
- **Pi / Oh My Pi transcripts with a leading title record are discovered.** OMP writes a `type: "title"` line before the session header; discovery now scans a bounded number of leading lines for the first session record instead of requiring it on the first physical line. (#846, #859, thanks @jbspeakr, @avs-io)

### Fixed
- Claude Desktop and Cowork sessions are discovered for Windows Microsoft Store (MSIX) installs. (#611)

Expand Down
9 changes: 7 additions & 2 deletions src/usage-aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,14 @@ export async function buildDurablePeriod(periodInfo: PeriodInfo, opts: Aggregate
liveProjects = daysSelection ? filterProjectsByDays(raw, daysSelection.days) : raw
scanRange = periodInfo.range
// A period that reaches today contains today's turns already, so derive the
// today slice from the same parse instead of scanning today again.
// today slice from the same parse instead of scanning today again. Slice it
// to today first (filterProjectsByDays re-anchors a midnight-straddling
// turn to its surviving today calls), so today's category / turn count
// lands on today rather than staying anchored on the turn's yesterday
// start. Otherwise the post-midnight half vanishes from By Activity and the
// JSON daily turn count while the per-call cost/calls still bucket to today.
todayAllDays = rangeEndStr >= todayStr
? aggregateProjectsIntoDays(raw).filter(d => d.date === todayStr)
? aggregateProjectsIntoDays(filterProjectsByDays(raw, new Set([todayStr]))).filter(d => d.date === todayStr)
: aggregateProjectsIntoDays(fp(await parseAllSessions(todayRange, 'all'))).filter(d => d.date === todayStr)
}
} else {
Expand Down
30 changes: 30 additions & 0 deletions tests/cli-durable-totals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,36 @@ describe('midnight-straddling turn conservation (issue #852)', () => {
}
}, 60_000)

it('reconciles By Activity (categories) and daily turns with the headline on the multi-day all-provider view', async () => {
fakeNow()
try {
await seedStraddlingClaudeTurn()
const [costN, costN1] = await truthCosts('claude')

const range: DateRange = { start: new Date(2026, 6, 27, 0, 0, 0), end: new Date() }
clearSessionCache()
const durable = await buildDurablePeriod({ range, label: '2d' }, { provider: 'all' })

// By Activity must sum to the whole-range headline. Before this fix the
// today slice came from the unsliced whole-range parse, so the straddling
// turn's category stayed anchored on its yesterday start and the
// post-midnight half vanished from By Activity while cost / calls / By
// Model were already correct; categories summed to only the pre-midnight
// cost.
const categoryTotal = durable.data.categories.reduce((s, c) => s + c.cost, 0)
expect(categoryTotal).toBeCloseTo(costN + costN1, 8)
expect(categoryTotal).toBeCloseTo(durable.data.cost, 8)

// Today's day entry carries its turn's category, not calls>0 with an empty
// category map (which rendered as `turns: 0` in the JSON daily rows).
const dayN1 = durable.days.find(d => d.date === DAY_N1)
expect(dayN1?.calls).toBe(1)
expect(Object.keys(dayN1?.categories ?? {}).length).toBeGreaterThan(0)
} finally {
vi.useRealTimers()
}
}, 60_000)

it('shows the post-midnight call in the today-only view on the Claude Code path', async () => {
fakeNow()
try {
Expand Down