Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -8,6 +8,7 @@
- Settings: allow minimizing the Settings window while keeping its Dock tile available for restoring it (#2945). Thanks @Yuxin-Qiao!

### Fixed
- 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
Loading