mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-04 13:51:50 +00:00
fix(aggregator): attribute a straddling turn's category to today on the all-provider view
buildDurablePeriod derived the today slice of the multi-day, all-provider headline from the unsliced whole-range parse, so a turn spanning local midnight kept its category and turn count anchored on its yesterday start. The per-call cost and calls bucketed onto today correctly, but By Activity and the JSON daily turn count lost the post-midnight half — categories summed to only the pre-midnight cost while the headline, By Model and By Project were right. Slice the today parse with filterProjectsByDays first, which re-anchors the straddling turn to its surviving today calls, so today's category cost lands on today. Category cost is the sum of the slice's own calls, so day-N + day-N+1 still equals the whole-range total (no over-count); the per-day turn-count split matches the cache side and the documented per-day semantics. Adds a regression test in the straddling-turn conservation suite (mutation-checked: fails on the pre-fix code). Also fills in the CHANGELOG Unreleased entries for the batch (#853, #856, #872, #846/#859, #866/#867, #833).
This commit is contained in:
parent
55bf65ef6a
commit
dbdbf466a0
3 changed files with 44 additions and 3 deletions
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue