From 70f98b4aac6206008583330112b258ad3d5fc6bd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 05:47:04 -0700 Subject: [PATCH 01/10] fix: preserve Codex request-tier pricing across forks --- CHANGELOG.md | 1 + .../Generated/CodexParserHash.generated.swift | 2 +- .../Vendored/CostUsage/CostUsagePricing.swift | 106 +++--- .../CostUsageScanner+CacheHelpers.swift | 45 ++- ...ostUsageScanner+ReportReconciliation.swift | 105 ++++++ .../CostUsagePerformanceGateTests.swift | 3 +- .../CodexBarTests/CostUsagePricingTests.swift | 44 +++ .../CostUsageScannerForkSplitTests.swift | 323 +++++++++++++++++- .../ProviderArchitectureGatekeeperTests.swift | 4 +- 9 files changed, 546 insertions(+), 87 deletions(-) create mode 100644 Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 62141b59d0..d995c01f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.49.3 — Unreleased ### Fixed +- Codex: preserve request-level pricing tiers while reconciling forked usage, preventing day aggregates from triggering long-context rates (#2858). Thanks @thomaschow19! - CLI: stop standalone version lookup from walking past the filesystem root and hanging with unbounded memory on affected macOS versions (#2856). Thanks @Manwholikespie! - Cost store: prevent launch-time executor-assumption crashes on macOS 15 by keeping synchronous SQLite cache bridges on their validated serial queue (#2857). Thanks @Manwholikespie! diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index b901ff1ba8..856145060c 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "b975eb705f905b9a" + static let value = "73039569b15802bf" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift index 1bd0c5e624..31c9b1b0a2 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift @@ -493,6 +493,47 @@ enum CostUsagePricing { cacheWriteInputTokens: Int = 0, modelsDevCatalog: ModelsDevCatalog? = nil, modelsDevCacheRoot: URL? = nil) -> Double? + { + guard let pricing = self.resolvedCodexPricing( + model: model, + modelsDevCatalog: modelsDevCatalog, + modelsDevCacheRoot: modelsDevCacheRoot) + else { return nil } + return self.codexCostUSD( + pricing: pricing, + inputTokens: inputTokens, + cachedInputTokens: cachedInputTokens, + cacheWriteInputTokens: cacheWriteInputTokens, + outputTokens: outputTokens) + } + + static func codexAggregateCostUSD( + model: String, + inputTokens: Int, + cachedInputTokens: Int, + outputTokens: Int, + cacheWriteInputTokens: Int = 0, + modelsDevCatalog: ModelsDevCatalog? = nil, + modelsDevCacheRoot: URL? = nil) -> Double? + { + guard let pricing = self.resolvedCodexPricing( + model: model, + modelsDevCatalog: modelsDevCatalog, + modelsDevCacheRoot: modelsDevCacheRoot), + pricing.thresholdTokens == nil + else { return nil } + return self.codexCostUSD( + pricing: pricing, + inputTokens: inputTokens, + cachedInputTokens: cachedInputTokens, + cacheWriteInputTokens: cacheWriteInputTokens, + outputTokens: outputTokens) + } + + private static func resolvedCodexPricing( + model: String, + modelsDevCatalog: ModelsDevCatalog?, + modelsDevCacheRoot: URL?) -> CodexPricing? { let key = self.normalizeCodexModel(model) guard key != self.codexUnattributedModel else { return nil } @@ -524,32 +565,25 @@ enum CostUsagePricing { ?? lookup.pricing.inputCostPerTokenAboveThreshold ?? lookup.pricing.inputCostPerToken : bundledLongContext?.cacheWriteInputCostPerTokenAboveThreshold) - return self.codexCostUSD( - pricing: lookup.pricing, + return CodexPricing( + inputCostPerToken: lookup.pricing.inputCostPerToken, + outputCostPerToken: lookup.pricing.outputCostPerToken, + cacheReadInputCostPerToken: lookup.pricing.cacheReadInputCostPerToken + ?? bundled?.cacheReadInputCostPerToken, + displayLabel: nil, + cacheWriteInputCostPerToken: lookup.pricing.cacheCreationInputCostPerToken + ?? bundled?.cacheWriteInputCostPerToken, thresholdTokens: bundled?.thresholdTokens ?? lookup.pricing.thresholdTokens, inputCostPerTokenAboveThreshold: lookup.pricing.inputCostPerTokenAboveThreshold ?? bundledLongContext?.inputCostPerTokenAboveThreshold, outputCostPerTokenAboveThreshold: lookup.pricing.outputCostPerTokenAboveThreshold ?? bundledLongContext?.outputCostPerTokenAboveThreshold, - cacheReadInputCostPerToken: lookup.pricing.cacheReadInputCostPerToken - ?? bundled?.cacheReadInputCostPerToken, cacheReadInputCostPerTokenAboveThreshold: cacheReadAboveThreshold, - cacheWriteInputCostPerToken: lookup.pricing.cacheCreationInputCostPerToken - ?? bundled?.cacheWriteInputCostPerToken, - cacheWriteInputCostPerTokenAboveThreshold: cacheWriteAboveThreshold, - inputTokens: inputTokens, - cachedInputTokens: cachedInputTokens, - cacheWriteInputTokens: cacheWriteInputTokens, - outputTokens: outputTokens) + cacheWriteInputCostPerTokenAboveThreshold: cacheWriteAboveThreshold) } guard let pricing = self.codex[key] else { return nil } - return self.codexCostUSD( - pricing: pricing, - inputTokens: inputTokens, - cachedInputTokens: cachedInputTokens, - cacheWriteInputTokens: cacheWriteInputTokens, - outputTokens: outputTokens) + return pricing } static func codexPriorityCostUSD( @@ -628,44 +662,6 @@ enum CostUsagePricing { + (Double(max(0, outputTokens)) * outputRate) } - private static func codexCostUSD( - pricing: ModelsDevPricingInfo, - thresholdTokens: Int? = nil, - inputCostPerTokenAboveThreshold: Double? = nil, - outputCostPerTokenAboveThreshold: Double? = nil, - cacheReadInputCostPerToken: Double? = nil, - cacheReadInputCostPerTokenAboveThreshold: Double? = nil, - cacheWriteInputCostPerToken: Double? = nil, - cacheWriteInputCostPerTokenAboveThreshold: Double? = nil, - inputTokens: Int, - cachedInputTokens: Int, - cacheWriteInputTokens: Int = 0, - outputTokens: Int) -> Double - { - self.codexCostUSD( - pricing: CodexPricing( - inputCostPerToken: pricing.inputCostPerToken, - outputCostPerToken: pricing.outputCostPerToken, - cacheReadInputCostPerToken: cacheReadInputCostPerToken - ?? pricing.cacheReadInputCostPerToken, - displayLabel: nil, - cacheWriteInputCostPerToken: cacheWriteInputCostPerToken - ?? pricing.cacheCreationInputCostPerToken, - thresholdTokens: thresholdTokens ?? pricing.thresholdTokens, - inputCostPerTokenAboveThreshold: inputCostPerTokenAboveThreshold - ?? pricing.inputCostPerTokenAboveThreshold, - outputCostPerTokenAboveThreshold: outputCostPerTokenAboveThreshold - ?? pricing.outputCostPerTokenAboveThreshold, - cacheReadInputCostPerTokenAboveThreshold: cacheReadInputCostPerTokenAboveThreshold - ?? pricing.cacheReadInputCostPerTokenAboveThreshold, - cacheWriteInputCostPerTokenAboveThreshold: cacheWriteInputCostPerTokenAboveThreshold - ?? pricing.cacheCreationInputCostPerTokenAboveThreshold), - inputTokens: inputTokens, - cachedInputTokens: cachedInputTokens, - cacheWriteInputTokens: cacheWriteInputTokens, - outputTokens: outputTokens) - } - static func claudeCostUSD( model: String, inputTokens: Int, diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index d471dc018a..335152eb6e 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -110,6 +110,8 @@ extension CostUsageScanner { var priorityTokens: Int = 0 var sawStandardCost = false var sawPriorityCost = false + var hasUnstableTokenRows = false + var hasTokenOverflow = false var optionalStandardCostUSD: Double? { self.sawStandardCost ? self.standardCostUSD : nil @@ -138,7 +140,10 @@ extension CostUsageScanner { func isTrusted(canonicalTotalTokens: Int) -> Bool { let (rowTokenTotal, overflow) = self.standardTokens.addingReportingOverflow(self.priorityTokens) - return !overflow && rowTokenTotal <= canonicalTotalTokens + return !self.hasUnstableTokenRows + && !self.hasTokenOverflow + && !overflow + && rowTokenTotal == canonicalTotalTokens } } @@ -150,13 +155,23 @@ extension CostUsageScanner { { var breakdown = CodexRowCostBreakdown() for row in rows { - let tokenCount = row.input + row.output + let (tokenCount, tokenOverflow) = max(0, row.input).addingReportingOverflow(max(0, row.output)) + if tokenOverflow { + breakdown.hasTokenOverflow = true + } + if row.input > 0 || row.cached > 0 || row.output > 0, row.eventIndex == nil { + breakdown.hasUnstableTokenRows = true + } let priorityMetadata = row.turnID.flatMap { priorityTurns[$0] } let isPriority = priorityMetadata != nil || row.pricingMode == "priority" if isPriority { - breakdown.priorityTokens += tokenCount + let (total, overflow) = breakdown.priorityTokens.addingReportingOverflow(tokenCount) + breakdown.priorityTokens = overflow ? breakdown.priorityTokens : total + breakdown.hasTokenOverflow = breakdown.hasTokenOverflow || overflow } else { - breakdown.standardTokens += tokenCount + let (total, overflow) = breakdown.standardTokens.addingReportingOverflow(tokenCount) + breakdown.standardTokens = overflow ? breakdown.standardTokens : total + breakdown.hasTokenOverflow = breakdown.hasTokenOverflow || overflow } guard let cost = self.codexResolvedCostUSD( for: row, @@ -1377,10 +1392,12 @@ extension CostUsageScanner { var (totalCost, costSeen) = (0.0, false) let dayKeys = self.codexReportDayKeys(cache: reportCache, range: range) - let authoritativeCostNanosByDayModel = self.codexCostNanosByDayModel(cache: reportCache, range: range) var rowsByDayModel: [String: [String: [CodexUsageRow]]] = [:] + var unresolvedRowGroups = Set() for usage in reportCache.files.values { - for row in self.codexRowsForReadTimePricing(usage) where CostUsageDayRange.isInRange( + let reconciled = self.codexCanonicalPricingRows(usage) + unresolvedRowGroups.formUnion(reconciled.unresolvedGroups) + for row in reconciled.rows where CostUsageDayRange.isInRange( dayKey: row.day, since: range.sinceKey, until: range.untilKey) @@ -1417,23 +1434,19 @@ extension CostUsageScanner { priorityTurns: priorityTurns, modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader), modelsDevCacheRoot: modelsDevCacheRoot) - let rowCostIsTrusted = rowCost?.isTrusted(canonicalTotalTokens: totalTokens) ?? true - let authoritativeCost = authoritativeCostNanosByDayModel[day]?[model].map { - Double($0) / Self.costScale - } - let canonicalCost = CostUsagePricing.codexCostUSD( + let group = CodexDayModelKey(day: day, model: model) + let rowCostIsTrusted = !unresolvedRowGroups.contains(group) + && rowCost?.isTrusted(canonicalTotalTokens: totalTokens) == true + let aggregateCost = CostUsagePricing.codexAggregateCostUSD( model: model, inputTokens: input, cachedInputTokens: cached, outputTokens: output, modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader), modelsDevCacheRoot: modelsDevCacheRoot) - // Physical pricing rows can retain fork-copied usage after canonical ownership - // has deduplicated the day/model totals. Reject the whole row-derived price so - // Fast uplift from the same unowned rows cannot leak into the fallback cost. let cost = rowCostIsTrusted - ? rowCost?.totalCostUSD ?? authoritativeCost ?? canonicalCost - : canonicalCost + ? rowCost?.totalCostUSD ?? aggregateCost + : aggregateCost let hasModeSplit = rowCostIsTrusted && rowCost?.hasModeSplit == true breakdown.append( CostUsageDailyReport.ModelBreakdown( diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift new file mode 100644 index 0000000000..256d7c1739 --- /dev/null +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift @@ -0,0 +1,105 @@ +import Foundation + +extension CostUsageScanner { + struct CodexDayModelKey: Hashable { + let day: String + let model: String + } + + struct CodexCanonicalPricingRows { + let rows: [CodexUsageRow] + let unresolvedGroups: Set + } + + static func codexCanonicalPricingRows(_ usage: CostUsageFileUsage) -> CodexCanonicalPricingRows { + let persistedRows = usage.codexRows ?? [] + let rowsByGroup = Dictionary(grouping: persistedRows) { + CodexDayModelKey(day: $0.day, model: $0.model) + } + var canonicalRows: [CodexUsageRow] = [] + var unresolvedGroups = Set() + + for day in usage.days.keys.sorted() { + guard let models = usage.days[day] else { continue } + for model in models.keys.sorted() { + let key = CodexDayModelKey(day: day, model: model) + let packed = models[model] ?? [] + let target = CodexRowTokenTotals( + input: max(0, packed[safe: 0] ?? 0), + cached: max(0, packed[safe: 1] ?? 0), + output: max(0, packed[safe: 2] ?? 0)) + guard let rows = self.reconciledCodexPricingRows( + rowsByGroup[key] ?? [], + target: target) + else { + unresolvedGroups.insert(key) + continue + } + canonicalRows.append(contentsOf: rows) + } + } + + return CodexCanonicalPricingRows(rows: canonicalRows, unresolvedGroups: unresolvedGroups) + } + + private struct CodexRowTokenTotals: Equatable { + var input: Int = 0 + var cached: Int = 0 + var output: Int = 0 + + mutating func add(_ row: CodexUsageRow) -> Bool { + guard let input = Self.sum(self.input, max(0, row.input)), + let cached = Self.sum(self.cached, max(0, row.cached)), + let output = Self.sum(self.output, max(0, row.output)) + else { return false } + self = CodexRowTokenTotals(input: input, cached: cached, output: output) + return true + } + + func exceeds(_ other: CodexRowTokenTotals) -> Bool { + self.input > other.input || self.cached > other.cached || self.output > other.output + } + + private static func sum(_ lhs: Int, _ rhs: Int) -> Int? { + let (sum, overflow) = lhs.addingReportingOverflow(rhs) + return overflow ? nil : sum + } + } + + private static func reconciledCodexPricingRows( + _ rows: [CodexUsageRow], + target: CodexRowTokenTotals) -> [CodexUsageRow]? + { + var allRowsTotal = CodexRowTokenTotals() + guard rows.allSatisfy({ allRowsTotal.add($0) }) else { return nil } + if allRowsTotal == target { + return rows + } + guard allRowsTotal.exceeds(target) else { return nil } + + let chronological = rows.enumerated().sorted { lhs, rhs in + let lhsTimestamp = lhs.element.timestampUnixMs ?? Int64.min + let rhsTimestamp = rhs.element.timestampUnixMs ?? Int64.min + if lhsTimestamp != rhsTimestamp { + return lhsTimestamp < rhsTimestamp + } + let lhsEventIndex = lhs.element.eventIndex ?? Int.min + let rhsEventIndex = rhs.element.eventIndex ?? Int.min + if lhsEventIndex != rhsEventIndex { + return lhsEventIndex < rhsEventIndex + } + return lhs.offset < rhs.offset + } + var suffixTotal = CodexRowTokenTotals() + for index in chronological.indices.reversed() { + guard suffixTotal.add(chronological[index].element) else { return nil } + if suffixTotal == target { + return chronological[index...].map(\.element) + } + if suffixTotal.exceeds(target) { + return nil + } + } + return nil + } +} diff --git a/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift b/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift index 9919ff5970..ae33c7ce4e 100644 --- a/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift +++ b/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift @@ -334,7 +334,8 @@ struct CostUsagePerformanceGateTests { mixed.files[rowlessPath]?.codexRows = nil let mixedBackfilled = CostUsageScanner.buildCodexReportFromCache(cache: mixed, range: range) - #expect(abs((mixedBackfilled.summary?.totalCostUSD ?? 0) - (scanned.summary?.totalCostUSD ?? 0)) < 0.000000001) + #expect(mixedBackfilled.summary?.totalTokens == scanned.summary?.totalTokens) + #expect(mixedBackfilled.summary?.totalCostUSD == nil) let aggregateCost = CostUsagePricing.codexCostUSD( model: "gpt-5.5", diff --git a/Tests/CodexBarTests/CostUsagePricingTests.swift b/Tests/CodexBarTests/CostUsagePricingTests.swift index 934a171af7..82363225f8 100644 --- a/Tests/CodexBarTests/CostUsagePricingTests.swift +++ b/Tests/CodexBarTests/CostUsagePricingTests.swift @@ -616,6 +616,50 @@ struct CostUsagePricingTests { #expect(aboveBoundary == (272_001.0 * 1e-5) + (10.0 * 4.5e-5)) } + @Test + func `codex aggregate pricing rejects effective thresholds and keeps linear rates`() throws { + let emptyRoot = try Self.cacheRoot() + let bundledThreshold = CostUsagePricing.codexAggregateCostUSD( + model: "gpt-5.6-sol", + inputTokens: 400_000, + cachedInputTokens: 0, + outputTokens: 100, + modelsDevCacheRoot: emptyRoot) + let linear = CostUsagePricing.codexAggregateCostUSD( + model: "gpt-5.4-mini", + inputTokens: 400_000, + cachedInputTokens: 100_000, + outputTokens: 100, + modelsDevCacheRoot: emptyRoot) + let catalogThresholdRoot = try Self.seedModelsDevCache(""" + { + "openai": { + "id": "openai", + "models": { + "aggregate-threshold-model": { + "id": "aggregate-threshold-model", + "cost": { + "input": 5, + "output": 30, + "context_over_200k": { "input": 10, "output": 45 } + } + } + } + } + } + """) + let catalogThreshold = CostUsagePricing.codexAggregateCostUSD( + model: "aggregate-threshold-model", + inputTokens: 400_000, + cachedInputTokens: 0, + outputTokens: 100, + modelsDevCacheRoot: catalogThresholdRoot) + + #expect(bundledThreshold == nil) + #expect(linear == (300_000.0 * 7.5e-7) + (100_000.0 * 7.5e-8) + (100.0 * 4.5e-6)) + #expect(catalogThreshold == nil) + } + @Test func `codex models dev cached fallback uses long context input rate when cache read is absent`() throws { let root = try Self.seedModelsDevCache(""" diff --git a/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift b/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift index 03342ddd5e..ef0f76b802 100644 --- a/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift +++ b/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift @@ -5,7 +5,301 @@ import Testing struct CostUsageScannerForkSplitTests { @Test - func `codex report rejects fork inflated row split and its fast uplift`() throws { + func `codex report preserves short request pricing after copied fork prefix`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.6-sol" + let projectPath = "/tmp/codexbar-fork-tier-project" + let parentRow = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "parent-turn", + eventIndex: 0, + timestampUnixMs: Int64(day.timeIntervalSince1970 * 1000), + input: 200_000, + cached: 0, + output: 100) + let childRow = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "child-turn", + eventIndex: 1, + timestampUnixMs: Int64(day.addingTimeInterval(1).timeIntervalSince1970 * 1000), + input: 200_000, + cached: 0, + output: 100) + let parentUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: parentRow.timestampUnixMs ?? 0, + size: 1, + days: [dayKey: [model: [200_000, 0, 100]]], + parsedBytes: 1, + sessionId: "parent-session", + projectPath: projectPath, + canonicalProjectPath: projectPath, + codexRows: [parentRow], + codexScanComplete: true) + let childUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: childRow.timestampUnixMs ?? 0, + size: 1, + days: [dayKey: [model: [200_000, 0, 100]]], + parsedBytes: 1, + sessionId: "child-session", + forkedFromId: "parent-session", + projectPath: projectPath, + canonicalProjectPath: projectPath, + codexRows: [parentRow, childRow], + codexScanComplete: true) + var cache = CostUsageCache() + cache.files = ["/parent.jsonl": parentUsage, "/child.jsonl": childUsage] + cache.days = [dayKey: [model: [400_000, 0, 200]]] + cache.scanSinceKey = dayKey + cache.scanUntilKey = dayKey + cache.timeZoneIdentifier = range.calendar.timeZone.identifier + + let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + let requestCost = try #require(CostUsagePricing.codexCostUSD( + model: model, + inputTokens: 200_000, + cachedInputTokens: 0, + outputTokens: 100)) + let aggregateCost = try #require(CostUsagePricing.codexCostUSD( + model: model, + inputTokens: 400_000, + cachedInputTokens: 0, + outputTokens: 200)) + let reportCost = try #require(report.summary?.totalCostUSD) + let requestCostSum = requestCost * 2 + + #expect(abs(reportCost - requestCostSum) < 1e-12) + #expect(aggregateCost > requestCostSum * 1.9) + #expect(abs(reportCost - aggregateCost) > 0.9) + + let projects = CostUsageScanner.buildCodexProjectBreakdownsFromCache(cache: cache, range: range) + let sessions = CostUsageScanner.buildCodexSessionBreakdownsFromCache(cache: cache, range: range) + #expect(abs(projects.compactMap(\.totalCostUSD).reduce(0, +) - reportCost) < 1e-12) + #expect(abs(sessions.compactMap(\.costUSD).reduce(0, +) - reportCost) < 1e-12) + + _ = CostUsageStoreAccess.replace( + cacheRoot: environment.cacheRoot, + cache: cache, + calendar: range.calendar) + let restored = CostUsageStoreAccess.read(cacheRoot: environment.cacheRoot, calendar: range.calendar) + let warmReport = CostUsageScanner.buildCodexReportFromCache(cache: restored, range: range) + #expect(restored.files.values.flatMap { $0.codexRows ?? [] }.count == 3) + #expect(warmReport.data == report.data) + #expect(warmReport.summary == report.summary) + } + + @Test + func `codex report applies long context pricing only to the genuine long request`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.6-sol" + let timestamp = Int64(day.timeIntervalSince1970 * 1000) + let longRow = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "long-turn", + eventIndex: 0, + timestampUnixMs: timestamp, + input: 300_000, + cached: 0, + output: 100, + reasoning: 50, + pricingModel: model, + pricingMode: "standard") + let shortRow = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "short-turn", + eventIndex: 1, + timestampUnixMs: timestamp + 1, + input: 100_000, + cached: 0, + output: 100, + reasoning: 25, + pricingModel: model, + pricingMode: "standard") + let trailingZeroRow = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "short-turn", + eventIndex: 2, + timestampUnixMs: timestamp + 2, + input: 0, + cached: 0, + output: 0, + reasoning: 0, + pricingModel: model, + pricingMode: "standard") + let parentUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: timestamp, + size: 1, + days: [dayKey: [model: [300_000, 0, 100]]], + parsedBytes: 1, + sessionId: "long-session", + codexRows: [longRow], + codexScanComplete: true) + let childUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: timestamp + 2, + size: 1, + days: [dayKey: [model: [100_000, 0, 100]]], + parsedBytes: 1, + sessionId: "short-session", + forkedFromId: "long-session", + codexRows: [longRow, shortRow, trailingZeroRow], + codexScanComplete: true) + var cache = CostUsageCache() + cache.files = ["/long.jsonl": parentUsage, "/short.jsonl": childUsage] + cache.days = [dayKey: [model: [400_000, 0, 200]]] + + let reconciledChild = CostUsageScanner.codexCanonicalPricingRows(childUsage) + #expect(reconciledChild.unresolvedGroups.isEmpty) + #expect(reconciledChild.rows == [shortRow, trailingZeroRow]) + + let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + let longCost = try #require(CostUsagePricing.codexCostUSD( + model: model, + inputTokens: 300_000, + cachedInputTokens: 0, + outputTokens: 100)) + let shortCost = try #require(CostUsagePricing.codexCostUSD( + model: model, + inputTokens: 100_000, + cachedInputTokens: 0, + outputTokens: 100)) + let aggregateCost = try #require(CostUsagePricing.codexCostUSD( + model: model, + inputTokens: 400_000, + cachedInputTokens: 0, + outputTokens: 200)) + + #expect(abs((report.summary?.totalCostUSD ?? 0) - (longCost + shortCost)) < 1e-12) + #expect(abs((report.summary?.totalCostUSD ?? 0) - aggregateCost) > 0.4) + } + + @Test + func `codex report leaves threshold cost unavailable without exact request rows`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.6-sol" + func row(index: Int, input: Int) -> CostUsageScanner.CodexUsageRow { + CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "turn-\(index)", + eventIndex: index, + timestampUnixMs: Int64(index), + input: input, + cached: 0, + output: 0, + knownCostNanos: 42_000_000_000) + } + + var usage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 2, + size: 1, + days: [dayKey: [model: [200_000, 0, 0]]], + parsedBytes: 1, + sessionId: "irreconcilable", + codexCostNanos: [dayKey: [model: 84_000_000_000]], + codexRows: [row(index: 0, input: 120_000), row(index: 1, input: 120_000)], + codexScanComplete: true) + var cache = CostUsageCache() + cache.files = ["/irreconcilable.jsonl": usage] + cache.days = usage.days + + let irreconcilable = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + #expect(irreconcilable.summary?.totalTokens == 200_000) + #expect(irreconcilable.summary?.totalCostUSD == nil) + + usage.codexRows = nil + cache.files = ["/aggregate-only.jsonl": usage] + let aggregateOnly = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + #expect(aggregateOnly.summary?.totalTokens == 200_000) + #expect(aggregateOnly.summary?.totalCostUSD == nil) + } + + @Test + func `codex report safely prices linear aggregate fallback`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.4-mini" + let usage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 1, + size: 1, + days: [dayKey: [model: [400_000, 100_000, 100]]], + parsedBytes: 1, + sessionId: "linear-aggregate", + codexRows: nil, + codexScanComplete: true) + var cache = CostUsageCache() + cache.files = ["/linear.jsonl": usage] + cache.days = usage.days + + let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + let expected = try #require(CostUsagePricing.codexAggregateCostUSD( + model: model, + inputTokens: 400_000, + cachedInputTokens: 100_000, + outputTokens: 100)) + + #expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 1e-12) + } + + @Test + func `exact codex pricing rows retain persisted order`() { + let dayKey = "2026-08-11" + let model = "gpt-5.6-sol" + let later = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "later", + eventIndex: 1, + timestampUnixMs: 2, + input: 20, + cached: 2, + output: 2) + let earlier = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "earlier", + eventIndex: 0, + timestampUnixMs: 1, + input: 10, + cached: 1, + output: 1) + let usage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 2, + size: 1, + days: [dayKey: [model: [30, 3, 3]]], + parsedBytes: 1, + codexRows: [later, earlier], + codexScanComplete: true) + + let reconciled = CostUsageScanner.codexCanonicalPricingRows(usage) + #expect(reconciled.unresolvedGroups.isEmpty) + #expect(reconciled.rows == [later, earlier]) + } + + @Test + func `codex report reconciles copied fork prefix without losing fast split`() throws { let fixture = try self.makeFixture() defer { fixture.environment.cleanup() } @@ -27,18 +321,23 @@ struct CostUsageScannerForkSplitTests { let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: fixture.range) let breakdown = try #require(report.data.first?.modelBreakdowns?.first) - let canonicalCost = try #require(CostUsagePricing.codexCostUSD( + let standardCost = try #require(CostUsagePricing.codexCostUSD( + model: fixture.model, + inputTokens: 50, + cachedInputTokens: 20, + outputTokens: 5)) + let priorityCost = try #require(CostUsagePricing.codexPriorityCostUSD( model: fixture.model, - inputTokens: canonical[0], - cachedInputTokens: canonical[1], - outputTokens: canonical[2])) - - #expect(abs((breakdown.costUSD ?? 0) - canonicalCost) < 1e-12) - #expect(breakdown.standardCostUSD == nil) - #expect(breakdown.priorityCostUSD == nil) - #expect(breakdown.standardTokens == nil) - #expect(breakdown.priorityTokens == nil) - #expect(abs((report.summary?.totalCostUSD ?? 0) - canonicalCost) < 1e-12) + inputTokens: 100, + cachedInputTokens: 40, + outputTokens: 10)) + + #expect(abs((breakdown.costUSD ?? 0) - (standardCost + priorityCost)) < 1e-12) + #expect(abs((breakdown.standardCostUSD ?? 0) - standardCost) < 1e-12) + #expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 1e-12) + #expect(breakdown.standardTokens == 55) + #expect(breakdown.priorityTokens == 110) + #expect(abs((report.summary?.totalCostUSD ?? 0) - (standardCost + priorityCost)) < 1e-12) } @Test diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index c9ca73a5ce..ccb061b7ec 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -3638,7 +3638,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift", - line: 546, + line: 585, anchor: "guard let pricing = self.codex[key] else { return nil }", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -3646,7 +3646,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift", - line: 708, + line: 704, anchor: "guard let pricing = self.claude[key] else { return nil }", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, From 3c4c624ba0412031da73591947f1f10bc7683ba0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 05:54:42 -0700 Subject: [PATCH 02/10] fix: keep unresolved Codex pricing conservative --- .../Generated/CodexParserHash.generated.swift | 2 +- .../CostUsageScanner+CacheHelpers.swift | 27 ++++++++++++++----- ...ostUsageScanner+ReportReconciliation.swift | 2 ++ .../CostUsageScannerForkSplitTests.swift | 24 +++++++++++++++++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 856145060c..69cc6e9424 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "73039569b15802bf" + static let value = "3d0b43c94de6a1c6" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index 335152eb6e..96f5ba3b2e 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -1394,9 +1394,20 @@ extension CostUsageScanner { let dayKeys = self.codexReportDayKeys(cache: reportCache, range: range) var rowsByDayModel: [String: [String: [CodexUsageRow]]] = [:] var unresolvedRowGroups = Set() + var priorityEvidenceGroups = Set() for usage in reportCache.files.values { let reconciled = self.codexCanonicalPricingRows(usage) unresolvedRowGroups.formUnion(reconciled.unresolvedGroups) + for (day, models) in usage.codexPriorityTokens ?? [:] { + for (model, tokens) in models where tokens > 0 { + priorityEvidenceGroups.insert(CodexDayModelKey(day: day, model: model)) + } + } + for row in usage.codexRows ?? [] where row.pricingMode == "priority" + || row.turnID.flatMap({ priorityTurns[$0] }) != nil + { + priorityEvidenceGroups.insert(CodexDayModelKey(day: row.day, model: row.model)) + } for row in reconciled.rows where CostUsageDayRange.isInRange( dayKey: row.day, since: range.sinceKey, @@ -1437,13 +1448,15 @@ extension CostUsageScanner { let group = CodexDayModelKey(day: day, model: model) let rowCostIsTrusted = !unresolvedRowGroups.contains(group) && rowCost?.isTrusted(canonicalTotalTokens: totalTokens) == true - let aggregateCost = CostUsagePricing.codexAggregateCostUSD( - model: model, - inputTokens: input, - cachedInputTokens: cached, - outputTokens: output, - modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader), - modelsDevCacheRoot: modelsDevCacheRoot) + let aggregateCost = priorityEvidenceGroups.contains(group) + ? nil + : CostUsagePricing.codexAggregateCostUSD( + model: model, + inputTokens: input, + cachedInputTokens: cached, + outputTokens: output, + modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader), + modelsDevCacheRoot: modelsDevCacheRoot) let cost = rowCostIsTrusted ? rowCost?.totalCostUSD ?? aggregateCost : aggregateCost diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift index 256d7c1739..bf0857e86b 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift @@ -76,6 +76,8 @@ extension CostUsageScanner { return rows } guard allRowsTotal.exceeds(target) else { return nil } + let timestampPresence = Set(rows.map { $0.timestampUnixMs != nil }) + guard timestampPresence.count <= 1 else { return nil } let chronological = rows.enumerated().sorted { lhs, rhs in let lhsTimestamp = lhs.element.timestampUnixMs ?? Int64.min diff --git a/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift b/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift index ef0f76b802..c051b3c663 100644 --- a/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift +++ b/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift @@ -230,6 +230,23 @@ struct CostUsageScannerForkSplitTests { let aggregateOnly = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) #expect(aggregateOnly.summary?.totalTokens == 200_000) #expect(aggregateOnly.summary?.totalCostUSD == nil) + + let timestampedParent = row(index: 0, input: 200_000) + let untimestampedChild = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "mixed-timestamp-child", + eventIndex: 1, + timestampUnixMs: nil, + input: 200_000, + cached: 0, + output: 0) + usage.codexRows = [timestampedParent, untimestampedChild] + let mixedTimestamps = CostUsageScanner.codexCanonicalPricingRows(usage) + #expect(mixedTimestamps.rows.isEmpty) + #expect(mixedTimestamps.unresolvedGroups == [ + CostUsageScanner.CodexDayModelKey(day: dayKey, model: model), + ]) } @Test @@ -261,6 +278,13 @@ struct CostUsageScannerForkSplitTests { outputTokens: 100)) #expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 1e-12) + + var priorityUsage = usage + priorityUsage.codexPriorityTokens = [dayKey: [model: 400_100]] + cache.files = ["/linear-priority.jsonl": priorityUsage] + let priorityAggregate = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + #expect(priorityAggregate.summary?.totalTokens == 400_100) + #expect(priorityAggregate.summary?.totalCostUSD == nil) } @Test From 2b773e990a146531ba9a931f7b82a3429ab6b5eb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 06:10:46 -0700 Subject: [PATCH 03/10] fix: validate canonical Codex mode ownership --- .../Generated/CodexParserHash.generated.swift | 2 +- .../CostUsageScanner+CacheHelpers.swift | 19 +++---- ...ostUsageScanner+ReportReconciliation.swift | 56 +++++++++++++++++++ .../CostUsageScannerForkSplitTests.swift | 14 +++++ 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 69cc6e9424..b65c26ed78 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "3d0b43c94de6a1c6" + static let value = "8717fd480fd7d00f" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index 96f5ba3b2e..a632697147 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -1394,20 +1394,18 @@ extension CostUsageScanner { let dayKeys = self.codexReportDayKeys(cache: reportCache, range: range) var rowsByDayModel: [String: [String: [CodexUsageRow]]] = [:] var unresolvedRowGroups = Set() + var modeOwnershipMismatchGroups = Set() var priorityEvidenceGroups = Set() for usage in reportCache.files.values { let reconciled = self.codexCanonicalPricingRows(usage) unresolvedRowGroups.formUnion(reconciled.unresolvedGroups) - for (day, models) in usage.codexPriorityTokens ?? [:] { - for (model, tokens) in models where tokens > 0 { - priorityEvidenceGroups.insert(CodexDayModelKey(day: day, model: model)) - } - } - for row in usage.codexRows ?? [] where row.pricingMode == "priority" - || row.turnID.flatMap({ priorityTurns[$0] }) != nil - { - priorityEvidenceGroups.insert(CodexDayModelKey(day: row.day, model: row.model)) - } + let modeEvidence = self.codexPricingModeEvidence( + usage: usage, + reconciledRows: reconciled.rows, + range: range, + priorityTurns: priorityTurns) + modeOwnershipMismatchGroups.formUnion(modeEvidence.mismatchGroups) + priorityEvidenceGroups.formUnion(modeEvidence.priorityGroups) for row in reconciled.rows where CostUsageDayRange.isInRange( dayKey: row.day, since: range.sinceKey, @@ -1447,6 +1445,7 @@ extension CostUsageScanner { modelsDevCacheRoot: modelsDevCacheRoot) let group = CodexDayModelKey(day: day, model: model) let rowCostIsTrusted = !unresolvedRowGroups.contains(group) + && !modeOwnershipMismatchGroups.contains(group) && rowCost?.isTrusted(canonicalTotalTokens: totalTokens) == true let aggregateCost = priorityEvidenceGroups.contains(group) ? nil diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift index bf0857e86b..0e51ea76b3 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift @@ -11,6 +11,11 @@ extension CostUsageScanner { let unresolvedGroups: Set } + struct CodexPricingModeEvidence { + let mismatchGroups: Set + let priorityGroups: Set + } + static func codexCanonicalPricingRows(_ usage: CostUsageFileUsage) -> CodexCanonicalPricingRows { let persistedRows = usage.codexRows ?? [] let rowsByGroup = Dictionary(grouping: persistedRows) { @@ -42,6 +47,57 @@ extension CostUsageScanner { return CodexCanonicalPricingRows(rows: canonicalRows, unresolvedGroups: unresolvedGroups) } + static func codexPricingModeEvidence( + usage: CostUsageFileUsage, + reconciledRows: [CodexUsageRow], + range: CostUsageDayRange, + priorityTurns: [String: CodexPriorityTurnMetadata]) -> CodexPricingModeEvidence + { + let reconciledModeTokens = self.codexModeTokenMaps( + rows: reconciledRows, + range: range, + priorityTurns: priorityTurns) + var mismatchGroups = Set() + var priorityGroups = Set() + for (day, models) in usage.days { + for (model, packed) in models { + let persistedStandard = usage.codexStandardTokens?[day]?[model] + let persistedPriority = usage.codexPriorityTokens?[day]?[model] + guard persistedStandard != nil || persistedPriority != nil else { continue } + let (persistedModeTotal, modeOverflow) = max(0, persistedStandard ?? 0) + .addingReportingOverflow(max(0, persistedPriority ?? 0)) + let (canonicalTotal, canonicalOverflow) = max(0, packed[safe: 0] ?? 0) + .addingReportingOverflow(max(0, packed[safe: 2] ?? 0)) + let key = CodexDayModelKey(day: day, model: model) + guard !modeOverflow, !canonicalOverflow else { + mismatchGroups.insert(key) + continue + } + // Copied fork prefixes can stale these legacy maps too. Only a map that is + // independently canonical for its file may constrain retained row ownership. + guard persistedModeTotal == canonicalTotal else { continue } + let rowStandard = reconciledModeTokens.standard?[day]?[model] ?? 0 + let rowPriority = reconciledModeTokens.priority?[day]?[model] ?? 0 + if rowStandard != max(0, persistedStandard ?? 0) + || rowPriority != max(0, persistedPriority ?? 0) + { + mismatchGroups.insert(key) + } + } + } + for (day, models) in usage.codexPriorityTokens ?? [:] { + for (model, tokens) in models where tokens > 0 { + priorityGroups.insert(CodexDayModelKey(day: day, model: model)) + } + } + for row in usage.codexRows ?? [] where row.pricingMode == "priority" + || row.turnID.flatMap({ priorityTurns[$0] }) != nil + { + priorityGroups.insert(CodexDayModelKey(day: row.day, model: row.model)) + } + return CodexPricingModeEvidence(mismatchGroups: mismatchGroups, priorityGroups: priorityGroups) + } + private struct CodexRowTokenTotals: Equatable { var input: Int = 0 var cached: Int = 0 diff --git a/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift b/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift index c051b3c663..b6f8177726 100644 --- a/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift +++ b/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift @@ -285,6 +285,20 @@ struct CostUsageScannerForkSplitTests { let priorityAggregate = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) #expect(priorityAggregate.summary?.totalTokens == 400_100) #expect(priorityAggregate.summary?.totalCostUSD == nil) + + priorityUsage.codexRows = [CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "legacy-mode-less-turn", + eventIndex: 0, + timestampUnixMs: 1, + input: 400_000, + cached: 100_000, + output: 100)] + cache.files = ["/linear-mode-mismatch.jsonl": priorityUsage] + let modeMismatch = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + #expect(modeMismatch.summary?.totalTokens == 400_100) + #expect(modeMismatch.summary?.totalCostUSD == nil) } @Test From 87fd3f94bed63ede6821797f5358eb35e17dea29 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 07:28:53 -0700 Subject: [PATCH 04/10] fix: harden Codex report reconciliation --- .../Generated/CodexParserHash.generated.swift | 2 +- .../Vendored/CostUsage/CostUsagePricing.swift | 8 +- .../CostUsageScanner+CacheHelpers.swift | 17 +- .../CostUsage/CostUsageScanner+Projects.swift | 32 +- ...ostUsageScanner+ReportReconciliation.swift | 24 +- .../CodexBarTests/CostUsagePricingTests.swift | 33 +- .../CostUsageScannerForkSplitTests.swift | 357 ++++++++++++++++-- 7 files changed, 408 insertions(+), 65 deletions(-) diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index b65c26ed78..85926a15f5 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "8717fd480fd7d00f" + static let value = "c79a713e417a766b" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift index 31c9b1b0a2..4c04113d89 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift @@ -519,9 +519,13 @@ enum CostUsagePricing { guard let pricing = self.resolvedCodexPricing( model: model, modelsDevCatalog: modelsDevCatalog, - modelsDevCacheRoot: modelsDevCacheRoot), - pricing.thresholdTokens == nil + modelsDevCacheRoot: modelsDevCacheRoot) else { return nil } + if let thresholdTokens = pricing.thresholdTokens, + max(0, inputTokens) > thresholdTokens + { + return nil + } return self.codexCostUSD( pricing: pricing, inputTokens: inputTokens, diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index a632697147..e071219a5e 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -112,6 +112,7 @@ extension CostUsageScanner { var sawPriorityCost = false var hasUnstableTokenRows = false var hasTokenOverflow = false + var hasIncompletePricing = false var optionalStandardCostUSD: Double? { self.sawStandardCost ? self.standardCostUSD : nil @@ -142,6 +143,7 @@ extension CostUsageScanner { let (rowTokenTotal, overflow) = self.standardTokens.addingReportingOverflow(self.priorityTokens) return !self.hasUnstableTokenRows && !self.hasTokenOverflow + && !self.hasIncompletePricing && !overflow && rowTokenTotal == canonicalTotalTokens } @@ -156,12 +158,16 @@ extension CostUsageScanner { var breakdown = CodexRowCostBreakdown() for row in rows { 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 { breakdown.hasTokenOverflow = true } - if row.input > 0 || row.cached > 0 || row.output > 0, row.eventIndex == nil { + if hasTokens, row.eventIndex == nil { breakdown.hasUnstableTokenRows = 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 { @@ -178,7 +184,10 @@ extension CostUsageScanner { priorityTurns: priorityTurns, modelsDevCatalog: modelsDevCatalog, modelsDevCacheRoot: modelsDevCacheRoot) - else { continue } + else { + breakdown.hasIncompletePricing = breakdown.hasIncompletePricing || hasTokens + continue + } if isPriority { breakdown.priorityCostUSD += cost breakdown.sawPriorityCost = true @@ -374,7 +383,7 @@ extension CostUsageScanner { output: row.output, reasoning: row.reasoning, knownCostNanos: row.knownCostNanos, - unpricedTokens: nil, + unpricedTokens: row.unpricedTokens, pricingModel: pricedModel, pricingMode: isPriority ? "priority" : "standard") } @@ -1447,7 +1456,7 @@ extension CostUsageScanner { let rowCostIsTrusted = !unresolvedRowGroups.contains(group) && !modeOwnershipMismatchGroups.contains(group) && rowCost?.isTrusted(canonicalTotalTokens: totalTokens) == true - let aggregateCost = priorityEvidenceGroups.contains(group) + let aggregateCost = priorityEvidenceGroups.contains(group) || rowCost?.hasIncompletePricing == true ? nil : CostUsagePricing.codexAggregateCostUSD( model: model, diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Projects.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Projects.swift index 5e79e86c07..485785ede5 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Projects.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Projects.swift @@ -113,20 +113,29 @@ extension CostUsageScanner { ?? "" let sourceKey = usage.projectPath ?? "" var accumulator = accumulatorsByProjectPath[projectKey] ?? CodexProjectBreakdownAccumulator() - accumulator.add(report: report, sourcePath: sourceKey) + accumulator.add(filePath: filePath, usage: usage, report: report, sourcePath: sourceKey) accumulatorsByProjectPath[projectKey] = accumulator } return accumulatorsByProjectPath.map { projectPath, accumulator in - let merged = CostUsageDailyReport.merged(accumulator.reports) + var projectCache = CostUsageCache() + projectCache.files = accumulator.files + for usage in accumulator.files.values { + Self.applyFileDays(cache: &projectCache, fileDays: usage.days, sign: 1) + } + let report = Self.buildCodexReportFromCache( + cache: projectCache, + range: range, + modelsDevCatalog: resolvedModelsDevCatalog, + priorityTurns: priorityTurns) let resolvedPath = projectPath.isEmpty ? nil : projectPath return CostUsageProjectBreakdown( name: Self.codexProjectName(path: resolvedPath), path: resolvedPath, - totalTokens: merged.summary?.totalTokens, - totalCostUSD: merged.summary?.totalCostUSD, - daily: merged.data, - modelBreakdowns: Self.codexProjectModelBreakdowns(from: merged.data), + totalTokens: report.summary?.totalTokens, + totalCostUSD: report.summary?.totalCostUSD, + daily: report.data, + modelBreakdowns: Self.codexProjectModelBreakdowns(from: report.data), sources: Self.codexProjectSourceBreakdowns(from: accumulator.reportsBySourcePath)) } .sorted { lhs, rhs in @@ -151,11 +160,16 @@ extension CostUsageScanner { } private struct CodexProjectBreakdownAccumulator { - var reports: [CostUsageDailyReport] = [] + var files: [String: CostUsageFileUsage] = [:] var reportsBySourcePath: [String: [CostUsageDailyReport]] = [:] - mutating func add(report: CostUsageDailyReport, sourcePath: String) { - self.reports.append(report) + mutating func add( + filePath: String, + usage: CostUsageFileUsage, + report: CostUsageDailyReport, + sourcePath: String) + { + self.files[filePath] = usage self.reportsBySourcePath[sourcePath, default: []].append(report) } } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift index 0e51ea76b3..6dce36c59f 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift @@ -132,27 +132,15 @@ extension CostUsageScanner { return rows } guard allRowsTotal.exceeds(target) else { return nil } - let timestampPresence = Set(rows.map { $0.timestampUnixMs != nil }) - guard timestampPresence.count <= 1 else { return nil } - - let chronological = rows.enumerated().sorted { lhs, rhs in - let lhsTimestamp = lhs.element.timestampUnixMs ?? Int64.min - let rhsTimestamp = rhs.element.timestampUnixMs ?? Int64.min - if lhsTimestamp != rhsTimestamp { - return lhsTimestamp < rhsTimestamp - } - let lhsEventIndex = lhs.element.eventIndex ?? Int.min - let rhsEventIndex = rhs.element.eventIndex ?? Int.min - if lhsEventIndex != rhsEventIndex { - return lhsEventIndex < rhsEventIndex - } - return lhs.offset < rhs.offset + if target == CodexRowTokenTotals() { + return [] } + var suffixTotal = CodexRowTokenTotals() - for index in chronological.indices.reversed() { - guard suffixTotal.add(chronological[index].element) else { return nil } + for index in rows.indices.reversed() { + guard suffixTotal.add(rows[index]) else { return nil } if suffixTotal == target { - return chronological[index...].map(\.element) + return Array(rows[index...]) } if suffixTotal.exceeds(target) { return nil diff --git a/Tests/CodexBarTests/CostUsagePricingTests.swift b/Tests/CodexBarTests/CostUsagePricingTests.swift index 82363225f8..96ce407a6a 100644 --- a/Tests/CodexBarTests/CostUsagePricingTests.swift +++ b/Tests/CodexBarTests/CostUsagePricingTests.swift @@ -617,9 +617,21 @@ struct CostUsagePricingTests { } @Test - func `codex aggregate pricing rejects effective thresholds and keeps linear rates`() throws { + func `codex aggregate pricing uses safe base rates and rejects aggregates above thresholds`() throws { let emptyRoot = try Self.cacheRoot() - let bundledThreshold = CostUsagePricing.codexAggregateCostUSD( + let bundledBelowThreshold = CostUsagePricing.codexAggregateCostUSD( + model: "gpt-5.6-sol", + inputTokens: 200_000, + cachedInputTokens: 0, + outputTokens: 100, + modelsDevCacheRoot: emptyRoot) + let bundledAtThreshold = CostUsagePricing.codexAggregateCostUSD( + model: "gpt-5.6-sol", + inputTokens: 272_000, + cachedInputTokens: 0, + outputTokens: 100, + modelsDevCacheRoot: emptyRoot) + let bundledAboveThreshold = CostUsagePricing.codexAggregateCostUSD( model: "gpt-5.6-sol", inputTokens: 400_000, cachedInputTokens: 0, @@ -648,16 +660,25 @@ struct CostUsagePricingTests { } } """) - let catalogThreshold = CostUsagePricing.codexAggregateCostUSD( + let catalogAtThreshold = CostUsagePricing.codexAggregateCostUSD( model: "aggregate-threshold-model", - inputTokens: 400_000, + inputTokens: 200_000, + cachedInputTokens: 0, + outputTokens: 100, + modelsDevCacheRoot: catalogThresholdRoot) + let catalogAboveThreshold = CostUsagePricing.codexAggregateCostUSD( + model: "aggregate-threshold-model", + inputTokens: 200_001, cachedInputTokens: 0, outputTokens: 100, modelsDevCacheRoot: catalogThresholdRoot) - #expect(bundledThreshold == nil) + #expect(bundledBelowThreshold == (200_000.0 * 5e-6) + (100.0 * 30e-6)) + #expect(bundledAtThreshold == (272_000.0 * 5e-6) + (100.0 * 30e-6)) + #expect(bundledAboveThreshold == nil) #expect(linear == (300_000.0 * 7.5e-7) + (100_000.0 * 7.5e-8) + (100.0 * 4.5e-6)) - #expect(catalogThreshold == nil) + #expect(catalogAtThreshold == (200_000.0 * 5e-6) + (100.0 * 30e-6)) + #expect(catalogAboveThreshold == nil) } @Test diff --git a/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift b/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift index b6f8177726..96b0be75ea 100644 --- a/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift +++ b/Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift @@ -5,7 +5,7 @@ import Testing struct CostUsageScannerForkSplitTests { @Test - func `codex report preserves short request pricing after copied fork prefix`() throws { + func `codex report preserves short request pricing after copied fork prefix`() async throws { let environment = try CostUsageTestEnvironment() defer { environment.cleanup() } @@ -83,12 +83,21 @@ struct CostUsageScannerForkSplitTests { #expect(abs(projects.compactMap(\.totalCostUSD).reduce(0, +) - reportCost) < 1e-12) #expect(abs(sessions.compactMap(\.costUSD).reduce(0, +) - reportCost) < 1e-12) - _ = CostUsageStoreAccess.replace( + let predecessorHash = try #require(CostUsageStore.compatiblePredecessorParserHashes.first) + let predecessorStore = CostUsageStore( cacheRoot: environment.cacheRoot, - cache: cache, - calendar: range.calendar) - let restored = CostUsageStoreAccess.read(cacheRoot: environment.cacheRoot, calendar: range.calendar) + schemaVersion: CostUsageStore.combinedSchemaVersion( + base: CostUsageStore.baseSchemaVersion, + parserHash: predecessorHash), + parserHash: predecessorHash) + _ = predecessorStore.syncSaveCodexCache( + cache, + calendar: range.calendar, + requestedScanWindow: (sinceKey: dayKey, untilKey: dayKey)) + let currentStore = CostUsageStore(cacheRoot: environment.cacheRoot) + let restored = currentStore.syncLoadCodexCache(calendar: range.calendar) let warmReport = CostUsageScanner.buildCodexReportFromCache(cache: restored, range: range) + #expect(await currentStore.rebuildCount == 0) #expect(restored.files.values.flatMap { $0.codexRows ?? [] }.count == 3) #expect(warmReport.data == report.data) #expect(warmReport.summary == report.summary) @@ -211,42 +220,37 @@ struct CostUsageScannerForkSplitTests { var usage = CostUsageScanner.makeFileUsage( mtimeUnixMs: 2, size: 1, - days: [dayKey: [model: [200_000, 0, 0]]], + days: [dayKey: [model: [400_000, 0, 0]]], parsedBytes: 1, sessionId: "irreconcilable", codexCostNanos: [dayKey: [model: 84_000_000_000]], - codexRows: [row(index: 0, input: 120_000), row(index: 1, input: 120_000)], + codexRows: [row(index: 0, input: 220_000), row(index: 1, input: 220_000)], codexScanComplete: true) var cache = CostUsageCache() cache.files = ["/irreconcilable.jsonl": usage] cache.days = usage.days let irreconcilable = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) - #expect(irreconcilable.summary?.totalTokens == 200_000) + #expect(irreconcilable.summary?.totalTokens == 400_000) #expect(irreconcilable.summary?.totalCostUSD == nil) usage.codexRows = nil cache.files = ["/aggregate-only.jsonl": usage] let aggregateOnly = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) - #expect(aggregateOnly.summary?.totalTokens == 200_000) + #expect(aggregateOnly.summary?.totalTokens == 400_000) #expect(aggregateOnly.summary?.totalCostUSD == nil) - let timestampedParent = row(index: 0, input: 200_000) - let untimestampedChild = CostUsageScanner.CodexUsageRow( - day: dayKey, + usage.days = [dayKey: [model: [200_000, 0, 0]]] + usage.codexRows = nil + cache.files = ["/below-threshold.jsonl": usage] + cache.days = usage.days + let belowThreshold = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + let belowThresholdCost = try #require(CostUsagePricing.codexCostUSD( model: model, - turnID: "mixed-timestamp-child", - eventIndex: 1, - timestampUnixMs: nil, - input: 200_000, - cached: 0, - output: 0) - usage.codexRows = [timestampedParent, untimestampedChild] - let mixedTimestamps = CostUsageScanner.codexCanonicalPricingRows(usage) - #expect(mixedTimestamps.rows.isEmpty) - #expect(mixedTimestamps.unresolvedGroups == [ - CostUsageScanner.CodexDayModelKey(day: dayKey, model: model), - ]) + inputTokens: 200_000, + cachedInputTokens: 0, + outputTokens: 0)) + #expect(abs((belowThreshold.summary?.totalCostUSD ?? 0) - belowThresholdCost) < 1e-12) } @Test @@ -336,6 +340,309 @@ struct CostUsageScannerForkSplitTests { #expect(reconciled.rows == [later, earlier]) } + @Test + func `copied fast prefix with later timestamp cannot replace persisted standard suffix`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.6-sol" + let parent = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "fast-parent", + eventIndex: 0, + timestampUnixMs: 2, + input: 100_000, + cached: 0, + output: 10, + pricingMode: "priority") + let child = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "standard-child", + eventIndex: 1, + timestampUnixMs: 1, + input: 100_000, + cached: 0, + output: 10, + pricingMode: "standard") + let parentUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 2, + size: 1, + days: [dayKey: [model: [100_000, 0, 10]]], + parsedBytes: 1, + sessionId: "parent", + codexRows: [parent], + codexScanComplete: true) + let childUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 2, + size: 1, + days: [dayKey: [model: [100_000, 0, 10]]], + parsedBytes: 1, + sessionId: "child", + forkedFromId: "parent", + codexRows: [parent, child], + codexScanComplete: true) + let reconciled = CostUsageScanner.codexCanonicalPricingRows(childUsage) + #expect(reconciled.unresolvedGroups.isEmpty) + #expect(reconciled.rows == [child]) + + var cache = CostUsageCache() + cache.files = ["/parent.jsonl": parentUsage, "/child.jsonl": childUsage] + cache.days = [dayKey: [model: [200_000, 0, 20]]] + let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + let breakdown = try #require(report.data.first?.modelBreakdowns?.first) + let standardCost = try #require(CostUsagePricing.codexCostUSD( + model: model, + inputTokens: 100_000, + cachedInputTokens: 0, + outputTokens: 10)) + let priorityCost = try #require(CostUsagePricing.codexPriorityCostUSD( + model: model, + inputTokens: 100_000, + cachedInputTokens: 0, + outputTokens: 10)) + #expect(abs((breakdown.standardCostUSD ?? 0) - standardCost) < 1e-12) + #expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 1e-12) + #expect(breakdown.standardTokens == 100_010) + #expect(breakdown.priorityTokens == 100_010) + } + + @Test + func `zero owned fork rows use an empty suffix without hiding parent cost`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.6-sol" + let parentRow = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "parent", + eventIndex: 0, + timestampUnixMs: 1, + input: 300_000, + cached: 0, + output: 10) + let parentUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 1, + size: 1, + days: [dayKey: [model: [300_000, 0, 10]]], + parsedBytes: 1, + sessionId: "parent", + codexRows: [parentRow], + codexScanComplete: true) + let childUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 2, + size: 1, + days: [dayKey: [model: [0, 0, 0]]], + parsedBytes: 1, + sessionId: "zero-child", + forkedFromId: "parent", + codexRows: [parentRow], + codexScanComplete: true) + let reconciled = CostUsageScanner.codexCanonicalPricingRows(childUsage) + #expect(reconciled.rows.isEmpty) + #expect(reconciled.unresolvedGroups.isEmpty) + + var cache = CostUsageCache() + cache.files = ["/parent.jsonl": parentUsage, "/child.jsonl": childUsage] + cache.days = parentUsage.days + let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + let expected = try #require(CostUsagePricing.codexCostUSD( + model: model, + inputTokens: 300_000, + cachedInputTokens: 0, + outputTokens: 10)) + #expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 1e-12) + } + + @Test + func `exact rows require complete request pricing coverage`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.6-sol" + let priced = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "priced", + eventIndex: 0, + input: 100_000, + cached: 0, + output: 10, + pricingModel: model) + let unpriced = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "unpriced", + eventIndex: 1, + input: 100_000, + cached: 0, + output: 10, + unpricedTokens: 100_010, + pricingModel: "unpriced-test-model") + let usage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 1, + size: 1, + days: [dayKey: [model: [200_000, 0, 20]]], + parsedBytes: 1, + codexRows: [priced, unpriced], + codexScanComplete: true) + var cache = CostUsageCache() + cache.files = ["/partial-pricing.jsonl": usage] + cache.days = usage.days + let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + #expect(report.data.first?.modelBreakdowns?.first?.costUSD == nil) + #expect(report.summary?.totalCostUSD == nil) + + let authoritativeZero = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "authoritative-zero", + eventIndex: 2, + input: 0, + cached: 0, + output: 0, + knownCostNanos: 42_000_000_000) + let completeUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 1, + size: 1, + days: [dayKey: [model: [100_000, 0, 10]]], + parsedBytes: 1, + codexRows: [priced, authoritativeZero], + codexScanComplete: true) + cache.files = ["/complete-pricing.jsonl": completeUsage] + cache.days = completeUsage.days + let complete = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + let pricedCost = try #require(CostUsagePricing.codexCostUSD( + model: model, + inputTokens: 100_000, + cachedInputTokens: 0, + outputTokens: 10)) + #expect(abs((complete.summary?.totalCostUSD ?? 0) - (pricedCost + 42)) < 1e-12) + } + + @Test + func `project primary report propagates unresolved same model ownership`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.6-sol" + let projectPath = "/tmp/codexbar-unresolved-project" + let exactRow = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "exact", + eventIndex: 0, + input: 300_000, + cached: 0, + output: 10) + func unresolvedRow(_ index: Int) -> CostUsageScanner.CodexUsageRow { + CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "unresolved-\(index)", + eventIndex: index, + input: 220_000, + cached: 0, + output: 5) + } + let exactUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 1, + size: 1, + days: [dayKey: [model: [300_000, 0, 10]]], + parsedBytes: 1, + sessionId: "exact", + projectPath: projectPath, + canonicalProjectPath: projectPath, + codexRows: [exactRow], + codexScanComplete: true) + let unresolvedUsage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 2, + size: 1, + days: [dayKey: [model: [400_000, 0, 10]]], + parsedBytes: 1, + sessionId: "unresolved", + projectPath: projectPath, + canonicalProjectPath: projectPath, + codexRows: [unresolvedRow(1), unresolvedRow(2)], + codexScanComplete: true) + var cache = CostUsageCache() + cache.files = ["/exact.jsonl": exactUsage, "/unresolved.jsonl": unresolvedUsage] + cache.days = [dayKey: [model: [700_000, 0, 20]]] + let global = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + #expect(global.data.first?.modelBreakdowns?.first?.costUSD == nil) + #expect(global.summary?.totalCostUSD == nil) + + let project = try #require(CostUsageScanner.buildCodexProjectBreakdownsFromCache( + cache: cache, + range: range).first) + #expect(project.path == projectPath) + #expect(project.modelBreakdowns?.first?.costUSD == nil) + #expect(project.totalCostUSD == nil) + #expect(project.totalTokens == 700_020) + } + + @Test + func `project primary report keeps priced models beside explicitly unpriced models`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let pricedModel = "gpt-5.6-sol" + let unpricedModel = "codex-auto-review" + let projectPath = "/tmp/codexbar-partial-project" + func usage(model: String, input: Int, path: String) -> (String, CostUsageFileUsage) { + let row = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: path, + eventIndex: 0, + input: input, + cached: 0, + output: 10, + pricingModel: model) + return (path, CostUsageScanner.makeFileUsage( + mtimeUnixMs: 1, + size: 1, + days: [dayKey: [model: [input, 0, 10]]], + parsedBytes: 1, + sessionId: path, + projectPath: projectPath, + canonicalProjectPath: projectPath, + codexRows: [row], + codexScanComplete: true)) + } + let priced = usage(model: pricedModel, input: 100_000, path: "/priced.jsonl") + let unpriced = usage(model: unpricedModel, input: 50000, path: "/unpriced.jsonl") + var cache = CostUsageCache() + cache.files = [priced.0: priced.1, unpriced.0: unpriced.1] + cache.days = [ + dayKey: [pricedModel: [100_000, 0, 10], unpricedModel: [50000, 0, 10]], + ] + let expected = try #require(CostUsagePricing.codexCostUSD( + model: pricedModel, + inputTokens: 100_000, + cachedInputTokens: 0, + outputTokens: 10)) + + let project = try #require(CostUsageScanner.buildCodexProjectBreakdownsFromCache( + cache: cache, + range: range).first) + #expect(abs((project.totalCostUSD ?? 0) - expected) < 1e-12) + #expect(project.modelBreakdowns?.first { $0.modelName == pricedModel }?.costUSD == expected) + #expect(project.modelBreakdowns?.first { $0.modelName == unpricedModel }?.costUSD == nil) + } + @Test func `codex report reconciles copied fork prefix without losing fast split`() throws { let fixture = try self.makeFixture() @@ -346,7 +653,7 @@ struct CostUsageScannerForkSplitTests { let child = try #require(cache.files.first { $0.value.sessionId == "child-session" }) let copiedParentRows = try #require(parent.value.codexRows) var inflatedChild = child.value - inflatedChild.codexRows = (inflatedChild.codexRows ?? []) + copiedParentRows + inflatedChild.codexRows = copiedParentRows + (inflatedChild.codexRows ?? []) cache.files[child.key] = inflatedChild let canonical = try #require(cache.days[fixture.dayKey]?[fixture.model]) From 162215ab0d2a1dad7ebb1bb6493755311f10300b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 07:29:05 -0700 Subject: [PATCH 05/10] fix: adopt compatible Codex cost stores --- .../Vendored/CostUsage/CostUsageStore.swift | 38 +++++++- .../CostUsagePerformanceGateTests.swift | 53 ++++++++++ Tests/CodexBarTests/CostUsageStoreTests.swift | 97 +++++++++++++++++++ 3 files changed, 183 insertions(+), 5 deletions(-) diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift index 363b81a413..bfa47eb910 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift @@ -76,6 +76,8 @@ actor CostUsageStore { base: CostUsageStore.baseSchemaVersion, parserHash: CodexParserHash.value) static let cacheGeneration = "sqlite:\(CostUsageStore.schemaVersion)" + /// CodexBar 0.49.0-0.49.2 SQLite producer; its persisted CodexUsageRow payload is compatible. + static let compatiblePredecessorParserHashes: Set = ["b975eb705f905b9a"] /// Test-only crash injection: invoked inside `saveCodexCache`'s transaction after each /// persisted file with the running count, so a crash-safety harness can SIGKILL the @@ -322,19 +324,45 @@ extension CostUsageStore { } private func validateExistingDatabase(_ database: OpaquePointer) throws { - guard try Self.scalarInt(database, "PRAGMA user_version") == Int64(self.expectedSchemaVersion) else { - throw StoreError.incompatibleSchema - } - guard try Self.scalarText( + let actualVersion = try Self.scalarInt(database, "PRAGMA user_version") + guard let storedHash = try Self.scalarText( database, - "SELECT value FROM meta WHERE key = 'parser_hash'") == self.expectedParserHash + "SELECT value FROM meta WHERE key = 'parser_hash'") else { throw StoreError.incompatibleSchema } + let isCurrent = actualVersion == Int64(self.expectedSchemaVersion) && storedHash == self.expectedParserHash + let predecessorVersion = Self.combinedSchemaVersion( + base: Self.baseSchemaVersion, + parserHash: storedHash) + let canAdoptPredecessor = self.expectedParserHash == CodexParserHash.value + && self.expectedSchemaVersion == Self.schemaVersion + && Self.compatiblePredecessorParserHashes.contains(storedHash) + && actualVersion == Int64(predecessorVersion) + guard isCurrent || canAdoptPredecessor else { throw StoreError.incompatibleSchema } guard try Self.scalarText(database, "PRAGMA quick_check") == "ok" else { throw StoreError.invalidData } guard try Self.scalarInt(database, "PRAGMA auto_vacuum") == 2 else { throw StoreError.incompatibleSchema } + if canAdoptPredecessor { + try self.adoptCompatiblePredecessor(database) + } + } + + private func adoptCompatiblePredecessor(_ database: OpaquePointer) throws { + try Self.execute(database, "BEGIN IMMEDIATE") + do { + let statement = try Self.prepare(database, "UPDATE meta SET value = ? WHERE key = 'parser_hash'") + defer { sqlite3_finalize(statement) } + Self.bind(self.expectedParserHash, to: statement, at: 1) + try Self.stepDone(statement, database: database) + guard sqlite3_changes(database) == 1 else { throw StoreError.incompatibleSchema } + try Self.execute(database, "PRAGMA user_version = \(self.expectedSchemaVersion)") + try Self.execute(database, "COMMIT") + } catch { + try? Self.execute(database, "ROLLBACK") + throw error + } } private func createSchema(_ database: OpaquePointer) throws { diff --git a/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift b/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift index ae33c7ce4e..fd05fc7b68 100644 --- a/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift +++ b/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift @@ -55,6 +55,59 @@ struct CostUsagePerformanceGateTests { #expect(warm.data.first?.totalTokens == cold.data.first?.totalTokens) } + @Test + func `compatible predecessor store adoption performs zero session head parses`() throws { + let env = try CostUsageTestEnvironment() + defer { env.cleanup() } + let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10) + _ = try Self.writeSyntheticCodexCorpus(env: env, day: day, files: 3, turnsPerFile: 2) + let coldCacheRoot = env.root.appendingPathComponent("cold-cache", isDirectory: true) + var coldOptions = CostUsageScanner.Options( + codexSessionsRoot: env.codexSessionsRoot, + claudeProjectsRoots: nil, + cacheRoot: coldCacheRoot, + codexTraceDatabaseURL: env.root.appendingPathComponent("missing.sqlite")) + coldOptions.refreshMinIntervalSeconds = 0 + let cold = CostUsageScanner.loadDailyReport( + provider: .codex, + since: day, + until: day, + now: day, + options: coldOptions) + let cache = CostUsageStoreAccess.read(cacheRoot: coldCacheRoot, calendar: coldOptions.calendar) + let predecessorHash = try #require(CostUsageStore.compatiblePredecessorParserHashes.first) + let predecessorStore = CostUsageStore( + cacheRoot: env.cacheRoot, + schemaVersion: CostUsageStore.combinedSchemaVersion( + base: CostUsageStore.baseSchemaVersion, + parserHash: predecessorHash), + parserHash: predecessorHash) + _ = predecessorStore.syncSaveCodexCache( + cache, + calendar: coldOptions.calendar, + requestedScanWindow: ( + sinceKey: CostUsageScanner.CostUsageDayRange.dayKey(from: day), + untilKey: CostUsageScanner.CostUsageDayRange.dayKey(from: day))) + + var warmOptions = coldOptions + warmOptions.cacheRoot = env.cacheRoot + let counter = HeadParseCounter() + let warm = CostUsageScanner.withCodexSessionHeadParseObserverForTesting { + counter.increment() + } operation: { + CostUsageScanner.loadDailyReport( + provider: .codex, + since: day, + until: day, + now: day.addingTimeInterval(1), + options: warmOptions) + } + + #expect(counter.value == 0) + #expect(warm.data == cold.data) + #expect(warm.summary == cold.summary) + } + @Test func `over budget prune retains stale coverage file modified inside the window`() async throws { let env = try CostUsageTestEnvironment() diff --git a/Tests/CodexBarTests/CostUsageStoreTests.swift b/Tests/CodexBarTests/CostUsageStoreTests.swift index 884d49145b..05183b44e7 100644 --- a/Tests/CodexBarTests/CostUsageStoreTests.swift +++ b/Tests/CodexBarTests/CostUsageStoreTests.swift @@ -604,6 +604,58 @@ extension CostUsageStoreTests { } extension CostUsageStoreTests { + @Test + func `compatible predecessor parser hash adopts without rebuilding`() async throws { + let fixture = try StoreFixture() + defer { fixture.remove() } + #expect(CostUsageStore.compatiblePredecessorParserHashes == ["b975eb705f905b9a"]) + let predecessorHash = try #require(CostUsageStore.compatiblePredecessorParserHashes.first) + let predecessorVersion = CostUsageStore.combinedSchemaVersion( + base: CostUsageStore.baseSchemaVersion, + parserHash: predecessorHash) + let predecessor = CostUsageStore( + cacheRoot: fixture.root, + schemaVersion: predecessorVersion, + parserHash: predecessorHash) + let file = Self.file(path: "/rollouts/compatible.jsonl", day: "2026-08-01") + let token = Self.snapshot(path: file.path, eventIndex: 0) + let usageRow = CostUsageStoreUsageRow(path: file.path, rowIndex: 0, payload: Data([8, 9, 10])) + let aggregate = Self.aggregate(day: "2026-08-01", model: "gpt-5.6-sol", scale: 1) + let lineage = Self.lineage(path: file.path) + let line = Self.bufferedLine(path: file.path, kind: .subagent, index: 0) + let discovery = Self.discoveryState(paths: [file.path]) + let lookback = CostUsageStoreLookbackState( + scanSinceDay: "2026-08-01", + rootPaths: ["/root"], + nextDayByRoot: ["/root": "2026-08-02"], + completedRootPaths: [], + pendingFilePaths: [file.path], + legacyRecursivePendingRootPaths: []) + let accumulator = Self.accumulator(path: file.path) + let metadata = Self.metadata() + #expect(await predecessor.upsertFile(file)) + #expect(await predecessor.appendTokenSnapshots([token])) + #expect(await predecessor.replaceUsageRows(path: file.path, rows: [usageRow])) + #expect(await predecessor.replaceFileDayAggregates(path: file.path, aggregates: [aggregate])) + #expect(await predecessor.mergeDayAggregates([aggregate])) + #expect(await predecessor.upsertForkLineage(lineage)) + #expect(await predecessor.replaceBufferedLines(path: file.path, kind: .subagent, lines: [line])) + #expect(await predecessor.setDiscoveryState(discovery)) + #expect(await predecessor.setLookbackState(lookback)) + #expect(await predecessor.upsertAccumulator(accumulator)) + #expect(await predecessor.setMetadata(metadata)) + let before = await predecessor.readSnapshot() + + let current = CostUsageStore(cacheRoot: fixture.root) + let after = await current.readSnapshot() + #expect(after == before) + #expect(await current.rebuildCount == 0) + #expect(await current.configuration()?.userVersion == Int(CostUsageStore.schemaVersion)) + let connection = try SQLiteTestConnection(url: fixture.databaseURL, readOnly: true) + #expect(try connection.scalarInt( + "SELECT COUNT(*) FROM meta WHERE key = 'parser_hash' AND value = '\(CodexParserHash.value)'") == 1) + } + @Test func `version mismatch drops and recreates`() async throws { let fixture = try StoreFixture() @@ -637,6 +689,51 @@ extension CostUsageStoreTests { #expect(await store.rebuildCount == 1) } + @Test + func `compatible predecessor hash with mismatched version still rebuilds`() async throws { + let fixture = try StoreFixture() + defer { fixture.remove() } + let predecessorHash = try #require(CostUsageStore.compatiblePredecessorParserHashes.first) + let predecessorVersion = CostUsageStore.combinedSchemaVersion( + base: CostUsageStore.baseSchemaVersion, + parserHash: predecessorHash) + let predecessor = CostUsageStore( + cacheRoot: fixture.root, + schemaVersion: predecessorVersion, + parserHash: predecessorHash) + #expect(await predecessor.setMetadata(Self.metadata())) + try SQLiteTestConnection.execute( + at: fixture.databaseURL, + sql: "PRAGMA user_version = \(predecessorVersion + 1)") + + let current = CostUsageStore(cacheRoot: fixture.root) + #expect(await current.fetchMetadata() == .empty) + #expect(await current.rebuildCount == 1) + } + + @Test + func `compatible predecessor hash without incremental auto vacuum still rebuilds`() async throws { + let fixture = try StoreFixture() + defer { fixture.remove() } + let predecessorHash = try #require(CostUsageStore.compatiblePredecessorParserHashes.first) + let predecessorVersion = CostUsageStore.combinedSchemaVersion( + base: CostUsageStore.baseSchemaVersion, + parserHash: predecessorHash) + try FileManager.default.createDirectory( + at: fixture.databaseURL.deletingLastPathComponent(), + withIntermediateDirectories: true) + try SQLiteTestConnection.execute(at: fixture.databaseURL, sql: """ + CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT NOT NULL); + INSERT INTO meta(key, value) VALUES ('parser_hash', '\(predecessorHash)'); + PRAGMA user_version = \(predecessorVersion); + """) + + let current = CostUsageStore(cacheRoot: fixture.root) + #expect(await current.fetchMetadata() == .empty) + #expect(await current.rebuildCount == 1) + #expect(await current.configuration()?.autoVacuumMode == 2) + } + @Test func `garbage database recovers by rebuild`() async throws { let fixture = try StoreFixture() From f8b680c45f467eedd0e325b15ddff5d922b80b9b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 07:41:00 -0700 Subject: [PATCH 06/10] test: realign pricing architecture anchors --- Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index ccb061b7ec..d4abbbbcc6 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -3638,7 +3638,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift", - line: 585, + line: 589, anchor: "guard let pricing = self.codex[key] else { return nil }", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -3646,7 +3646,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift", - line: 704, + line: 708, anchor: "guard let pricing = self.claude[key] else { return nil }", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, From ab5ad518ab7abd4f5cb646497593ce8c8b518f40 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 08:13:31 -0700 Subject: [PATCH 07/10] fix: preserve incomplete Codex pricing evidence --- .../Generated/CodexParserHash.generated.swift | 2 +- .../CostUsageScanner+CacheHelpers.swift | 11 +++- ...ostUsageScanner+ReportReconciliation.swift | 25 ++++++++++ ...ScannerForkSplitPricingEvidenceTests.swift | 50 +++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 Tests/CodexBarTests/CostUsageScannerForkSplitPricingEvidenceTests.swift diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 85926a15f5..a98fefdee7 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "c79a713e417a766b" + static let value = "85464403d1c7bbb2" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index e071219a5e..294663d910 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -1405,6 +1405,7 @@ extension CostUsageScanner { var unresolvedRowGroups = Set() var modeOwnershipMismatchGroups = Set() var priorityEvidenceGroups = Set() + var incompletePricingEvidenceGroups = Set() for usage in reportCache.files.values { let reconciled = self.codexCanonicalPricingRows(usage) unresolvedRowGroups.formUnion(reconciled.unresolvedGroups) @@ -1415,6 +1416,12 @@ extension CostUsageScanner { priorityTurns: priorityTurns) modeOwnershipMismatchGroups.formUnion(modeEvidence.mismatchGroups) priorityEvidenceGroups.formUnion(modeEvidence.priorityGroups) + incompletePricingEvidenceGroups.formUnion(self.codexIncompletePricingEvidenceGroups( + usage: usage, + range: range, + priorityTurns: priorityTurns, + modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader), + modelsDevCacheRoot: modelsDevCacheRoot)) for row in reconciled.rows where CostUsageDayRange.isInRange( dayKey: row.day, since: range.sinceKey, @@ -1456,7 +1463,9 @@ extension CostUsageScanner { let rowCostIsTrusted = !unresolvedRowGroups.contains(group) && !modeOwnershipMismatchGroups.contains(group) && rowCost?.isTrusted(canonicalTotalTokens: totalTokens) == true - let aggregateCost = priorityEvidenceGroups.contains(group) || rowCost?.hasIncompletePricing == true + let aggregateCost = priorityEvidenceGroups.contains(group) + || incompletePricingEvidenceGroups.contains(group) + || rowCost?.hasIncompletePricing == true ? nil : CostUsagePricing.codexAggregateCostUSD( model: model, diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift index 6dce36c59f..593984b896 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift @@ -98,6 +98,31 @@ extension CostUsageScanner { return CodexPricingModeEvidence(mismatchGroups: mismatchGroups, priorityGroups: priorityGroups) } + static func codexIncompletePricingEvidenceGroups( + usage: CostUsageFileUsage, + range: CostUsageDayRange, + priorityTurns: [String: CodexPriorityTurnMetadata], + modelsDevCatalog: ModelsDevCatalog?, + modelsDevCacheRoot: URL?) -> Set + { + let rowsByGroup = Dictionary(grouping: usage.codexRows ?? []) { + CodexDayModelKey(day: $0.day, model: $0.model) + } + return Set(rowsByGroup.compactMap { group, rows in + guard CostUsageDayRange.isInRange( + dayKey: group.day, + since: range.sinceKey, + until: range.untilKey) + else { return nil } + let breakdown = self.codexRowCostBreakdown( + rows: rows, + priorityTurns: priorityTurns, + modelsDevCatalog: modelsDevCatalog, + modelsDevCacheRoot: modelsDevCacheRoot) + return breakdown.hasIncompletePricing ? group : nil + }) + } + private struct CodexRowTokenTotals: Equatable { var input: Int = 0 var cached: Int = 0 diff --git a/Tests/CodexBarTests/CostUsageScannerForkSplitPricingEvidenceTests.swift b/Tests/CodexBarTests/CostUsageScannerForkSplitPricingEvidenceTests.swift new file mode 100644 index 0000000000..2861904c1d --- /dev/null +++ b/Tests/CodexBarTests/CostUsageScannerForkSplitPricingEvidenceTests.swift @@ -0,0 +1,50 @@ +import Foundation +#if canImport(SQLite3) +import Testing +@testable import CodexBarCore + +extension CostUsageScannerForkSplitTests { + @Test + func `unresolved rows preserve incomplete pricing evidence`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + + let day = try environment.makeLocalNoon(year: 2026, month: 8, day: 11) + let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) + let dayKey = range.sinceKey + let model = "gpt-5.4-mini" + let priced = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "priced-prefix", + eventIndex: 0, + input: 150_000, + cached: 0, + output: 10, + pricingModel: model) + let unpriced = CostUsageScanner.CodexUsageRow( + day: dayKey, + model: model, + turnID: "unpriced-suffix", + eventIndex: 1, + input: 100_000, + cached: 0, + output: 10, + pricingModel: "unpriced-test-model") + let usage = CostUsageScanner.makeFileUsage( + mtimeUnixMs: 1, + size: 1, + days: [dayKey: [model: [200_000, 0, 20]]], + parsedBytes: 1, + codexRows: [priced, unpriced], + codexScanComplete: true) + var cache = CostUsageCache() + cache.files = ["/unresolved-unpriced.jsonl": usage] + cache.days = usage.days + + let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: range) + #expect(report.data.first?.modelBreakdowns?.first?.costUSD == nil) + #expect(report.summary?.totalCostUSD == nil) + } +} +#endif From b3152cdaef7a2600ea3709fadd3ee0121ea262ab Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 08:26:25 -0700 Subject: [PATCH 08/10] fix: reject ambiguous Codex cost prefixes --- .../Generated/CodexParserHash.generated.swift | 2 +- .../CostUsageScanner+CacheHelpers.swift | 5 ++ ...ostUsageScanner+ReportReconciliation.swift | 14 +++-- ...ScannerForkSplitPricingEvidenceTests.swift | 52 +++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index a98fefdee7..9307a08c79 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "85464403d1c7bbb2" + static let value = "43609cc56f76a003" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index 294663d910..31f435d89f 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -1406,6 +1406,7 @@ extension CostUsageScanner { var modeOwnershipMismatchGroups = Set() var priorityEvidenceGroups = Set() var incompletePricingEvidenceGroups = Set() + var authoritativeCostEvidenceGroups = Set() for usage in reportCache.files.values { let reconciled = self.codexCanonicalPricingRows(usage) unresolvedRowGroups.formUnion(reconciled.unresolvedGroups) @@ -1422,6 +1423,9 @@ extension CostUsageScanner { priorityTurns: priorityTurns, modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader), modelsDevCacheRoot: modelsDevCacheRoot)) + for row in usage.codexRows ?? [] where (row.knownCostNanos ?? 0) != 0 { + authoritativeCostEvidenceGroups.insert(CodexDayModelKey(day: row.day, model: row.model)) + } for row in reconciled.rows where CostUsageDayRange.isInRange( dayKey: row.day, since: range.sinceKey, @@ -1465,6 +1469,7 @@ extension CostUsageScanner { && rowCost?.isTrusted(canonicalTotalTokens: totalTokens) == true let aggregateCost = priorityEvidenceGroups.contains(group) || incompletePricingEvidenceGroups.contains(group) + || (unresolvedRowGroups.contains(group) && authoritativeCostEvidenceGroups.contains(group)) || rowCost?.hasIncompletePricing == true ? nil : CostUsagePricing.codexAggregateCostUSD( diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift index 593984b896..a55e91667c 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift @@ -153,13 +153,21 @@ extension CostUsageScanner { { var allRowsTotal = CodexRowTokenTotals() guard rows.allSatisfy({ allRowsTotal.add($0) }) else { return nil } + if target == CodexRowTokenTotals() { + return [] + } if allRowsTotal == target { + let firstTokenRow = rows.firstIndex { + $0.input > 0 || $0.cached > 0 || $0.output > 0 + } + if let firstTokenRow, + rows[.. Date: Tue, 11 Aug 2026 08:26:31 -0700 Subject: [PATCH 09/10] fix: serialize compatible cost store adoption --- .../Vendored/CostUsage/CostUsageStore.swift | 65 ++++++++++--------- Tests/CodexBarTests/CostUsageStoreTests.swift | 30 +++++++++ 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift index bfa47eb910..eebe74b2ed 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift @@ -324,40 +324,32 @@ extension CostUsageStore { } private func validateExistingDatabase(_ database: OpaquePointer) throws { - let actualVersion = try Self.scalarInt(database, "PRAGMA user_version") - guard let storedHash = try Self.scalarText( - database, - "SELECT value FROM meta WHERE key = 'parser_hash'") - else { throw StoreError.incompatibleSchema } - let isCurrent = actualVersion == Int64(self.expectedSchemaVersion) && storedHash == self.expectedParserHash - let predecessorVersion = Self.combinedSchemaVersion( - base: Self.baseSchemaVersion, - parserHash: storedHash) - let canAdoptPredecessor = self.expectedParserHash == CodexParserHash.value - && self.expectedSchemaVersion == Self.schemaVersion - && Self.compatiblePredecessorParserHashes.contains(storedHash) - && actualVersion == Int64(predecessorVersion) - guard isCurrent || canAdoptPredecessor else { throw StoreError.incompatibleSchema } - guard try Self.scalarText(database, "PRAGMA quick_check") == "ok" else { - throw StoreError.invalidData - } - guard try Self.scalarInt(database, "PRAGMA auto_vacuum") == 2 else { - throw StoreError.incompatibleSchema - } - if canAdoptPredecessor { - try self.adoptCompatiblePredecessor(database) - } - } - - private func adoptCompatiblePredecessor(_ database: OpaquePointer) throws { try Self.execute(database, "BEGIN IMMEDIATE") do { - let statement = try Self.prepare(database, "UPDATE meta SET value = ? WHERE key = 'parser_hash'") - defer { sqlite3_finalize(statement) } - Self.bind(self.expectedParserHash, to: statement, at: 1) - try Self.stepDone(statement, database: database) - guard sqlite3_changes(database) == 1 else { throw StoreError.incompatibleSchema } - try Self.execute(database, "PRAGMA user_version = \(self.expectedSchemaVersion)") + let actualVersion = try Self.scalarInt(database, "PRAGMA user_version") + guard let storedHash = try Self.scalarText( + database, + "SELECT value FROM meta WHERE key = 'parser_hash'") + else { throw StoreError.incompatibleSchema } + let isCurrent = actualVersion == Int64(self.expectedSchemaVersion) + && storedHash == self.expectedParserHash + let predecessorVersion = Self.combinedSchemaVersion( + base: Self.baseSchemaVersion, + parserHash: storedHash) + let canAdoptPredecessor = self.expectedParserHash == CodexParserHash.value + && self.expectedSchemaVersion == Self.schemaVersion + && Self.compatiblePredecessorParserHashes.contains(storedHash) + && actualVersion == Int64(predecessorVersion) + guard isCurrent || canAdoptPredecessor else { throw StoreError.incompatibleSchema } + guard try Self.scalarText(database, "PRAGMA quick_check") == "ok" else { + throw StoreError.invalidData + } + guard try Self.scalarInt(database, "PRAGMA auto_vacuum") == 2 else { + throw StoreError.incompatibleSchema + } + if canAdoptPredecessor { + try self.adoptCompatiblePredecessor(database) + } try Self.execute(database, "COMMIT") } catch { try? Self.execute(database, "ROLLBACK") @@ -365,6 +357,15 @@ extension CostUsageStore { } } + private func adoptCompatiblePredecessor(_ database: OpaquePointer) throws { + let statement = try Self.prepare(database, "UPDATE meta SET value = ? WHERE key = 'parser_hash'") + defer { sqlite3_finalize(statement) } + Self.bind(self.expectedParserHash, to: statement, at: 1) + try Self.stepDone(statement, database: database) + guard sqlite3_changes(database) == 1 else { throw StoreError.incompatibleSchema } + try Self.execute(database, "PRAGMA user_version = \(self.expectedSchemaVersion)") + } + private func createSchema(_ database: OpaquePointer) throws { try Self.execute(database, Self.schemaSQL) try Self.execute(database, "PRAGMA user_version = \(self.expectedSchemaVersion)") diff --git a/Tests/CodexBarTests/CostUsageStoreTests.swift b/Tests/CodexBarTests/CostUsageStoreTests.swift index 05183b44e7..7e47479fe4 100644 --- a/Tests/CodexBarTests/CostUsageStoreTests.swift +++ b/Tests/CodexBarTests/CostUsageStoreTests.swift @@ -656,6 +656,36 @@ extension CostUsageStoreTests { "SELECT COUNT(*) FROM meta WHERE key = 'parser_hash' AND value = '\(CodexParserHash.value)'") == 1) } + @Test(.timeLimit(.minutes(1))) + func `concurrent predecessor adoption never rebuilds a valid store`() async throws { + let fixture = try StoreFixture() + defer { fixture.remove() } + let predecessorHash = try #require(CostUsageStore.compatiblePredecessorParserHashes.first) + let predecessorVersion = CostUsageStore.combinedSchemaVersion( + base: CostUsageStore.baseSchemaVersion, + parserHash: predecessorHash) + let predecessor = CostUsageStore( + cacheRoot: fixture.root, + schemaVersion: predecessorVersion, + parserHash: predecessorHash) + let metadata = Self.metadata() + #expect(await predecessor.setMetadata(metadata)) + + let stores = (0..<16).map { _ in CostUsageStore(cacheRoot: fixture.root) } + await withTaskGroup(of: Void.self) { group in + for store in stores { + group.addTask { + #expect(await store.fetchMetadata() == metadata) + #expect(await store.rebuildCount == 0) + } + } + } + let connection = try SQLiteTestConnection(url: fixture.databaseURL, readOnly: true) + #expect(try connection.scalarInt("PRAGMA user_version") == Int64(CostUsageStore.schemaVersion)) + #expect(try connection.scalarInt( + "SELECT COUNT(*) FROM meta WHERE key = 'parser_hash' AND value = '\(CodexParserHash.value)'") == 1) + } + @Test func `version mismatch drops and recreates`() async throws { let fixture = try StoreFixture() From b392c5669162d1f7c40c39b2b123395dd0747d63 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 11:20:16 -0700 Subject: [PATCH 10/10] fix: preserve locked cost usage stores --- .../Vendored/CostUsage/CostUsageStore.swift | 72 +++++++--- .../CostUsageStoreOpenLockTests.swift | 123 ++++++++++++++++++ 2 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 Tests/CodexBarTests/CostUsageStoreOpenLockTests.swift diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift index eebe74b2ed..ce182b8802 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore.swift @@ -288,6 +288,7 @@ extension CostUsageStore { self.connection = SQLiteConnection(handle: opened) return opened } catch { + guard Self.shouldRebuild(after: error) else { throw error } self.rebuildDatabase(reason: "open failed: \(error)") guard let database = self.connection?.handle else { throw error } return database @@ -324,30 +325,31 @@ extension CostUsageStore { } private func validateExistingDatabase(_ database: OpaquePointer) throws { - try Self.execute(database, "BEGIN IMMEDIATE") + let state: (isCurrent: Bool, canAdoptPredecessor: Bool) + try Self.execute(database, "BEGIN") do { - let actualVersion = try Self.scalarInt(database, "PRAGMA user_version") - guard let storedHash = try Self.scalarText( - database, - "SELECT value FROM meta WHERE key = 'parser_hash'") - else { throw StoreError.incompatibleSchema } - let isCurrent = actualVersion == Int64(self.expectedSchemaVersion) - && storedHash == self.expectedParserHash - let predecessorVersion = Self.combinedSchemaVersion( - base: Self.baseSchemaVersion, - parserHash: storedHash) - let canAdoptPredecessor = self.expectedParserHash == CodexParserHash.value - && self.expectedSchemaVersion == Self.schemaVersion - && Self.compatiblePredecessorParserHashes.contains(storedHash) - && actualVersion == Int64(predecessorVersion) - guard isCurrent || canAdoptPredecessor else { throw StoreError.incompatibleSchema } - guard try Self.scalarText(database, "PRAGMA quick_check") == "ok" else { - throw StoreError.invalidData + state = try self.databaseCompatibilityState(database) + guard state.isCurrent || state.canAdoptPredecessor else { + throw StoreError.incompatibleSchema } - guard try Self.scalarInt(database, "PRAGMA auto_vacuum") == 2 else { + try Self.validateDatabaseIntegrity(database) + try Self.execute(database, "COMMIT") + } catch { + try? Self.execute(database, "ROLLBACK") + throw error + } + if state.isCurrent { return } + + try Self.execute(database, "BEGIN IMMEDIATE") + do { + // Another process may have adopted the predecessor while this connection waited + // for the writer lock. Re-read the compatibility state before changing metadata. + let lockedState = try self.databaseCompatibilityState(database) + guard lockedState.isCurrent || lockedState.canAdoptPredecessor else { throw StoreError.incompatibleSchema } - if canAdoptPredecessor { + try Self.validateDatabaseIntegrity(database) + if lockedState.canAdoptPredecessor { try self.adoptCompatiblePredecessor(database) } try Self.execute(database, "COMMIT") @@ -357,6 +359,36 @@ extension CostUsageStore { } } + private func databaseCompatibilityState(_ database: OpaquePointer) throws -> ( + isCurrent: Bool, + canAdoptPredecessor: Bool) + { + let actualVersion = try Self.scalarInt(database, "PRAGMA user_version") + guard let storedHash = try Self.scalarText( + database, + "SELECT value FROM meta WHERE key = 'parser_hash'") + else { throw StoreError.incompatibleSchema } + let isCurrent = actualVersion == Int64(self.expectedSchemaVersion) + && storedHash == self.expectedParserHash + let predecessorVersion = Self.combinedSchemaVersion( + base: Self.baseSchemaVersion, + parserHash: storedHash) + let canAdoptPredecessor = self.expectedParserHash == CodexParserHash.value + && self.expectedSchemaVersion == Self.schemaVersion + && Self.compatiblePredecessorParserHashes.contains(storedHash) + && actualVersion == Int64(predecessorVersion) + return (isCurrent, canAdoptPredecessor) + } + + private static func validateDatabaseIntegrity(_ database: OpaquePointer) throws { + guard try self.scalarText(database, "PRAGMA quick_check") == "ok" else { + throw StoreError.invalidData + } + guard try self.scalarInt(database, "PRAGMA auto_vacuum") == 2 else { + throw StoreError.incompatibleSchema + } + } + private func adoptCompatiblePredecessor(_ database: OpaquePointer) throws { let statement = try Self.prepare(database, "UPDATE meta SET value = ? WHERE key = 'parser_hash'") defer { sqlite3_finalize(statement) } diff --git a/Tests/CodexBarTests/CostUsageStoreOpenLockTests.swift b/Tests/CodexBarTests/CostUsageStoreOpenLockTests.swift new file mode 100644 index 0000000000..7ea2208eb3 --- /dev/null +++ b/Tests/CodexBarTests/CostUsageStoreOpenLockTests.swift @@ -0,0 +1,123 @@ +import Foundation +import Testing +@testable import CodexBarCore + +#if canImport(SQLite3) +import SQLite3 +#elseif canImport(CSQLite3) +import CSQLite3 +#endif + +extension CostUsageStoreTests { + @Test(.timeLimit(.minutes(1))) + func `current store opens read only while another process holds the write lock`() async throws { + let fixture = try OpenLockStoreFixture() + defer { fixture.remove() } + let seed = CostUsageStore(cacheRoot: fixture.root) + let file = Self.file(path: "/rollouts/current-lock.jsonl") + #expect(await seed.upsertFile(file)) + + let holder = try OpenLockSQLiteConnection(url: seed.databaseURL) + try holder.execute("BEGIN IMMEDIATE") + try holder.execute("INSERT OR REPLACE INTO meta(key, value) VALUES ('holder', '1')") + + let reader = CostUsageStore(cacheRoot: fixture.root) + #expect(await reader.fetchFile(path: file.path) == file) + #expect(await reader.rebuildCount == 0) + + try holder.execute("COMMIT") + #expect(await reader.fetchFile(path: file.path) == file) + #expect(await reader.rebuildCount == 0) + } + + @Test(.timeLimit(.minutes(1))) + func `locked compatible predecessor is preserved and adopts after retry`() async throws { + let fixture = try OpenLockStoreFixture() + defer { fixture.remove() } + let predecessorHash = try #require(CostUsageStore.compatiblePredecessorParserHashes.first) + let predecessorVersion = CostUsageStore.combinedSchemaVersion( + base: CostUsageStore.baseSchemaVersion, + parserHash: predecessorHash) + let predecessor = CostUsageStore( + cacheRoot: fixture.root, + schemaVersion: predecessorVersion, + parserHash: predecessorHash) + let file = Self.file(path: "/rollouts/predecessor-lock.jsonl") + #expect(await predecessor.upsertFile(file)) + + let holder = try OpenLockSQLiteConnection(url: predecessor.databaseURL) + try holder.execute("BEGIN IMMEDIATE") + try holder.execute("INSERT OR REPLACE INTO meta(key, value) VALUES ('holder', '1')") + + let current = CostUsageStore(cacheRoot: fixture.root) + #expect(await current.fetchFile(path: file.path) == nil) + #expect(await current.rebuildCount == 0) + #expect(FileManager.default.fileExists(atPath: current.databaseURL.path)) + + try holder.execute("COMMIT") + #expect(await current.fetchFile(path: file.path) == file) + #expect(await current.rebuildCount == 0) + } + + private static func file(path: String) -> CostUsageStoreFile { + CostUsageStoreFile( + path: path, + inode: 42, + mtimeUnixMs: 1000, + size: 500, + parsedBytes: 400, + anchor: CostUsageStoreValidationAnchor(indexedBytes: 400, windowStart: 144, sha256: "abc123"), + scanState: CostUsageStoreScanState( + targetSize: 500, + isComplete: true, + resumePayload: Data([1, 2, 3]), + tokenTimestampsMonotonic: true, + nextUsageRowIndex: 7, + lastModel: "gpt-5.6-sol", + lastTurnID: "turn-1", + fileIdentity: "1:42", + detailsPayload: Data([4, 5, 6])), + sessionID: "session-\(path)", + coverageSinceDay: "2026-08-01", + coverageUntilDay: "2026-08-01", + updatedAtUnixMs: 10) + } +} + +private struct OpenLockStoreFixture { + let root: URL + + init() throws { + self.root = FileManager.default.temporaryDirectory + .appendingPathComponent("CodexBar-CostUsageStoreOpenLockTests-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: self.root, withIntermediateDirectories: true) + } + + func remove() { + try? FileManager.default.removeItem(at: self.root) + } +} + +private final class OpenLockSQLiteConnection { + enum TestError: Error { + case sqlite(Int32) + } + + private var database: OpaquePointer? + + init(url: URL) throws { + let result = sqlite3_open_v2(url.path, &self.database, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil) + guard result == SQLITE_OK else { throw TestError.sqlite(result) } + } + + deinit { + if let database { + sqlite3_close_v2(database) + } + } + + func execute(_ sql: String) throws { + let result = sqlite3_exec(self.database, sql, nil, nil, nil) + guard result == SQLITE_OK else { throw TestError.sqlite(result) } + } +}