Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions Sources/CodexBarCLI/CLICostCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ extension CodexBarCLI {
sessionTokens: snapshot?.sessionTokens,
sessionCostUSD: snapshot?.sessionCostUSD,
historyDays: snapshot?.historyDays,
historyCoverageIsEstablished: snapshot?.historyCoverageIsEstablished,
last30DaysTokens: snapshot?.last30DaysTokens,
last30DaysCostUSD: snapshot?.last30DaysCostUSD,
meteredCostUSD: snapshot?.meteredCostUSD,
Expand Down Expand Up @@ -479,6 +480,7 @@ struct CostPayload: Encodable, Sendable {
let sessionTokens: Int?
let sessionCostUSD: Double?
let historyDays: Int?
let historyCoverageIsEstablished: Bool?
let last30DaysTokens: Int?
let last30DaysCostUSD: Double?
let meteredCostUSD: Double?
Expand All @@ -495,6 +497,7 @@ struct CostPayload: Encodable, Sendable {
sessionTokens: Int?,
sessionCostUSD: Double?,
historyDays: Int?,
historyCoverageIsEstablished: Bool? = nil,
last30DaysTokens: Int?,
last30DaysCostUSD: Double?,
meteredCostUSD: Double? = nil,
Expand All @@ -510,6 +513,7 @@ struct CostPayload: Encodable, Sendable {
self.sessionTokens = sessionTokens
self.sessionCostUSD = sessionCostUSD
self.historyDays = historyDays
self.historyCoverageIsEstablished = historyCoverageIsEstablished
self.last30DaysTokens = last30DaysTokens
self.last30DaysCostUSD = last30DaysCostUSD
self.meteredCostUSD = meteredCostUSD
Expand Down
17 changes: 14 additions & 3 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ public struct CostUsageFetcher: Sendable {
return options
}

// swiftlint:disable:next function_body_length
static func loadTokenSnapshot(
provider: UsageProvider,
environment: [String: String] = ProcessInfo.processInfo.environment,
Expand Down Expand Up @@ -288,13 +289,14 @@ public struct CostUsageFetcher: Sendable {
// scanner-level checks.
let scanOptions = options
let scanResult = try await CostUsageScanExecutor.run { checkCancellation in
var daily = try CostUsageScanner.loadDailyReportCancellable(
var dailyResult = try CostUsageScanner.loadDailyReportResultCancellable(
provider: provider,
since: since,
until: now,
now: now,
options: scanOptions,
checkCancellation: checkCancellation)
var daily = dailyResult.report
try checkCancellation()

if provider == .vertexai,
Expand All @@ -304,13 +306,14 @@ public struct CostUsageFetcher: Sendable {
{
var fallback = scanOptions
fallback.claudeLogProviderFilter = .all
daily = try CostUsageScanner.loadDailyReportCancellable(
dailyResult = try CostUsageScanner.loadDailyReportResultCancellable(
provider: provider,
since: since,
until: now,
now: now,
options: fallback,
checkCancellation: checkCancellation)
daily = dailyResult.report
try checkCancellation()
}

Expand Down Expand Up @@ -355,7 +358,11 @@ public struct CostUsageFetcher: Sendable {
sessions = []
}
}
return (daily: daily, projects: projects, sessions: sessions)
return (
daily: daily,
projects: projects,
sessions: sessions,
historyCoverageIsEstablished: dailyResult.historyCoverageIsEstablished)
}

if allowPricingRefresh,
Expand Down Expand Up @@ -391,6 +398,7 @@ public struct CostUsageFetcher: Sendable {
now: now,
historyDays: clampedHistoryDays,
calendar: scanOptions.calendar,
historyCoverageIsEstablished: scanResult.historyCoverageIsEstablished,
projects: scanResult.projects,
sessions: scanResult.sessions)
}
Expand Down Expand Up @@ -597,6 +605,7 @@ public struct CostUsageFetcher: Sendable {
now: now,
historyDays: clampedHistoryDays,
calendar: options.calendar,
historyCoverageIsEstablished: cache.codexHistoryCoverageIsEstablished == true,

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 Validate the native cache before reusing its coverage flag

When the Pi cache is valid but the native Codex cache is rejected by the root, timezone, or requested-window checks above, reports can still contain the Pi report and reach this assignment. The coverage value nevertheless comes unconditionally from that rejected native cache; for example, after a complete 30-day native scan and a wider Pi scan, switching to 365 days can hydrate a Pi-only snapshot with historyCoverageIsEstablished: true even though native Codex history for the expanded window was never scanned. Gate this flag on the same native-cache validity checks, otherwise use false when only Pi data is loaded.

Useful? React with 👍 / 👎.

projects: Self.mergedProjectBreakdowns(projects),
sessions: sessions,
updatedAt: scanTimes.min()),
Expand Down Expand Up @@ -750,6 +759,7 @@ public struct CostUsageFetcher: Sendable {
historyDays: Int = 30,
useCurrentLocalDayForSession: Bool = true,
calendar: Calendar = .current,
historyCoverageIsEstablished: Bool = true,
meteredCostUSD: Double? = nil,
credentialScopeFingerprint: String? = nil,
historyLabel: String? = nil,
Expand Down Expand Up @@ -789,6 +799,7 @@ public struct CostUsageFetcher: Sendable {
last30DaysTokens: last30DaysTokens,
last30DaysCostUSD: last30DaysCostUSD,
historyDays: historyDays,
historyCoverageIsEstablished: historyCoverageIsEstablished,
historyLabel: historyLabel,
meteredCostUSD: meteredCostUSD,
credentialScopeFingerprint: credentialScopeFingerprint,
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 = "3aa49b47f4b78e13"
static let value = "7f01e8da62d05fec"
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ struct CostUsageCache: Codable {
var codexProjectMetadataVersion: Int?
var codexPriorityTurnKeys: [String: String]?
var codexPriorityTurnIDsByDay: [String: [String]]?
var codexHistoryCoverageIsEstablished: Bool?

/// filePath -> file usage
var files: [String: CostUsageFileUsage] = [:]
Expand Down
87 changes: 65 additions & 22 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,11 @@ enum CostUsageScanner {
let parsedBytes: Int64
}

struct DailyReportLoadResult {
let report: CostUsageDailyReport
let historyCoverageIsEstablished: Bool
}

enum ClaudePathRole: String, Codable {
case parent
case subagent
Expand Down Expand Up @@ -1009,6 +1014,23 @@ enum CostUsageScanner {
now: Date = Date(),
options: Options = Options(),
checkCancellation: CancellationCheck?) throws -> CostUsageDailyReport
{
try self.loadDailyReportResultCancellable(
provider: provider,
since: since,
until: until,
now: now,
options: options,
checkCancellation: checkCancellation).report
}

static func loadDailyReportResultCancellable(
provider: UsageProvider,
since: Date,
until: Date,
now: Date = Date(),
options: Options = Options(),
checkCancellation: CancellationCheck?) throws -> DailyReportLoadResult
{
let range = CostUsageDayRange(since: since, until: until, calendar: options.calendar)
let emptyReport = CostUsageDailyReport(data: [], summary: nil)
Expand All @@ -1022,31 +1044,35 @@ enum CostUsageScanner {
options: options,
checkCancellation: checkCancellation)
case .claude:
return try self.loadClaudeDaily(
provider: .claude,
range: range,
now: now,
options: options,
checkCancellation: checkCancellation)
return try DailyReportLoadResult(
report: self.loadClaudeDaily(
provider: .claude,
range: range,
now: now,
options: options,
checkCancellation: checkCancellation),
historyCoverageIsEstablished: true)
case .vertexai:
var filtered = options
if filtered.claudeLogProviderFilter == .all {
filtered.claudeLogProviderFilter = .vertexAIOnly
}
return try self.loadClaudeDaily(
provider: .vertexai,
range: range,
now: now,
options: filtered,
checkCancellation: checkCancellation)
return try DailyReportLoadResult(
report: self.loadClaudeDaily(
provider: .vertexai,
range: range,
now: now,
options: filtered,
checkCancellation: checkCancellation),
historyCoverageIsEstablished: true)
case .openai, .azureopenai, .clinepass, .zai, .gemini, .antigravity, .cursor, .opencode, .opencodego, .alibaba,
.alibabatokenplan, .qwencloud, .factory,
.copilot, .devin, .minimax, .manus, .kilo, .kiro, .kimi, .moonshot, .augment, .jetbrains, .amp,
.ollama, .t3chat, .synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .sakana,
.abacus, .mistral, .deepseek, .deepinfra, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode,
.qoder, .stepfun, .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .neuralwatt,
.clawrouter, .longcat, .sub2api, .wayfinder, .zenmux, .aiand, .zoommate, .xai:
return emptyReport
return DailyReportLoadResult(report: emptyReport, historyCoverageIsEstablished: true)
}
}

Expand Down Expand Up @@ -3390,7 +3416,7 @@ enum CostUsageScanner {
range: CostUsageDayRange,
now: Date,
options: Options,
checkCancellation: CancellationCheck?) throws -> CostUsageDailyReport
checkCancellation: CancellationCheck?) throws -> DailyReportLoadResult
{
var cache = Self.loadCodexCache(options: options, range: range)
let nowMs = Int64(now.timeIntervalSince1970 * 1000)
Expand Down Expand Up @@ -3531,8 +3557,22 @@ enum CostUsageScanner {
}
}

let shouldRetainWiderWindow = !options.forceRescan && !plan.pricingChanged && !plan
.priorityMetadataChanged && !plan.needsTurnIDCacheMigration && !plan.needsProjectMetadataMigration
let historyCoverageIsEstablished =
scanBudget.resumedPartialFileCount == 0
&& scanBudget.deferredByBudgetFileCount == 0
&& !cache.files.values.contains {
$0.codexScanComplete == false
&& $0.touchesCodexScanWindow(
sinceKey: range.scanSinceKey,
untilKey: range.scanUntilKey)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep unfinished lookback files in the coverage check

When a recently modified session outside the requested date partitions is only partially scanned before reaching events in the requested window, its cached days do not touch this range. Subsequent refreshes neither repeat the cold-cache lookback nor select it through cachedCodexSessionFiles, and this predicate consequently ignores its codexScanComplete == false state; with no new per-refresh deferrals, the cache is then persisted as complete even though that file was never finished, so cost JSON can expose historyCoverageIsEstablished: true alongside partial totals. Track unfinished candidates independently of their parsed days or keep the lookback active while prior coverage is false.

Useful? React with 👍 / 👎.

}
let shouldRetainWiderWindow = !options.forceRescan
&& !plan.pricingChanged
&& !plan.priorityMetadataChanged
&& !plan.needsTurnIDCacheMigration
&& !plan.needsProjectMetadataMigration
&& cache.codexHistoryCoverageIsEstablished == true
&& historyCoverageIsEstablished
let retainedSinceKey = shouldRetainWiderWindow
? [cachedSinceKey, range.scanSinceKey].compactMap(\.self).min() ?? range.scanSinceKey
: range.scanSinceKey
Expand All @@ -3546,6 +3586,7 @@ enum CostUsageScanner {
cache.codexPricingKey = plan.codexPricingKey
cache.codexPriorityMetadataKey = plan.codexPriorityMetadataKey
cache.codexProjectMetadataVersion = Self.codexProjectMetadataVersion
cache.codexHistoryCoverageIsEstablished = historyCoverageIsEstablished
if plan.hasPriorityMetadata {
cache.codexPriorityTurnKeys = Self.mergePriorityTurnKeys(
existing: shouldRetainWiderWindow ? cache.codexPriorityTurnKeys : nil,
Expand All @@ -3565,12 +3606,14 @@ enum CostUsageScanner {
Self.saveCodexCache(cache, options: options, range: range)
}

return Self.buildCodexReportFromCache(
cache: cache,
range: range,
modelsDevCatalog: plan.modelsDevCatalog,
modelsDevCacheRoot: options.cacheRoot,
priorityTurns: plan.priorityTurns)
return DailyReportLoadResult(
report: Self.buildCodexReportFromCache(
cache: cache,
range: range,
modelsDevCatalog: plan.modelsDevCatalog,
modelsDevCacheRoot: options.cacheRoot,
priorityTurns: plan.priorityTurns),
historyCoverageIsEstablished: cache.codexHistoryCoverageIsEstablished == true)
}

private static func codexFileScanContext(
Expand Down
2 changes: 2 additions & 0 deletions Tests/CodexBarTests/CLICostTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ struct CLICostTests {
sessionCostUSD: 0.01,
last30DaysTokens: 40,
last30DaysCostUSD: 0.04,
historyCoverageIsEstablished: false,
daily: [
CostUsageDailyReport.Entry(
date: "2026-04-02",
Expand Down Expand Up @@ -232,6 +233,7 @@ struct CLICostTests {
}

#expect(json.contains("\"projects\""))
#expect(json.contains("\"historyCoverageIsEstablished\":false"))
#expect(json.contains("\"sources\""))
#expect(json.contains("\"name\":\"client-a\""))
#expect(json.contains("/work/client-a") || json.contains("\\/work\\/client-a"))
Expand Down
17 changes: 13 additions & 4 deletions Tests/CodexBarTests/CostUsagePerformanceGateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,18 @@ struct CostUsagePerformanceGateTests {
options.refreshMinIntervalSeconds = 0

var offsets: [Int64] = []
var coverage: [Bool] = []
var report: CostUsageDailyReport?
for _ in 0..<12 {
report = CostUsageScanner.loadDailyReport(
let result = try CostUsageScanner.loadDailyReportResultCancellable(
provider: .codex,
since: day,
until: day,
now: day,
options: options)
options: options,
checkCancellation: nil)
report = result.report
coverage.append(result.historyCoverageIsEstablished)
let cached = try #require(CostUsageCacheIO.load(
provider: .codex,
cacheRoot: env.cacheRoot).files.values.first)
Expand All @@ -337,6 +341,8 @@ struct CostUsagePerformanceGateTests {
}

#expect(offsets.count > 1)
#expect(coverage.first == false)
#expect(coverage.last == true)
#expect(zip(offsets, offsets.dropFirst()).allSatisfy { $0 <= $1 })
#expect(offsets.last == metadata.size)
#expect(report?.summary?.totalTokens == baseline.summary?.totalTokens)
Expand Down Expand Up @@ -561,15 +567,18 @@ struct CostUsagePerformanceGateTests {
preferNewestCodexSessionsFirst: true)
options.refreshMinIntervalSeconds = 0

_ = CostUsageScanner.loadDailyReport(
let result = try CostUsageScanner.loadDailyReportResultCancellable(
provider: .codex,
since: day,
until: day,
now: day,
options: options)
options: options,
checkCancellation: nil)
let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
let cachedNames = Set(cache.files.keys.map { URL(fileURLWithPath: $0).lastPathComponent })

#expect(!result.historyCoverageIsEstablished)
#expect(cache.codexHistoryCoverageIsEstablished == false)
#expect(cachedNames.contains(newer.lastPathComponent))
#expect(!cachedNames.contains(older.lastPathComponent))
}
Expand Down
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ payloads include the visible account label in `account`.
- `provider`, `source` (`local` for Claude/Codex log scans, `web` for Cursor dashboard data), `updatedAt`
- `sessionTokens`, `sessionCostUSD`
- `last30DaysTokens`, `last30DaysCostUSD`
- `historyCoverageIsEstablished`: `false` while a bounded Codex scan still has deferred history
- Cursor only: `meteredCostUSD` — what Cursor's plan actually deducts over the window, alongside the API-rate estimate in `last30DaysCostUSD`.
- `daily[]`: `date`, `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheCreationTokens`, `totalTokens`, `totalCost`, `modelsUsed`, `modelBreakdowns[]` (`modelName`, `cost`)
- Codex only: `projects[]`: `name`, `path`, `totalTokens`, `totalCost`, `daily[]`, `modelBreakdowns[]`, `sources[]`
Expand Down