mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-22 11:08:33 +00:00
Fix per-provider data loss, history regression, and decode fragility (#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.
This commit is contained in:
parent
3542407f8f
commit
0f55a446da
8 changed files with 39 additions and 89 deletions
|
|
@ -122,10 +122,10 @@ extension CurrentBlock {
|
|||
oneShotRate = try c.decodeIfPresent(Double.self, forKey: .oneShotRate)
|
||||
inputTokens = try c.decode(Int.self, forKey: .inputTokens)
|
||||
outputTokens = try c.decode(Int.self, forKey: .outputTokens)
|
||||
cacheHitPercent = try c.decode(Double.self, forKey: .cacheHitPercent)
|
||||
topActivities = try c.decode([ActivityEntry].self, forKey: .topActivities)
|
||||
topModels = try c.decode([ModelEntry].self, forKey: .topModels)
|
||||
providers = try c.decode([String: Double].self, forKey: .providers)
|
||||
cacheHitPercent = try c.decodeIfPresent(Double.self, forKey: .cacheHitPercent) ?? 0
|
||||
topActivities = try c.decodeIfPresent([ActivityEntry].self, forKey: .topActivities) ?? []
|
||||
topModels = try c.decodeIfPresent([ModelEntry].self, forKey: .topModels) ?? []
|
||||
providers = try c.decodeIfPresent([String: Double].self, forKey: .providers) ?? [:]
|
||||
topProjects = try c.decodeIfPresent([ProjectEntry].self, forKey: .topProjects) ?? []
|
||||
modelEfficiency = try c.decodeIfPresent([ModelEfficiencyEntry].self, forKey: .modelEfficiency) ?? []
|
||||
topSessions = try c.decodeIfPresent([TopSessionEntry].self, forKey: .topSessions) ?? []
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ struct ActivitySection: View {
|
|||
}
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
let maxCost = store.payload.current.topActivities.map(\.cost).max() ?? 1
|
||||
let maxCost = max(store.payload.current.topActivities.map(\.cost).max() ?? 1, 0.01)
|
||||
ForEach(store.payload.current.topActivities, id: \.name) { activity in
|
||||
ActivityRow(activity: activity, maxCost: maxCost)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ private struct BarTooltipCard: View {
|
|||
|
||||
if !bar.topModels.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
ForEach(Array(bar.topModels.prefix(4).enumerated()), id: \.element.name) { idx, m in
|
||||
ForEach(Array(bar.topModels.prefix(4).enumerated()), id: \.offset) { idx, m in
|
||||
HStack(spacing: 6) {
|
||||
RoundedRectangle(cornerRadius: 1)
|
||||
.fill(Theme.brandAccent.opacity(0.75 - Double(idx) * 0.12))
|
||||
|
|
|
|||
|
|
@ -653,8 +653,10 @@ struct FooterBar: View {
|
|||
}
|
||||
|
||||
let fresh = await FXRateCache.shared.rate(for: code)
|
||||
store.currency = code
|
||||
CurrencyState.shared.apply(code: code, rate: fresh ?? cached, symbol: symbol)
|
||||
if let rate = fresh ?? cached {
|
||||
store.currency = code
|
||||
CurrencyState.shared.apply(code: code, rate: rate, symbol: symbol)
|
||||
}
|
||||
}
|
||||
|
||||
CLICurrencyConfig.persist(code: code)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ struct ModelsSection: View {
|
|||
}
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
let maxCost = store.payload.current.topModels.map(\.cost).max() ?? 1
|
||||
let maxCost = max(store.payload.current.topModels.map(\.cost).max() ?? 1, 0.01)
|
||||
ForEach(store.payload.current.topModels, id: \.name) { model in
|
||||
ModelRow(model: model, maxCost: maxCost)
|
||||
}
|
||||
|
|
|
|||
95
src/main.ts
95
src/main.ts
|
|
@ -467,7 +467,6 @@ program
|
|||
let scanRange: DateRange
|
||||
let cache: DailyCache
|
||||
let todayProviderData: PeriodData | null = null
|
||||
let usedPerProviderCachePath = false
|
||||
|
||||
if (isAllProviders) {
|
||||
cache = await hydrateCache()
|
||||
|
|
@ -487,32 +486,11 @@ program
|
|||
}
|
||||
} else {
|
||||
cache = await loadDailyCache()
|
||||
const cacheIsCurrent = cache.lastComputedDate !== null
|
||||
&& cache.lastComputedDate >= yesterdayStr
|
||||
if (cacheIsCurrent && rangeStartStr < todayStr) {
|
||||
const todayProviderProjects = fp(await parseAllSessions(todayRange, pf))
|
||||
todayProviderData = buildPeriodData(periodInfo.label, todayProviderProjects)
|
||||
const historicalDays = getDaysInRange(cache, rangeStartStr, yesterdayStr)
|
||||
let histCost = 0, histCalls = 0
|
||||
for (const d of historicalDays) {
|
||||
const prov = d.providers[pf]
|
||||
if (prov) { histCost += prov.cost; histCalls += prov.calls }
|
||||
}
|
||||
currentData = {
|
||||
...todayProviderData,
|
||||
cost: todayProviderData.cost + histCost,
|
||||
calls: todayProviderData.calls + histCalls,
|
||||
}
|
||||
scanProjects = todayProviderProjects
|
||||
scanRange = todayRange
|
||||
usedPerProviderCachePath = true
|
||||
} else {
|
||||
const fullProjects = fp(await parseAllSessions(periodInfo.range, pf))
|
||||
todayProviderData = buildPeriodData(periodInfo.label, fullProjects)
|
||||
currentData = todayProviderData
|
||||
scanProjects = fullProjects
|
||||
scanRange = periodInfo.range
|
||||
}
|
||||
const fullProjects = fp(await parseAllSessions(periodInfo.range, pf))
|
||||
todayProviderData = buildPeriodData(periodInfo.label, fullProjects)
|
||||
currentData = todayProviderData
|
||||
scanProjects = fullProjects
|
||||
scanRange = periodInfo.range
|
||||
}
|
||||
|
||||
// PROVIDERS
|
||||
|
|
@ -579,7 +557,8 @@ program
|
|||
topModels,
|
||||
}
|
||||
})
|
||||
} else if (usedPerProviderCachePath) {
|
||||
} else {
|
||||
const emptyModels = [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[]
|
||||
const historyFromCache = allCacheDays.map(d => {
|
||||
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
|
||||
return {
|
||||
|
|
@ -590,53 +569,25 @@ program
|
|||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
|
||||
topModels: emptyModels,
|
||||
}
|
||||
})
|
||||
const todayCost = todayProviderData!.cost
|
||||
const todayCalls = todayProviderData!.calls
|
||||
if (todayCost > 0 || todayCalls > 0) {
|
||||
historyFromCache.push({
|
||||
date: todayStr,
|
||||
cost: todayCost,
|
||||
calls: todayCalls,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
topModels: [],
|
||||
const todayFromParse = aggregateProjectsIntoDays(scanProjects)
|
||||
.filter(d => d.date === todayStr)
|
||||
.map(d => {
|
||||
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
|
||||
return {
|
||||
date: d.date,
|
||||
cost: prov.cost,
|
||||
calls: prov.calls,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
topModels: emptyModels,
|
||||
}
|
||||
})
|
||||
}
|
||||
dailyHistory = historyFromCache
|
||||
} else {
|
||||
const histFromCache = allCacheDays.map(d => {
|
||||
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
|
||||
return {
|
||||
date: d.date,
|
||||
cost: prov.cost,
|
||||
calls: prov.calls,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
|
||||
}
|
||||
})
|
||||
const fallbackDays = aggregateProjectsIntoDays(scanProjects)
|
||||
const liveDays = fallbackDays.map(d => {
|
||||
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
|
||||
return {
|
||||
date: d.date,
|
||||
cost: prov.cost,
|
||||
calls: prov.calls,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
|
||||
}
|
||||
})
|
||||
dailyHistory = [...histFromCache, ...liveDays]
|
||||
dailyHistory = [...historyFromCache, ...todayFromParse]
|
||||
}
|
||||
|
||||
const home = homedir()
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
.filter(c => c.type === 'input_text')
|
||||
.map(c => c.text ?? '')
|
||||
.filter(Boolean)
|
||||
if (texts.length > 0) pendingUserMessage = texts.join(' ')
|
||||
if (texts.length > 0) pendingUserMessage = texts.join(' ').slice(0, 500)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { readdir, readFile, stat } from 'fs/promises'
|
||||
import { readdir, stat } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
import { readSessionFile } from '../fs-utils.js'
|
||||
import { calculateCost } from '../models.js'
|
||||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
|
@ -185,12 +186,8 @@ function parseJsonl(raw: string): GeminiSession | null {
|
|||
function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
|
||||
return {
|
||||
async *parse(): AsyncGenerator<ParsedProviderCall> {
|
||||
let raw: string
|
||||
try {
|
||||
raw = await readFile(source.path, 'utf-8')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const raw = await readSessionFile(source.path)
|
||||
if (raw === null) return
|
||||
|
||||
let data: GeminiSession | null = null
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue