Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0f55a44

Browse files
authored
Fix per-provider data loss, history regression, and decode fragility (getagentseal#362)
* Fix per-provider data loss, division-by-zero, and decode fragility - Per-provider multi-day queries only merged cost/calls from cache, dropping categories/models/sessions/tokens. Remove broken cache shortcut and always do full parse for per-provider periods. - Remove per-provider daily history double-counting from overlapping cache + live data. - Guard maxCost against zero in ActivitySection and ModelsSection to prevent NaN in bar width calculations. - Use offset-based ForEach ID in BarTooltipCard to avoid duplicate model name collisions. - Make cacheHitPercent, topActivities, topModels, providers use decodeIfPresent for backward compat with older CLI versions. - Skip currency switch when FX rate fetch fails with no cache, preventing rate/symbol desync. - Use readSessionFile in Gemini parser for 128MB size cap. - Truncate Codex userMessage to 500 chars like other providers. * Restore cache-backed trend history for provider-filtered views The previous commit removed the broken per-provider cache shortcut but also dropped cache-backed daily history, causing provider-filtered views to lose trend data outside the selected period range. Use allCacheDays for historical days (cost/calls per provider is accurate in cache) and today's entry from the full parse. No overlap since cache ends at yesterday.
1 parent 3542407 commit 0f55a44

8 files changed

Lines changed: 39 additions & 89 deletions

File tree

mac/Sources/CodeBurnMenubar/Data/MenubarPayload.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ extension CurrentBlock {
122122
oneShotRate = try c.decodeIfPresent(Double.self, forKey: .oneShotRate)
123123
inputTokens = try c.decode(Int.self, forKey: .inputTokens)
124124
outputTokens = try c.decode(Int.self, forKey: .outputTokens)
125-
cacheHitPercent = try c.decode(Double.self, forKey: .cacheHitPercent)
126-
topActivities = try c.decode([ActivityEntry].self, forKey: .topActivities)
127-
topModels = try c.decode([ModelEntry].self, forKey: .topModels)
128-
providers = try c.decode([String: Double].self, forKey: .providers)
125+
cacheHitPercent = try c.decodeIfPresent(Double.self, forKey: .cacheHitPercent) ?? 0
126+
topActivities = try c.decodeIfPresent([ActivityEntry].self, forKey: .topActivities) ?? []
127+
topModels = try c.decodeIfPresent([ModelEntry].self, forKey: .topModels) ?? []
128+
providers = try c.decodeIfPresent([String: Double].self, forKey: .providers) ?? [:]
129129
topProjects = try c.decodeIfPresent([ProjectEntry].self, forKey: .topProjects) ?? []
130130
modelEfficiency = try c.decodeIfPresent([ModelEfficiencyEntry].self, forKey: .modelEfficiency) ?? []
131131
topSessions = try c.decodeIfPresent([TopSessionEntry].self, forKey: .topSessions) ?? []

mac/Sources/CodeBurnMenubar/Views/ActivitySection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ struct ActivitySection: View {
2020
}
2121
) {
2222
VStack(alignment: .leading, spacing: 7) {
23-
let maxCost = store.payload.current.topActivities.map(\.cost).max() ?? 1
23+
let maxCost = max(store.payload.current.topActivities.map(\.cost).max() ?? 1, 0.01)
2424
ForEach(store.payload.current.topActivities, id: \.name) { activity in
2525
ActivityRow(activity: activity, maxCost: maxCost)
2626
}

mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ private struct BarTooltipCard: View {
373373

374374
if !bar.topModels.isEmpty {
375375
VStack(alignment: .leading, spacing: 3) {
376-
ForEach(Array(bar.topModels.prefix(4).enumerated()), id: \.element.name) { idx, m in
376+
ForEach(Array(bar.topModels.prefix(4).enumerated()), id: \.offset) { idx, m in
377377
HStack(spacing: 6) {
378378
RoundedRectangle(cornerRadius: 1)
379379
.fill(Theme.brandAccent.opacity(0.75 - Double(idx) * 0.12))

mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -653,8 +653,10 @@ struct FooterBar: View {
653653
}
654654

655655
let fresh = await FXRateCache.shared.rate(for: code)
656-
store.currency = code
657-
CurrencyState.shared.apply(code: code, rate: fresh ?? cached, symbol: symbol)
656+
if let rate = fresh ?? cached {
657+
store.currency = code
658+
CurrencyState.shared.apply(code: code, rate: rate, symbol: symbol)
659+
}
658660
}
659661

660662
CLICurrencyConfig.persist(code: code)

mac/Sources/CodeBurnMenubar/Views/ModelsSection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct ModelsSection: View {
1919
}
2020
) {
2121
VStack(alignment: .leading, spacing: 7) {
22-
let maxCost = store.payload.current.topModels.map(\.cost).max() ?? 1
22+
let maxCost = max(store.payload.current.topModels.map(\.cost).max() ?? 1, 0.01)
2323
ForEach(store.payload.current.topModels, id: \.name) { model in
2424
ModelRow(model: model, maxCost: maxCost)
2525
}

src/main.ts

Lines changed: 23 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,6 @@ program
467467
let scanRange: DateRange
468468
let cache: DailyCache
469469
let todayProviderData: PeriodData | null = null
470-
let usedPerProviderCachePath = false
471470

472471
if (isAllProviders) {
473472
cache = await hydrateCache()
@@ -487,32 +486,11 @@ program
487486
}
488487
} else {
489488
cache = await loadDailyCache()
490-
const cacheIsCurrent = cache.lastComputedDate !== null
491-
&& cache.lastComputedDate >= yesterdayStr
492-
if (cacheIsCurrent && rangeStartStr < todayStr) {
493-
const todayProviderProjects = fp(await parseAllSessions(todayRange, pf))
494-
todayProviderData = buildPeriodData(periodInfo.label, todayProviderProjects)
495-
const historicalDays = getDaysInRange(cache, rangeStartStr, yesterdayStr)
496-
let histCost = 0, histCalls = 0
497-
for (const d of historicalDays) {
498-
const prov = d.providers[pf]
499-
if (prov) { histCost += prov.cost; histCalls += prov.calls }
500-
}
501-
currentData = {
502-
...todayProviderData,
503-
cost: todayProviderData.cost + histCost,
504-
calls: todayProviderData.calls + histCalls,
505-
}
506-
scanProjects = todayProviderProjects
507-
scanRange = todayRange
508-
usedPerProviderCachePath = true
509-
} else {
510-
const fullProjects = fp(await parseAllSessions(periodInfo.range, pf))
511-
todayProviderData = buildPeriodData(periodInfo.label, fullProjects)
512-
currentData = todayProviderData
513-
scanProjects = fullProjects
514-
scanRange = periodInfo.range
515-
}
489+
const fullProjects = fp(await parseAllSessions(periodInfo.range, pf))
490+
todayProviderData = buildPeriodData(periodInfo.label, fullProjects)
491+
currentData = todayProviderData
492+
scanProjects = fullProjects
493+
scanRange = periodInfo.range
516494
}
517495

518496
// PROVIDERS
@@ -579,7 +557,8 @@ program
579557
topModels,
580558
}
581559
})
582-
} else if (usedPerProviderCachePath) {
560+
} else {
561+
const emptyModels = [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[]
583562
const historyFromCache = allCacheDays.map(d => {
584563
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
585564
return {
@@ -590,53 +569,25 @@ program
590569
outputTokens: 0,
591570
cacheReadTokens: 0,
592571
cacheWriteTokens: 0,
593-
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
572+
topModels: emptyModels,
594573
}
595574
})
596-
const todayCost = todayProviderData!.cost
597-
const todayCalls = todayProviderData!.calls
598-
if (todayCost > 0 || todayCalls > 0) {
599-
historyFromCache.push({
600-
date: todayStr,
601-
cost: todayCost,
602-
calls: todayCalls,
603-
inputTokens: 0,
604-
outputTokens: 0,
605-
cacheReadTokens: 0,
606-
cacheWriteTokens: 0,
607-
topModels: [],
575+
const todayFromParse = aggregateProjectsIntoDays(scanProjects)
576+
.filter(d => d.date === todayStr)
577+
.map(d => {
578+
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
579+
return {
580+
date: d.date,
581+
cost: prov.cost,
582+
calls: prov.calls,
583+
inputTokens: 0,
584+
outputTokens: 0,
585+
cacheReadTokens: 0,
586+
cacheWriteTokens: 0,
587+
topModels: emptyModels,
588+
}
608589
})
609-
}
610-
dailyHistory = historyFromCache
611-
} else {
612-
const histFromCache = allCacheDays.map(d => {
613-
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
614-
return {
615-
date: d.date,
616-
cost: prov.cost,
617-
calls: prov.calls,
618-
inputTokens: 0,
619-
outputTokens: 0,
620-
cacheReadTokens: 0,
621-
cacheWriteTokens: 0,
622-
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
623-
}
624-
})
625-
const fallbackDays = aggregateProjectsIntoDays(scanProjects)
626-
const liveDays = fallbackDays.map(d => {
627-
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
628-
return {
629-
date: d.date,
630-
cost: prov.cost,
631-
calls: prov.calls,
632-
inputTokens: 0,
633-
outputTokens: 0,
634-
cacheReadTokens: 0,
635-
cacheWriteTokens: 0,
636-
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
637-
}
638-
})
639-
dailyHistory = [...histFromCache, ...liveDays]
590+
dailyHistory = [...historyFromCache, ...todayFromParse]
640591
}
641592

642593
const home = homedir()

src/providers/codex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
370370
.filter(c => c.type === 'input_text')
371371
.map(c => c.text ?? '')
372372
.filter(Boolean)
373-
if (texts.length > 0) pendingUserMessage = texts.join(' ')
373+
if (texts.length > 0) pendingUserMessage = texts.join(' ').slice(0, 500)
374374
continue
375375
}
376376

src/providers/gemini.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { readdir, readFile, stat } from 'fs/promises'
1+
import { readdir, stat } from 'fs/promises'
22
import { join } from 'path'
33
import { homedir } from 'os'
44

5+
import { readSessionFile } from '../fs-utils.js'
56
import { calculateCost } from '../models.js'
67
import { extractBashCommands } from '../bash-utils.js'
78
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
@@ -185,12 +186,8 @@ function parseJsonl(raw: string): GeminiSession | null {
185186
function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
186187
return {
187188
async *parse(): AsyncGenerator<ParsedProviderCall> {
188-
let raw: string
189-
try {
190-
raw = await readFile(source.path, 'utf-8')
191-
} catch {
192-
return
193-
}
189+
const raw = await readSessionFile(source.path)
190+
if (raw === null) return
194191

195192
let data: GeminiSession | null = null
196193

0 commit comments

Comments
 (0)