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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
### Fixed
- Menu: apply the cost summary display style to every provider's menu card, so Submenu only hides inline cost rows for z.ai and other providers (#2976). Thanks @ar0nbg!
- Cost history: align x-axis date labels with their bars in status-menu charts (#2974). Thanks @Yuxin-Qiao!
- Codex: preserve completed empty local history as known-zero usage and spend without fabricating zeroes for incomplete scans (#2932). Thanks @Yuxin-Qiao!
- Codex: keep CLI-owned `auth.json` read-only during usage refresh, delegate stale native credentials to CLI recovery, and fail closed for stale external OAuth files (#2944). Thanks @Yuxin-Qiao!
- Usage & Spend: keep safely priced Codex totals visible after completed history scans when request-tier uncertainty leaves some days unpriced (#2948). Thanks @Atopoz for the report!
- Vertex AI: match Cloud Monitoring quota usage without a `limit_name` to its unambiguous same-metric, same-location limit, restoring quota percentages (#2958). Thanks @MachApple!
Expand Down
54 changes: 50 additions & 4 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,9 @@ public struct CostUsageFetcher: Sendable {

let scoped = CostUsageScanner.codexCache(cache, scopedTo: roots)
let progressKey = self.codexScanProgressKey(cache: cache, scopedFiles: scoped.files)
let hasIncompleteFile = scoped.files.values.contains { $0.codexScanComplete == false }
let hasIncompleteFile = scoped.files.values.contains {
$0.codexScanComplete == false || $0.hasBufferedCodexForkRetryLines
}
let pending = cache.codexScanCatchUpPending == true || hasIncompleteFile
return CodexScanCatchUpStatus(
pending: pending,
Expand All @@ -332,6 +334,25 @@ public struct CostUsageFetcher: Sendable {
return !status.pending && status.progressKey != "scope-mismatch"
}

private static let establishedEmptyCodexDailyReport = CostUsageDailyReport(data: [], summary: nil)

private static func codexCachedHistoryCoverageIsEstablished(
cache: CostUsageCache,
range: CostUsageScanner.CostUsageDayRange,
rootsFingerprint: [String: Int64]) -> Bool
{
guard cache.lastScanUnixMs > 0,
cache.timeZoneIdentifier == range.calendar.timeZone.identifier,
cache.roots == rootsFingerprint,
cache.codexScanCatchUpPending != true,
!cache.files.values.contains(where: {
$0.codexScanComplete == false || $0.hasBufferedCodexForkRetryLines
}),
!CostUsageScanner.requestedWindowExpandsCache(range: range, cache: cache)
else { return false }
return true
}

private static func resolvedScannerOptions(
_ override: CostUsageScanner.Options?,
provider: UsageProvider,
Expand Down Expand Up @@ -736,7 +757,9 @@ public struct CostUsageFetcher: Sendable {
guard cache.timeZoneIdentifier == options.calendar.timeZone.identifier,
cache.roots == rootsFingerprint,
cache.codexScanCatchUpPending != true,
!cache.files.values.contains(where: { $0.codexScanComplete == false }),
!cache.files.values.contains(where: {
$0.codexScanComplete == false || $0.hasBufferedCodexForkRetryLines
}),
let cachedSince = cache.scanSinceKey,
let cachedUntil = cache.scanUntilKey
else { return nil }
Expand Down Expand Up @@ -822,6 +845,10 @@ public struct CostUsageFetcher: Sendable {
var scanTimes: [Date] = []
var piMerged = false
var staleSnapshotUpdatedAt: Date?
let nativeHistoryCoverageIsEstablished = Self.codexCachedHistoryCoverageIsEstablished(
cache: cache,
range: range,
rootsFingerprint: rootsFingerprint)

if let previous = CostUsageScanner.codexPreviousReport(
cache: cache,
Expand Down Expand Up @@ -865,6 +892,18 @@ public struct CostUsageFetcher: Sendable {
}
}

// A completed scan can legitimately have no rows (a fresh account or a quiet
// window). Keep that established-empty state across app restarts instead of
// collapsing it back to "unavailable" merely because the cache has no day map.
if reports.isEmpty, nativeHistoryCoverageIsEstablished {
reports.append(Self.establishedEmptyCodexDailyReport)
if cache.lastScanUnixMs > 0 {
let scanAt = Date(timeIntervalSince1970: TimeInterval(cache.lastScanUnixMs) / 1000)
nativeScanAt = scanAt
scanTimes.append(scanAt)
}
}

if includePiSessions,
shouldMergePiUsage,
let piResult = PiSessionCostScanner.loadCachedDailyReportResult(
Expand Down Expand Up @@ -1055,17 +1094,22 @@ public struct CostUsageFetcher: Sendable {
? CostUsageTokenSnapshot.entry(in: daily.data, forLocalDayContaining: now, calendar: calendar)
: CostUsageTokenSnapshot.latestEntry(in: daily.data)
let hasHistoricalRows = !daily.data.isEmpty
let establishedEmptyHistory = historyCoverageIsEstablished && daily.data.isEmpty

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 Restrict established-empty zeroes to Codex

When Claude or Vertex returns an empty transcript report, loadLocalTokenScanResult unconditionally marks coverage established for every non-Codex provider; Bedrock likewise calls this helper with the default true. This shared predicate therefore converts those providers' existing “no data available” state into fabricated zero token and spend totals. Pass a Codex-specific established-empty flag instead of deriving zeroes from the generic coverage value.

Useful? React with 👍 / 👎.

let sessionTokens: Int? = if let sessionEntry {
sessionEntry.totalTokens
} else if hasHistoricalRows {
0
} else if establishedEmptyHistory {
0
} else {
nil
}
let sessionCostUSD: Double? = if let sessionEntry {
sessionEntry.costUSD
} else if hasHistoricalRows {
0
} else if establishedEmptyHistory {
0
} else {
nil
}
Expand All @@ -1076,14 +1120,16 @@ public struct CostUsageFetcher: Sendable {
let totalFromEntries = daily.data.compactMap(\.costUSD).reduce(0, +)
let allEntriesCarryCost = !daily.data.isEmpty && daily.data.allSatisfy { $0.costUSD != nil }
let last30DaysCostUSD = totalFromSummary
?? (allEntriesCarryCost ? totalFromEntries : nil)
?? (allEntriesCarryCost
? totalFromEntries
: establishedEmptyHistory ? 0 : nil)
let totalTokensFromSummary = daily.summary?.totalTokens
let totalTokensFromEntries = daily.data.compactMap(\.totalTokens).reduce(0, +)
let allEntriesCarryTokens = !daily.data.isEmpty && daily.data.allSatisfy { $0.totalTokens != nil }
let last30DaysTokens = totalTokensFromSummary
?? (allEntriesCarryTokens
? totalTokensFromEntries
: nil)
: establishedEmptyHistory ? 0 : nil)

return CostUsageTokenSnapshot(
sessionTokens: sessionTokens,
Expand Down
153 changes: 153 additions & 0 deletions Tests/CodexBarTests/CostUsageFetcherCacheSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,159 @@ struct CostUsageFetcherCacheSnapshotTests {
#expect(activity?.daily.isEmpty == true)
}

@Test
func `cached codex token snapshot preserves a completed empty history`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }

let now = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
let scanTime = now.addingTimeInterval(-60)
var cache = CostUsageCache()
cache.lastScanUnixMs = Int64(scanTime.timeIntervalSince1970 * 1000)
cache.scanSinceKey = "2026-04-07"
cache.scanUntilKey = "2026-04-09"
cache.timeZoneIdentifier = options.calendar.timeZone.identifier
cache.roots = CostUsageScanner.codexRootsFingerprint(options: options)
CostUsageStoreAccess.replace(
cacheRoot: env.cacheRoot,
cache: cache,
calendar: options.calendar)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshotResult(
now: now,
historyDays: 1,
includePiSessions: false,
scannerOptions: options)

#expect(cached?.snapshot.sessionTokens == 0)
#expect(cached?.snapshot.sessionCostUSD == 0)
#expect(cached?.snapshot.last30DaysTokens == 0)
#expect(cached?.snapshot.last30DaysCostUSD == 0)
#expect(cached?.snapshot.historyCoverageIsEstablished == true)
#expect(cached?.lastRefreshAt == scanTime)
}

@Test
func `cached empty history becomes unavailable after the local day rolls over`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(identifier: "America/Los_Angeles"))
var beforeMidnightComponents = DateComponents()
beforeMidnightComponents.calendar = calendar
beforeMidnightComponents.timeZone = calendar.timeZone
beforeMidnightComponents.year = 2026
beforeMidnightComponents.month = 4
beforeMidnightComponents.day = 8
beforeMidnightComponents.hour = 23
beforeMidnightComponents.minute = 59
let beforeMidnight = try #require(beforeMidnightComponents.date)
let afterMidnight = beforeMidnight.addingTimeInterval(2 * 60)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot,
calendar: calendar)
var cache = CostUsageCache()
cache.lastScanUnixMs = Int64(beforeMidnight.timeIntervalSince1970 * 1000)
cache.scanSinceKey = "2026-04-07"
cache.scanUntilKey = "2026-04-09"
cache.timeZoneIdentifier = calendar.timeZone.identifier
cache.roots = CostUsageScanner.codexRootsFingerprint(options: options)
CostUsageStoreAccess.replace(
cacheRoot: env.cacheRoot,
cache: cache,
calendar: calendar)

let established = await CostUsageFetcher.loadCachedCodexTokenSnapshotResult(
now: beforeMidnight,
historyDays: 1,
includePiSessions: false,
scannerOptions: options)
let expanded = await CostUsageFetcher.loadCachedCodexTokenSnapshotResult(
now: afterMidnight,
historyDays: 1,
includePiSessions: false,
scannerOptions: options)

#expect(established?.snapshot.last30DaysCostUSD == 0)
#expect(established?.snapshot.historyCoverageIsEstablished == true)
#expect(expanded == nil)
}

@Test
func `cached codex token snapshot refuses an empty history while catch up is pending`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }

let now = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
var cache = CostUsageCache()
cache.lastScanUnixMs = Int64(now.addingTimeInterval(-60).timeIntervalSince1970 * 1000)
cache.scanSinceKey = "2026-04-07"
cache.scanUntilKey = "2026-04-09"
cache.timeZoneIdentifier = options.calendar.timeZone.identifier
cache.roots = CostUsageScanner.codexRootsFingerprint(options: options)
cache.codexScanCatchUpPending = true
CostUsageStoreAccess.replace(
cacheRoot: env.cacheRoot,
cache: cache,
calendar: options.calendar)

let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshotResult(
now: now,
historyDays: 1,
includePiSessions: false,
scannerOptions: options)

#expect(cached == nil)
}

@Test
func `cached codex token snapshot refuses an empty history with buffered fork retries`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }

let now = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
let line = CostUsageScanner.CodexBufferedFastLine(
lineIndex: 1,
ordinal: nil,
line: .interAgentCommunication(triggerTurn: false))
let filePath = env.codexSessionsRoot.appendingPathComponent("fork.jsonl").path
var cache = CostUsageCache()
cache.lastScanUnixMs = Int64(now.addingTimeInterval(-60).timeIntervalSince1970 * 1000)
cache.scanSinceKey = "2026-04-07"
cache.scanUntilKey = "2026-04-09"
cache.timeZoneIdentifier = options.calendar.timeZone.identifier
cache.roots = CostUsageScanner.codexRootsFingerprint(options: options)
cache.files[filePath] = CostUsageScanner.makeFileUsage(
mtimeUnixMs: cache.lastScanUnixMs,
size: 1,
days: [:],
parsedBytes: 1,
codexScanComplete: true,
codexBufferedUnresolvedForkLines: [line])
CostUsageStoreAccess.replace(
cacheRoot: env.cacheRoot,
cache: cache,
calendar: options.calendar)

let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshotResult(
now: now,
historyDays: 1,
includePiSessions: false,
scannerOptions: options)

#expect(cached == nil)
}

@Test
func `cached codex token snapshot loads from existing cache without rescanning`() async throws {
let env = try CostUsageTestEnvironment()
Expand Down
25 changes: 25 additions & 0 deletions Tests/CodexBarTests/CostUsageFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ struct CostUsageFetcherTests {
}

extension CostUsageFetcherTests {
@Test
func `completed empty codex scan publishes known zero totals`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }

let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
var options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
options.refreshMinIntervalSeconds = 0

let snapshot = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: day,
historyDays: 1,
includePiSessions: false,
scannerOptions: options)

#expect(snapshot.historyCoverageIsEstablished)
#expect(snapshot.sessionTokens == 0)
#expect(snapshot.sessionCostUSD == 0)
#expect(snapshot.last30DaysTokens == 0)
#expect(snapshot.last30DaysCostUSD == 0)
}

@Test
func `codex history coverage follows pending catch up`() async throws {
let env = try CostUsageTestEnvironment()
Expand Down
30 changes: 30 additions & 0 deletions Tests/CodexBarTests/CostUsageTokenSnapshotDaySelectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,36 @@ struct CostUsageTokenSnapshotDaySelectionTests {
#expect(snapshot.last30DaysTokens == 0)
}

@Test
func `token snapshot reports known zero for an established empty history`() throws {
let now = try Self.localNoon(year: 2026, month: 5, day: 18)
let snapshot = CostUsageFetcher.tokenSnapshot(
from: CostUsageDailyReport(data: [], summary: nil),
now: now,
historyCoverageIsEstablished: true)

#expect(snapshot.sessionCostUSD == 0)
#expect(snapshot.sessionTokens == 0)
#expect(snapshot.last30DaysCostUSD == 0)
#expect(snapshot.last30DaysTokens == 0)
#expect(snapshot.historyCoverageIsEstablished)
}

@Test
func `token snapshot keeps an unestablished empty history unavailable`() throws {
let now = try Self.localNoon(year: 2026, month: 5, day: 18)
let snapshot = CostUsageFetcher.tokenSnapshot(
from: CostUsageDailyReport(data: [], summary: nil),
now: now,
historyCoverageIsEstablished: false)

#expect(snapshot.sessionCostUSD == nil)
#expect(snapshot.sessionTokens == nil)
#expect(snapshot.last30DaysCostUSD == nil)
#expect(snapshot.last30DaysTokens == nil)
#expect(!snapshot.historyCoverageIsEstablished)
}

@Test
func `token snapshot does not report a partial cost from mixed present and missing rows`() throws {
let now = try Self.localNoon(year: 2026, month: 5, day: 18)
Expand Down
Loading