Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c89c80c
feat(spend): cache-first and 5m TTL for dashboard
Yuxin-Qiao Aug 20, 2026
a0d0f94
fix(spend): repair cache-ttl LoadPhase comparison without Equatable
Yuxin-Qiao Aug 21, 2026
f7cd044
fix(test): pin claude spend snapshot in observation test
Yuxin-Qiao Aug 21, 2026
01c3518
feat(spend): gate dashboard refreshes on 5m snapshot staleness
Yuxin-Qiao Aug 21, 2026
43f691b
test(spend): cover OpenCodex window start and empty cache hit
Yuxin-Qiao Aug 21, 2026
4f373ac
perf(spend): load OpenCodex entries within report window
Yuxin-Qiao Aug 21, 2026
a2f7f27
fix(spend): route non-forced reopen through TTL-aware refresh
Yuxin-Qiao Aug 21, 2026
b8ff9f2
fix(test): sync spend controller gatekeeper anchors
Yuxin-Qiao Aug 21, 2026
9c4da39
fix(spend): apply report window to OpenCodex cache misses
Yuxin-Qiao Aug 21, 2026
5c1f6f5
perf(spend): hydrate codex reports from aggregates without row decode
Yuxin-Qiao Aug 21, 2026
23338cc
fix(spend): adopt legacy token freshness when dashboard slot has no m…
Yuxin-Qiao Aug 21, 2026
9cb04e2
Fix Sakana cancellation wait formatting
Yuxin-Qiao Aug 22, 2026
09fe486
perf(spend): reconcile aggregate hydration and OpenCodex cache windows
Yuxin-Qiao Aug 22, 2026
e4466e0
test(spend): correct gatekeeper anchors for refreshed dashboard files
Yuxin-Qiao Aug 22, 2026
51269cd
fix(spend): regenerate parser hash after reconciliation changes
Yuxin-Qiao Aug 22, 2026
99e5035
fix(spend): use exclusive output for cutover stored units
Yuxin-Qiao Aug 22, 2026
c71ff1a
fix(spend): cache-first aggregate hydration and tokscale parity
Yuxin-Qiao Aug 22, 2026
63e8df3
fix(spend): regenerate parser hash for aggregate hydration
Yuxin-Qiao Aug 22, 2026
4381ec5
fix(spend): repair gatekeeper anchors and pricing test for 3107
Yuxin-Qiao Aug 22, 2026
c8324ae
fix: repair 3107 gatekeeper and cutover tolerance
Yuxin-Qiao Aug 22, 2026
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
1 change: 1 addition & 0 deletions Sources/CodexBar/PreferencesSpendDashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ struct SpendDashboardPane: View {
.onAppear {
self.isVisible = true
self.controller.update(configuration: self.configuration)
self.controller.refreshIfStale()
if !self.controller.isRefreshing {
self.synchronizeCodexCostCatchUp()
}
Expand Down
40 changes: 32 additions & 8 deletions Sources/CodexBar/SpendDashboardController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ enum SpendDashboardRequestBuildMode: Equatable, Sendable {
self == .forceRefresh
}

/// Refreshes missing publications plus dashboard publications whose fetch metadata is stale.
func shouldRefresh(hasPublication: Bool, isDashboardTokenStale: Bool = false) -> Bool {
switch self {
case .refreshMissing: !hasPublication || isDashboardTokenStale
case .forceRefresh: true
case .captureOnly: false
}
}

func shouldRefresh(hasPublication: Bool) -> Bool {
switch self {
case .refreshMissing: !hasPublication
Expand Down Expand Up @@ -234,15 +243,21 @@ enum SpendDashboardSource {
publication: captured.publication,
publicationRevision: captured.revision)
}
let baselinesToRefresh = providerBaselines.filter { mode.shouldRefresh(hasPublication: $0.publication != nil) }
let baselinesToRefresh = providerBaselines.filter { baseline in
mode.shouldRefresh(
hasPublication: baseline.publication != nil,
isDashboardTokenStale: store.spendDashboardTokenFetchIsStale(for: baseline.provider))
}
if !baselinesToRefresh.isEmpty {
await withTaskGroup(of: Void.self) { group in
for baseline in baselinesToRefresh {
group.addTask {
if UsageStore.tokenCostRequiresProviderSnapshot(baseline.provider) {
await store.refreshProvider(baseline.provider)
} else {
await store.refreshSpendDashboardTokenUsageNow(for: baseline.provider, force: true)
await store.refreshSpendDashboardTokenUsageNow(
for: baseline.provider,
force: mode.forcesLoader)
}
}
}
Expand Down Expand Up @@ -1166,12 +1181,10 @@ final class SpendDashboardController {
self.failedSourceCount = 0
self.rebuildModel()
}
let shouldPrimeCachedCodex: Bool = if case .ordinary = phase {
self.cachedLoader != nil && !Set(Self.codexOwnershipByID(configuration.codexAccountIdentities).keys)
.isSubset(of: Set(self.loadedInputs.map(\.id)))
} else {
false
}
let shouldPrimeCachedCodex: Bool = self.cachedLoader != nil
&& !Set(Self.codexOwnershipByID(configuration.codexAccountIdentities).keys)
.isSubset(of: Set(self.loadedInputs.map(\.id)))
&& (!phase.manualRefreshOutstanding || self.loadedInputs.isEmpty)

guard configuration.costUsageEnabled,
!configuration.providerIDs.isEmpty || configuration.openCodexUsageLogsEnabled
Expand Down Expand Up @@ -1431,6 +1444,17 @@ final class SpendDashboardController {
self.update(configuration: configuration, force: true)
}

/// Reopening the pane can produce a configuration identical to the loaded one, which
/// `update(configuration:)` intentionally ignores. A stale-TTL-only reopen still needs
/// one non-forced request build so `.refreshMissing` can evaluate dashboard freshness.
func refreshIfStale() {
guard let configuration,
!self.isRefreshing,
!self.phase.manualRefreshOutstanding
else { return }
self.startLoad(configuration: configuration, phase: .ordinary)
}

func selectDays(_ days: Int) {
let days = Self.normalizedDays(days)
guard days != self.selectedDays else { return }
Expand Down
8 changes: 6 additions & 2 deletions Sources/CodexBar/SpendDashboardSource+OpenCodex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension SpendDashboardSource {
_ inputs: [SpendDashboardModel.ProviderInput],
request: SpendDashboardLoadRequest,
environment: [String: String] = ProcessInfo.processInfo.environment,
entryLoader: ((URL) throws -> [OpenCodexUsageEntry])? = nil) -> (
entryLoader: ((URL, Date?) throws -> [OpenCodexUsageEntry])? = nil) -> (
inputs: [SpendDashboardModel.ProviderInput],
observation: SpendDashboardLoadResult.OpenCodexObservation)
{
Expand All @@ -30,7 +30,11 @@ extension SpendDashboardSource {
let store = OpenCodexUsageStore(cacheRoot: OpenCodexUsageLog.cacheRoot())
let entries: [OpenCodexUsageEntry]
do {
entries = try entryLoader?(logURL) ?? store.loadEntries(logURL: logURL)
let since = OpenCodexUsageStore.windowStart(
now: request.now,
historyDays: Self.scanDays,
calendar: request.configuration.bucketCalendar)
entries = try entryLoader?(logURL, since) ?? store.loadEntries(logURL: logURL, since: since)
} catch {
return (inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }, .unavailable)
}
Expand Down
32 changes: 31 additions & 1 deletion Sources/CodexBar/UsageStore+SpendDashboardTokenCost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,35 @@ extension UsageStore {
self.spendDashboardTokenPublicationRevisions[provider.instanceID] ?? 0
}

func spendDashboardTokenFetchIsStale(for provider: UsageProvider) -> Bool {
guard Self.usesSpendDashboardIndependentTokenSnapshot(provider) else { return false }
let costScopeSignature = self.spendDashboardTokenSnapshotScopeSignature(for: provider)
guard let lastAt = self.lastSpendDashboardTokenFetchAt[provider.instanceID] else {
// A confirmed empty dashboard publication owns freshness itself; the legacy-slot
// adoption below only covers providers whose first scan has not published here yet.
if self.spendDashboardTokenSnapshotPublicationForCurrentConfig(for: provider) != nil {
return false
Comment on lines +35 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist the timestamp after a completed dashboard fetch

After the first successful or confirmed-empty dashboard scan, this branch treats the current publication as fresh whenever lastSpendDashboardTokenFetchAt is absent, but this change also removes the only production assignment to that dictionary and no success path replaces it. With an unchanged provider scope, the five-minute comparison is consequently never reached and ordinary pane reopens reuse the publication indefinitely; record the completion time when publishing a successful or empty result while leaving failures timestamp-free.

Useful? React with 👍 / 👎.

}
// Providers served by the shared token pipeline publish through the legacy slot;
// adopt its freshness instead of double-fetching on the first dashboard open.
guard self.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) != nil,
let legacyLast = self.lastTokenFetchAt[provider.instanceID]
else { return true }
return Date().timeIntervalSince(legacyLast) >= 5 * 60
Comment on lines +40 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require full dashboard coverage before adopting legacy freshness

When the regular token pipeline has just published its default 30-day snapshot before the first dashboard open, this branch treats it as fresh solely from its timestamp even though the dashboard requests scanDays == 365. capturedTokenPublication then falls back to that legacy snapshot and skips the independent dashboard fetch, so the dashboard's 365-day/All view is populated with only 30 days of history until another refresh is triggered. Reuse legacy freshness only when its history coverage and scope satisfy the dashboard request.

Useful? React with 👍 / 👎.

}
return self.spendDashboardTokenSnapshotPublicationForCurrentConfig(for: provider) == nil
|| self.lastSpendDashboardTokenFetchScope[provider.instanceID] != costScopeSignature
|| Date().timeIntervalSince(lastAt) >= 5 * 60
}

func _setLastSpendDashboardTokenFetchAtForTesting(_ date: Date?, provider: UsageProvider) {
if let date {
self.lastSpendDashboardTokenFetchAt[provider.instanceID] = date
} else {
self.lastSpendDashboardTokenFetchAt.removeValue(forKey: provider.instanceID)
}
}

func clearSpendDashboardTokenSnapshot(for provider: UsageProvider) {
self.spendDashboardTokenPublications.removeValue(forKey: provider.instanceID)
}
Expand Down Expand Up @@ -69,9 +98,10 @@ extension UsageStore {
}
let costScope = self.tokenCostScope(for: provider)
let costScopeSignature = self.spendDashboardTokenSnapshotScopeSignature(for: provider)
// TTL: pane re-open within 5m reuses existing dashboard snapshot.
if !force, !self.spendDashboardTokenFetchIsStale(for: provider) { return }
let publicationRevision = self.providerPublicationRevision(for: provider)
let providerConfigRevision = self.settings.providerConfigRevision(for: provider)
self.lastSpendDashboardTokenFetchAt[provider.instanceID] = now
self.lastSpendDashboardTokenFetchScope[provider.instanceID] = costScopeSignature
self.spendDashboardTokenRefreshInFlight.insert(provider.instanceID)
defer { self.spendDashboardTokenRefreshInFlight.remove(provider.instanceID) }
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ public struct CostUsageFetcher: Sendable {
let shouldMergePiUsage = scopedCodexHomePath?.isEmpty != false
let roots = CostUsageScanner.codexSessionsRoots(options: options)
let rootsFingerprint = CostUsageScanner.codexRootsFingerprint(options: options)
let loadedCache = CostUsageStoreAccess.read(
let loadedCache = CostUsageStoreAccess.readReportAggregate(
cacheRoot: options.cacheRoot,
calendar: options.calendar)
let cache = CostUsageScanner.codexCache(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "3c984b655688593f"
static let value = "585341b8f3aac0d8"
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,23 @@ extension CostUsageScanner {
{
var breakdown = CodexRowCostBreakdown()
for row in rows {
let priorityMetadata = row.turnID.flatMap { priorityTurns[$0] }
let isPriority = priorityMetadata != nil || row.pricingMode == "priority"
// Zero-token authoritative-cost carriers (aggregate-hydration synthesis) price a
// day without contributing to its token ownership, so exclude them from the row
// token totals that must equal the aggregate target, but still record their cost.
if row.input == 0, row.cached == 0, row.output == 0,
(row.knownCostNanos ?? 0) != 0
{
if isPriority {
breakdown.priorityCostUSD += Double(row.knownCostNanos ?? 0) / Self.costScale
breakdown.sawPriorityCost = true
} else {
breakdown.standardCostUSD += Double(row.knownCostNanos ?? 0) / Self.costScale
breakdown.sawStandardCost = true
}
continue
}
let (tokenCount, tokenOverflow) = max(0, row.input).addingReportingOverflow(max(0, row.output))
let hasTokens = row.input > 0 || row.cached > 0 || row.output > 0
if tokenOverflow {
Expand All @@ -166,11 +183,12 @@ extension CostUsageScanner {
if hasTokens, row.eventIndex == nil {
breakdown.hasUnstableTokenRows = true
}
if !hasTokens, (row.knownCostNanos ?? 0) != 0 {
breakdown.hasIncompletePricing = true
}
if (row.unpricedTokens ?? 0) > 0 {
breakdown.hasIncompletePricing = true
}
let priorityMetadata = row.turnID.flatMap { priorityTurns[$0] }
let isPriority = priorityMetadata != nil || row.pricingMode == "priority"
if isPriority {
let (total, overflow) = breakdown.priorityTokens.addingReportingOverflow(tokenCount)
breakdown.priorityTokens = overflow ? breakdown.priorityTokens : total
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ extension CostUsageScanner {
?? row.model
let overlay = customPricing ?? .empty
let pricingDate = row.timestampUnixMs.map { Date(timeIntervalSince1970: Double($0) / 1000) }
// Rows store output exclusive of reasoning (tokscale parity); OpenAI bills reasoning at
// the output rate, so add the subset back before pricing. USD matches the previous
// inclusive-output behavior exactly.
let billableOutputTokens = row.output + (row.reasoning ?? 0)
let baseCost = CostUsagePricing.codexCostUSD(
model: pricedModel,
inputTokens: row.input,
cachedInputTokens: row.cached,
outputTokens: row.output,
outputTokens: billableOutputTokens,
pricingDate: pricingDate,
modelsDevCatalog: modelsDevCatalog,
modelsDevCacheRoot: modelsDevCacheRoot,
Expand All @@ -32,7 +36,7 @@ extension CostUsageScanner {
model: pricedModel,
inputTokens: row.input,
cachedInputTokens: row.cached,
outputTokens: row.output,
outputTokens: billableOutputTokens,
pricingDate: pricingDate,
modelsDevCatalog: modelsDevCatalog,
modelsDevCacheRoot: modelsDevCacheRoot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,25 @@ extension CostUsageScanner {
}

static func codexCanonicalPricingRows(_ usage: CostUsageFileUsage) -> CodexCanonicalPricingRows {
let persistedRows = usage.codexRows ?? []
var persistedRows = usage.codexRows ?? []
// An all-zero aggregate cannot prove copied zero-token cost prefixes are owned by this
// file, so drop synthetic cost carriers there instead of pricing them.
let zeroTokenGroups = Set(
usage.days.flatMap { day, models in
models.compactMap { model, packed in
(packed[safe: 0] ?? 0) == 0 && (packed[safe: 1] ?? 0) == 0 && (packed[safe: 2] ?? 0) == 0
? CodexDayModelKey(day: day, model: model)
: nil
}
})
if !zeroTokenGroups.isEmpty {
persistedRows.removeAll { row in
row.turnID == nil && row.eventIndex == nil
&& row.input == 0 && row.cached == 0 && row.output == 0
&& (row.knownCostNanos ?? 0) != 0
&& zeroTokenGroups.contains(CodexDayModelKey(day: row.day, model: row.model))
}
}
let rowsByGroup = Dictionary(grouping: persistedRows) {
CodexDayModelKey(day: $0.day, model: $0.model)
}
Expand All @@ -29,18 +47,58 @@ extension CostUsageScanner {
for model in models.keys.sorted() {
let key = CodexDayModelKey(day: day, model: model)
let packed = models[model] ?? []
let groupRows = rowsByGroup[key] ?? []
let target = CodexRowTokenTotals(
input: max(0, packed[safe: 0] ?? 0),
cached: max(0, packed[safe: 1] ?? 0),
output: max(0, packed[safe: 2] ?? 0))
// Aggregate hydration synthesizes zero-token metadata rows (reasoning and
// authoritative costs). When the aggregate target has tokens, they carry no
// token totals of their own, so they must survive reconciliation even when a
// suffix subset would otherwise be selected. An all-zero target means exact
// ownership cannot be established; metadata rows stay excluded there.
let targetHasTokens = target.input != 0 || target.cached != 0 || target.output != 0
let metadataRows = targetHasTokens
? groupRows.filter { row in
row.input == 0 && row.cached == 0 && row.output == 0
&& row.turnID == nil && row.eventIndex == nil
&& (row.reasoning != nil || (row.knownCostNanos ?? 0) != 0)
}
: []
let tokenRows = groupRows.filter { row in
!(
row.input == 0 && row.cached == 0 && row.output == 0
&& row.turnID == nil && row.eventIndex == nil
&& (row.reasoning != nil || (row.knownCostNanos ?? 0) != 0))
}
guard let rows = self.reconciledCodexPricingRows(
rowsByGroup[key] ?? [],
tokenRows,
target: target)
else {
unresolvedGroups.insert(key)
continue
}
canonicalRows.append(contentsOf: rows)
let firstTokenIndex = rows.firstIndex {
$0.input > 0 || $0.cached > 0 || $0.output > 0
} ?? rows.endIndex
let hasSyntheticReasoning = metadataRows.contains { $0.reasoning != nil }
let hasSyntheticCost = metadataRows.contains { ($0.knownCostNanos ?? 0) != 0 }
if metadataRows.isEmpty {
canonicalRows.append(contentsOf: rows)
} else if hasSyntheticCost, firstTokenIndex != rows.startIndex {
// Cost carriers price a day, so they must sit inside the token-bearing
// span rather than before its first row; otherwise the zero-token skip in
// cost accounting would treat them as a stale copied prefix.
canonicalRows.append(contentsOf: rows[..<firstTokenIndex])
canonicalRows.append(contentsOf: metadataRows)
canonicalRows.append(contentsOf: rows[firstTokenIndex...])
} else {
// Reasoning-only carriers never price anything and cost-only carriers at
// the start of a span are handled by the zero-token skip above, so plain
// append keeps token ownership math untouched regardless of position.
canonicalRows.append(contentsOf: rows)
canonicalRows.append(contentsOf: metadataRows)
}
}
}

Expand Down Expand Up @@ -153,11 +211,19 @@ extension CostUsageScanner {
_ rows: [CodexUsageRow],
target: CodexRowTokenTotals) -> [CodexUsageRow]?
{
// Zero-token rows carry no token totals, so they never affect ownership math.
var allRowsTotal = CodexRowTokenTotals()
guard rows.allSatisfy({ allRowsTotal.add($0) }) else { return nil }
let tokenRows = rows.filter { row in
row.input != 0 || row.cached != 0 || row.output != 0
}
guard tokenRows.allSatisfy({ allRowsTotal.add($0) }) else { return nil }
if target == CodexRowTokenTotals() {
// An all-zero aggregate proves no token ownership. Drop every row, including
// synthetic cost/reasoning carriers, instead of letting a copied cost-only
// prefix price the group.
return []
}
guard !tokenRows.isEmpty else { return nil }
if allRowsTotal == target {
let firstTokenRow = rows.firstIndex {
$0.input > 0 || $0.cached > 0 || $0.output > 0
Expand All @@ -173,7 +239,9 @@ extension CostUsageScanner {

var suffixTotal = CodexRowTokenTotals()
for index in rows.indices.reversed() {
guard suffixTotal.add(rows[index]) else { return nil }
let row = rows[index]
if row.input == 0, row.cached == 0, row.output == 0 { continue }
guard suffixTotal.add(row) else { return nil }
if suffixTotal == target {
return Array(rows[index...])
}
Expand Down
Loading
Loading