#if canImport(CryptoKit) import CryptoKit #else import Crypto #endif import Dispatch import Foundation // swiftlint:disable type_body_length file_length enum CostUsageScanner { static let codexProjectMetadataVersion = 1 typealias CancellationCheck = () throws -> Void static let log = CodexBarLog.logger(LogCategories.tokenCost) static let codexActiveSessionLookbackDays = 30 static let costScale = 1_000_000_000.0 enum ClaudeLogProviderFilter { case all case vertexAIOnly case excludeVertexAI } struct Options { var codexSessionsRoot: URL? var claudeProjectsRoots: [URL]? var cacheRoot: URL? var codexTraceDatabaseURL: URL? var refreshMinIntervalSeconds: TimeInterval = 60 var claudeLogProviderFilter: ClaudeLogProviderFilter = .all /// Force a full rescan, ignoring per-file cache and incremental offsets. var forceRescan: Bool = false init( codexSessionsRoot: URL? = nil, claudeProjectsRoots: [URL]? = nil, cacheRoot: URL? = nil, codexTraceDatabaseURL: URL? = nil, claudeLogProviderFilter: ClaudeLogProviderFilter = .all, forceRescan: Bool = false) { self.codexSessionsRoot = codexSessionsRoot self.claudeProjectsRoots = claudeProjectsRoots self.cacheRoot = cacheRoot self.codexTraceDatabaseURL = codexTraceDatabaseURL self.claudeLogProviderFilter = claudeLogProviderFilter self.forceRescan = forceRescan } } struct CodexParseResult { let days: [String: [String: [Int]]] var parsedBytes: Int64 let lastModel: String? let lastTotals: CostUsageCodexTotals? let lastCountedTotals: CostUsageCodexTotals? let lastRawTotalsBaseline: CostUsageCodexTotals? let lastRawTotalsWatermark: CostUsageCodexTotals? let seenRawTotals: [CostUsageCodexTotals] let hasDivergentTotals: Bool let hasInterleavedTotals: Bool let lastCodexTurnID: String? let sessionId: String? let forkedFromId: String? let projectPath: String? let rows: [CodexUsageRow] } struct CodexUsageRow: Codable, Equatable { let day: String let model: String let turnID: String? let eventIndex: Int? let input: Int let cached: Int let output: Int } struct CodexScanState { var contributingSessionIds: Set = [] var seenFileIds: Set = [] var seenCodexUsageRowKeys: Set = [] } struct CodexScannedSession { let id: String? let contributedUsage: Bool init(id: String?, days: [String: [String: [Int]]]) { self.id = id self.contributedUsage = !days.isEmpty } } private struct CodexTimestampedTotals { let timestamp: String let date: Date? let totals: CostUsageCodexTotals } enum CodexForkBaseline { case resolved(CostUsageCodexTotals?) case unresolved } private static func codexTotalsEqual(_ lhs: CostUsageCodexTotals?, _ rhs: CostUsageCodexTotals?) -> Bool { lhs?.input == rhs?.input && lhs?.cached == rhs?.cached && lhs?.output == rhs?.output } private static func codexTotalsAtLeast(_ lhs: CostUsageCodexTotals, _ rhs: CostUsageCodexTotals) -> Bool { lhs.input >= rhs.input && lhs.cached >= rhs.cached && lhs.output >= rhs.output } private static func codexTotalsAtMost(_ lhs: CostUsageCodexTotals, _ rhs: CostUsageCodexTotals) -> Bool { lhs.input <= rhs.input && lhs.cached <= rhs.cached && lhs.output <= rhs.output } private static func codexShouldPreferTotalDelta( rawBaseline: CostUsageCodexTotals?, currentTotal: CostUsageCodexTotals, totalDelta: CostUsageCodexTotals, lastDelta: CostUsageCodexTotals, sawDivergentTotals: Bool) -> Bool { guard !sawDivergentTotals, let rawBaseline else { return false } return Self.codexTotalsAtLeast(currentTotal, rawBaseline) && Self.codexTotalsAtMost(totalDelta, lastDelta) } private static func codexAddTotals( _ lhs: CostUsageCodexTotals, _ rhs: CostUsageCodexTotals) -> CostUsageCodexTotals { CostUsageCodexTotals( input: lhs.input + rhs.input, cached: lhs.cached + rhs.cached, output: lhs.output + rhs.output) } private static func codexMinTotals( _ lhs: CostUsageCodexTotals, _ rhs: CostUsageCodexTotals) -> CostUsageCodexTotals { CostUsageCodexTotals( input: min(lhs.input, rhs.input), cached: min(lhs.cached, rhs.cached), output: min(lhs.output, rhs.output)) } private static func codexTotalDelta( from baseline: CostUsageCodexTotals?, to current: CostUsageCodexTotals) -> CostUsageCodexTotals { let baseline = baseline ?? .init(input: 0, cached: 0, output: 0) return CostUsageCodexTotals( input: max(0, current.input - baseline.input), cached: max(0, current.cached - baseline.cached), output: max(0, current.output - baseline.output)) } private static func codexDivergentTotalDelta( rawBaseline: CostUsageCodexTotals?, countedBaseline: CostUsageCodexTotals?, current: CostUsageCodexTotals) -> CostUsageCodexTotals { let rawBaseline = rawBaseline ?? .init(input: 0, cached: 0, output: 0) let countedBaseline = countedBaseline ?? .init(input: 0, cached: 0, output: 0) func delta(raw: Int, counted: Int, current: Int) -> Int { if current >= raw { return max(0, current - raw) } return max(0, current - counted) } return CostUsageCodexTotals( input: delta(raw: rawBaseline.input, counted: countedBaseline.input, current: current.input), cached: delta(raw: rawBaseline.cached, counted: countedBaseline.cached, current: current.cached), output: delta(raw: rawBaseline.output, counted: countedBaseline.output, current: current.output)) } private static func codexMaxTotals( _ lhs: CostUsageCodexTotals?, _ rhs: CostUsageCodexTotals) -> CostUsageCodexTotals { guard let lhs else { return rhs } return CostUsageCodexTotals( input: max(lhs.input, rhs.input), cached: max(lhs.cached, rhs.cached), output: max(lhs.output, rhs.output)) } /// Post-latch totals containment for interleaved cumulative counters (issue #2037 Phase 1). /// /// - When `current` is below the watermark, resume from the counted baseline so #968-style /// recovery still works (`current - counted`). /// - When `current` is at/above the watermark, advance from `max(watermark, counted)` so a /// high/low lineage flip cannot re-count the gap between lineages. private static func codexContainedTotalDelta( watermark: CostUsageCodexTotals?, counted: CostUsageCodexTotals?, current: CostUsageCodexTotals) -> CostUsageCodexTotals { let watermark = watermark ?? .init(input: 0, cached: 0, output: 0) let counted = counted ?? .init(input: 0, cached: 0, output: 0) func component(water: Int, counted: Int, current: Int) -> Int { if current >= water { return max(0, current - max(water, counted)) } return max(0, current - counted) } return CostUsageCodexTotals( input: component(water: watermark.input, counted: counted.input, current: current.input), cached: component(water: watermark.cached, counted: counted.cached, current: current.cached), output: component(water: watermark.output, counted: counted.output, current: current.output)) } /// Post-latch event delta: contained totals growth, optionally capped by `last`. /// /// `last` alone must never increase counted usage when the contained totals delta is zero /// (smaller lineage below the watermark is an accepted Phase 1 undercount). private static func codexPostLatchEventDelta( watermark: CostUsageCodexTotals?, counted: CostUsageCodexTotals?, current: CostUsageCodexTotals, adjustedLast: CostUsageCodexTotals?) -> CostUsageCodexTotals { let contained = Self.codexContainedTotalDelta( watermark: watermark, counted: counted, current: current) guard let adjustedLast else { return contained } return Self.codexMinTotals(adjustedLast, contained) } /// Shared accounting guard for cumulative Codex token counters (issue #2037). /// /// Ultra-mode sessions interleave cumulative snapshots from several fork lineages inside one /// session file. The tracker keeps a monotonic high watermark (never lowered). After a drop /// latches interleaved mode, deltas use `codexPostLatchEventDelta` so gap recounting is /// impossible. `seenRawTotals` is an optional precision optimization for exact re-emissions; /// correctness does not depend on it once post-latch containment is active. struct CodexTotalsTracker { static let seenRawTotalsLimit = 64 private(set) var watermark: CostUsageCodexTotals? private(set) var seenRawTotals: [CostUsageCodexTotals] private(set) var sawInterleavedTotals: Bool init( watermark: CostUsageCodexTotals? = nil, seenRawTotals: [CostUsageCodexTotals] = [], sawInterleavedTotals: Bool = false) { self.watermark = watermark self.seenRawTotals = Array(seenRawTotals.suffix(Self.seenRawTotalsLimit)) self.sawInterleavedTotals = sawInterleavedTotals } func isSeen(_ totals: CostUsageCodexTotals) -> Bool { self.seenRawTotals.contains(totals) } /// Latches interleaved mode when any component of an observed cumulative snapshot drops /// strictly below the watermark. A monotonic counter cannot decrease, so a drop means either /// a second lineage or a reset; both must stop trusting gap-sized totals deltas. mutating func latchIfBelowWatermark(_ totals: CostUsageCodexTotals) { guard let watermark = self.watermark else { return } if totals.input < watermark.input || totals.cached < watermark.cached || totals.output < watermark.output { self.sawInterleavedTotals = true } } /// Records an observed cumulative snapshot: raises the watermark and remembers the exact /// value for best-effort re-emission suppression. Call after computing the event's delta. mutating func commitObserved(_ totals: CostUsageCodexTotals) { self.raiseWatermark(to: totals) if !self.seenRawTotals.contains(totals) { self.seenRawTotals.append(totals) if self.seenRawTotals.count > Self.seenRawTotalsLimit { self.seenRawTotals.removeFirst(self.seenRawTotals.count - Self.seenRawTotalsLimit) } } } /// Raises the watermark for baseline assignments that are not observed raw snapshots /// (for example counted totals in last-only streams). Never lowers it. mutating func raiseWatermark(to totals: CostUsageCodexTotals) { self.watermark = CostUsageScanner.codexMaxTotals(self.watermark, totals) } } /// Cumulative-totals accounting for parent-session snapshot building. Applies the same /// containment policy as `parseCodexFileCancellable` so fork children inherit baselines /// computed under identical rules. private struct CodexSnapshotAccumulator { var countedTotals: CostUsageCodexTotals? var rawTotalsBaseline: CostUsageCodexTotals? var sawDivergentTotals = false var tracker = CodexTotalsTracker() /// Applies one token-count event and returns the counted cumulative totals afterwards. mutating func apply( last: CostUsageCodexTotals?, total: CostUsageCodexTotals?) -> CostUsageCodexTotals { let base = self.countedTotals ?? .init(input: 0, cached: 0, output: 0) if let total { // Best-effort exact re-emission suppression (precision only; containment is load-bearing). if self.tracker.isSeen(total) { return base } self.tracker.latchIfBelowWatermark(total) } let watermarkBaseline = self.tracker.watermark ?? self.rawTotalsBaseline defer { if let total { self.tracker.commitObserved(total) } } if let last { var countedDelta = last if let total { if self.tracker.sawInterleavedTotals { countedDelta = CostUsageScanner.codexPostLatchEventDelta( watermark: watermarkBaseline, counted: self.countedTotals, current: total, adjustedLast: last) } else { let totalDelta = CostUsageScanner.codexTotalDelta(from: watermarkBaseline, to: total) if CostUsageScanner.codexShouldPreferTotalDelta( rawBaseline: watermarkBaseline, currentTotal: total, totalDelta: totalDelta, lastDelta: last, sawDivergentTotals: self.sawDivergentTotals) { countedDelta = totalDelta } } let next = CostUsageScanner.codexAddTotals(base, countedDelta) self.countedTotals = next self.rawTotalsBaseline = total if !CostUsageScanner.codexTotalsEqual(total, next) { self.sawDivergentTotals = true } return next } let next = CostUsageScanner.codexAddTotals(base, countedDelta) self.countedTotals = next self.rawTotalsBaseline = next self.tracker.raiseWatermark(to: next) return next } if let total { let delta: CostUsageCodexTotals = if self.tracker.sawInterleavedTotals { CostUsageScanner.codexContainedTotalDelta( watermark: watermarkBaseline, counted: self.countedTotals, current: total) } else if self.sawDivergentTotals { CostUsageScanner.codexDivergentTotalDelta( rawBaseline: watermarkBaseline, countedBaseline: self.countedTotals, current: total) } else { CostUsageScanner.codexTotalDelta(from: watermarkBaseline, to: total) } let counted = CostUsageScanner.codexAddTotals(base, delta) self.countedTotals = counted self.rawTotalsBaseline = total if !CostUsageScanner.codexTotalsEqual(total, counted) { self.sawDivergentTotals = true } return counted } return base } } struct CodexScanResources { let fileIndex: CodexSessionFileIndex let inheritedResolver: CodexInheritedTotalsResolver let projectPathResolver: CodexCanonicalProjectPathResolver let modelsDevCatalog: ModelsDevCatalog? let modelsDevCacheRoot: URL? let priorityTurns: [String: CodexPriorityTurnMetadata] } struct CodexFileScanContext { let range: CostUsageDayRange let forceFullScan: Bool let dropDeferredCodexRows: Bool let requiresTurnIDCache: Bool let changedPriorityTurnIDs: Set let resources: CodexScanResources let checkCancellation: CancellationCheck? } final class CodexCanonicalProjectPathResolver { private var cache: [String: String] = [:] private let homeCodexWorktreesPrefix: String init(homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) { self.homeCodexWorktreesPrefix = homeDirectory .appendingPathComponent(".codex/worktrees", isDirectory: true) .standardizedFileURL .path } func canonicalProjectPath(for projectPath: String?) -> String? { guard let projectPath else { return nil } if let cached = self.cache[projectPath] { return cached } let resolved = self.resolveCanonicalProjectPath(projectPath) ?? projectPath self.cache[projectPath] = resolved return resolved } private func resolveCanonicalProjectPath(_ projectPath: String) -> String? { var isDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: projectPath, isDirectory: &isDirectory), isDirectory.boolValue else { return nil } guard let output = self.gitWorktreeList(projectPath: projectPath) else { return nil } let worktrees = output .split(separator: "\n") .compactMap { line -> String? in guard line.hasPrefix("worktree ") else { return nil } let rawPath = line.dropFirst("worktree ".count) return Self.standardizedAbsolutePath(String(rawPath)) } guard !worktrees.isEmpty else { return nil } return worktrees.first { !self.isEphemeralWorktreePath($0) } } private func gitWorktreeList(projectPath: String) -> String? { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/env") process.arguments = ["git", "-C", projectPath, "worktree", "list", "--porcelain"] let outputPipe = Pipe() let errorPipe = Pipe() process.standardOutput = outputPipe process.standardError = errorPipe let outputCapture = ProcessPipeCapture(pipe: outputPipe) let errorCapture = ProcessPipeCapture(pipe: errorPipe) let semaphore = DispatchSemaphore(value: 0) process.terminationHandler = { _ in semaphore.signal() } do { try process.run() } catch { return nil } outputCapture.start() errorCapture.start() if semaphore.wait(timeout: .now() + .seconds(1)) == .timedOut { process.terminate() outputCapture.stop() errorCapture.stop() return nil } let data = outputCapture.finishSynchronously(timeout: 0.1) errorCapture.stop() guard process.terminationStatus == 0 else { return nil } return String(data: data, encoding: .utf8) } private func isEphemeralWorktreePath(_ path: String) -> Bool { path == self.homeCodexWorktreesPrefix || path.hasPrefix(self.homeCodexWorktreesPrefix + "/") || path.hasSuffix("/.codex/worktrees") || path.contains("/.codex/worktrees/") || path == "/private/tmp" || path.hasPrefix("/private/tmp/") } private static func standardizedAbsolutePath(_ path: String) -> String? { let expanded = (path as NSString).expandingTildeInPath guard expanded.hasPrefix("/") else { return nil } return URL(fileURLWithPath: expanded, isDirectory: true).standardizedFileURL.path } } struct CodexRefreshPlan { let refreshMs: Int64 let roots: [URL] let rootsFingerprint: [String: Int64] let rootsChanged: Bool let windowExpanded: Bool let needsCostCacheMigration: Bool let needsProjectMetadataMigration: Bool let modelsDevCatalog: ModelsDevCatalog? let codexPricingKey: String let codexPriorityMetadataKey: String let hasPriorityMetadata: Bool let priorityTurns: [String: CodexPriorityTurnMetadata] let priorityTurnKeys: [String: String] let priorityTurnIDsByDay: [String: [String]] let pricingChanged: Bool let priorityMetadataChanged: Bool let priorityTurnsChanged: Bool let needsTurnIDCacheMigration: Bool let changedPriorityTurnIDs: Set let shouldRefresh: Bool } final class CodexSessionFileIndex { private let files: [URL] private let filePaths: Set private let roots: [URL] private let checkCancellation: CancellationCheck? private var nextUnindexedFile = 0 private var didIndexRoots = false private var fileURLBySessionId: [String: URL] = [:] private var missingSessionIds: Set = [] init( files: [URL], roots: [URL], cachedSessionFiles: [String: URL] = [:], checkCancellation: CancellationCheck? = nil) { self.files = files self.filePaths = Set(files.map(\.path)) self.roots = roots self.fileURLBySessionId = cachedSessionFiles self.checkCancellation = checkCancellation } func remember(fileURL: URL, sessionId: String?) { guard let sessionId, !sessionId.isEmpty else { return } self.fileURLBySessionId[sessionId] = fileURL } func fileURL(for sessionId: String) throws -> URL? { if let cached = self.fileURLBySessionId[sessionId] { return cached } if self.missingSessionIds.contains(sessionId) { return nil } while self.nextUnindexedFile < self.files.count { try self.checkCancellation?() let fileURL = self.files[self.nextUnindexedFile] self.nextUnindexedFile += 1 guard let indexedSessionId = try CostUsageScanner.parseCodexSessionIdentifier( fileURL: fileURL, checkCancellation: self.checkCancellation) else { continue } self.fileURLBySessionId[indexedSessionId] = fileURL if indexedSessionId == sessionId { return fileURL } } if !self.didIndexRoots { try self.indexRoots() if let indexed = self.fileURLBySessionId[sessionId] { return indexed } } self.missingSessionIds.insert(sessionId) return nil } private func indexRoots() throws { self.didIndexRoots = true guard !self.roots.isEmpty else { return } for root in self.roots { try self.checkCancellation?() guard let enumerator = FileManager.default.enumerator( at: root, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { continue } while let fileURL = enumerator.nextObject() as? URL { try self.checkCancellation?() guard fileURL.pathExtension.lowercased() == "jsonl" else { continue } guard !self.filePaths.contains(fileURL.path) else { continue } guard let indexedSessionId = try CostUsageScanner.parseCodexSessionIdentifier( fileURL: fileURL, checkCancellation: self.checkCancellation) else { continue } self.fileURLBySessionId[indexedSessionId] = fileURL } } } } final class CodexInheritedTotalsResolver { private let fileIndex: CodexSessionFileIndex private let checkCancellation: CancellationCheck? private var snapshotsBySessionId: [String: [CodexTimestampedTotals]] = [:] init(fileIndex: CodexSessionFileIndex, checkCancellation: CancellationCheck?) { self.fileIndex = fileIndex self.checkCancellation = checkCancellation } func inheritedTotals(for sessionId: String, atOrBefore cutoffTimestamp: String) throws -> CodexForkBaseline { guard !cutoffTimestamp.isEmpty else { CostUsageScanner.log.warning( "Codex cost usage fork timestamp missing; treating parent baseline as unresolved", metadata: ["sessionId": sessionId]) return .unresolved } let cutoffDate = CostUsageScanner.dateFromTimestamp(cutoffTimestamp) if cutoffDate == nil { CostUsageScanner.log.warning( "Codex cost usage could not parse fork timestamp; falling back to lexical comparison", metadata: ["sessionId": sessionId, "timestamp": cutoffTimestamp]) } guard let snapshots = try self.snapshots(for: sessionId) else { return .unresolved } var inherited: CostUsageCodexTotals? for snapshot in snapshots { let isAtOrBefore: Bool = if let snapshotDate = snapshot.date, let cutoffDate { snapshotDate <= cutoffDate } else { snapshot.timestamp <= cutoffTimestamp } if isAtOrBefore { inherited = snapshot.totals } } return .resolved(inherited) } private func snapshots(for sessionId: String) throws -> [CodexTimestampedTotals]? { if let cached = self.snapshotsBySessionId[sessionId] { return cached } try self.checkCancellation?() guard let fileURL = try self.fileIndex.fileURL(for: sessionId) else { CostUsageScanner.log.warning( "Codex cost usage parent session file not found", metadata: ["sessionId": sessionId]) return nil } let parsed = try CostUsageScanner.parseCodexTokenSnapshots( fileURL: fileURL, checkCancellation: self.checkCancellation) guard let parsedSessionId = parsed.sessionId else { CostUsageScanner.log.warning( "Codex cost usage parent session missing session metadata", metadata: ["sessionId": sessionId, "path": fileURL.path]) return nil } if parsedSessionId != sessionId { CostUsageScanner.log.warning( "Codex cost usage parent session resolved to mismatched session id", metadata: [ "requestedSessionId": sessionId, "resolvedSessionId": parsedSessionId, "path": fileURL.path, ]) return nil } self.snapshotsBySessionId[sessionId] = parsed.snapshots return parsed.snapshots } } struct ClaudeParseResult { let days: [String: [String: [Int]]] let rows: [ClaudeUsageRow] let parsedBytes: Int64 } enum ClaudePathRole: String, Codable { case parent case subagent } struct ClaudeUsageRow: Codable { let dayKey: String let model: String let sessionId: String? let messageId: String? let requestId: String? let timestampUnixMs: Int64? let isSidechain: Bool let pathRole: ClaudePathRole let input: Int let cacheRead: Int let cacheCreate: Int let cacheCreate1h: Int? let output: Int let costNanos: Int let costPriced: Bool? } static func loadDailyReport( provider: UsageProvider, since: Date, until: Date, now: Date = Date(), options: Options = Options()) -> CostUsageDailyReport { ( try? self.loadDailyReportCancellable( provider: provider, since: since, until: until, now: now, options: options, checkCancellation: nil)) ?? CostUsageDailyReport(data: [], summary: nil) } static func loadDailyReportCancellable( provider: UsageProvider, since: Date, until: Date, now: Date = Date(), options: Options = Options(), checkCancellation: CancellationCheck?) throws -> CostUsageDailyReport { let range = CostUsageDayRange(since: since, until: until) let emptyReport = CostUsageDailyReport(data: [], summary: nil) try checkCancellation?() switch provider { case .codex: return try self.loadCodexDaily( range: range, now: now, options: options, checkCancellation: checkCancellation) case .claude: return try self.loadClaudeDaily( provider: .claude, range: range, now: now, options: options, checkCancellation: checkCancellation) case .vertexai: var filtered = options if filtered.claudeLogProviderFilter == .all { filtered.claudeLogProviderFilter = .vertexAIOnly } return try self.loadClaudeDaily( provider: .vertexai, range: range, now: now, options: filtered, checkCancellation: checkCancellation) case .openai, .azureopenai, .zai, .gemini, .antigravity, .cursor, .opencode, .opencodego, .alibaba, .alibabatokenplan, .factory, .copilot, .devin, .minimax, .manus, .kilo, .kiro, .kimi, .kimik2, .moonshot, .augment, .jetbrains, .amp, .ollama, .t3chat, .synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .sakana, .abacus, .mistral, .deepseek, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode, .qoder, .stepfun, .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .crossmodel, .clawrouter, .sub2api, .wayfinder: return emptyReport } } // MARK: - Day keys struct CostUsageDayRange { let sinceKey: String let untilKey: String let scanSinceKey: String let scanUntilKey: String init(since: Date, until: Date) { self.sinceKey = Self.dayKey(from: since) self.untilKey = Self.dayKey(from: until) self.scanSinceKey = Self.dayKey(from: Calendar.current.date(byAdding: .day, value: -1, to: since) ?? since) self.scanUntilKey = Self.dayKey(from: Calendar.current.date(byAdding: .day, value: 1, to: until) ?? until) } static func dayKey(from date: Date) -> String { let cal = Calendar.current let comps = cal.dateComponents([.year, .month, .day], from: date) let y = comps.year ?? 1970 let m = comps.month ?? 1 let d = comps.day ?? 1 return String(format: "%04d-%02d-%02d", y, m, d) } static func isInRange(dayKey: String, since: String, until: String) -> Bool { if dayKey < since { return false } if dayKey > until { return false } return true } } // MARK: - Codex private static func defaultCodexSessionsRoot(options: Options) -> URL { if let override = options.codexSessionsRoot { return override } let env = ProcessInfo.processInfo.environment["CODEX_HOME"]?.trimmingCharacters(in: .whitespacesAndNewlines) if let env, !env.isEmpty { return URL(fileURLWithPath: env).appendingPathComponent("sessions", isDirectory: true) } return FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent(".codex", isDirectory: true) .appendingPathComponent("sessions", isDirectory: true) } private static func codexSessionsRoots(options: Options) -> [URL] { let root = self.defaultCodexSessionsRoot(options: options) if let archived = self.codexArchivedSessionsRoot(sessionsRoot: root) { return [root, archived] } return [root] } private static func codexArchivedSessionsRoot(sessionsRoot: URL) -> URL? { guard sessionsRoot.lastPathComponent == "sessions" else { return nil } return sessionsRoot .deletingLastPathComponent() .appendingPathComponent("archived_sessions", isDirectory: true) } private static func listCodexSessionFiles( root: URL, scanSinceKey: String, scanUntilKey: String, includeRecursive: Bool) -> [URL] { let partitioned = self.listCodexSessionFilesByDatePartition( root: root, scanSinceKey: scanSinceKey, scanUntilKey: scanUntilKey) let flat = self.listCodexSessionFilesFlat(root: root, scanSinceKey: scanSinceKey, scanUntilKey: scanUntilKey) let recursive = includeRecursive ? self.listCodexLegacySessionFilesRecursive(root: root) : [] var seen: Set = [] var out: [URL] = [] for item in partitioned + flat + recursive where !seen.contains(item.path) { seen.insert(item.path) out.append(item) } return out } private static func cachedCodexSessionFiles( cache: CostUsageCache, range: CostUsageDayRange, roots: [URL], excludingPaths: Set) -> [URL] { cache.files.compactMap { path, usage in guard !excludingPaths.contains(path) else { return nil } let hasRelevantDay = usage.days.keys.contains { CostUsageDayRange.isInRange(dayKey: $0, since: range.scanSinceKey, until: range.scanUntilKey) } guard hasRelevantDay else { return nil } guard FileManager.default.fileExists(atPath: path) else { return nil } let fileURL = URL(fileURLWithPath: path) guard Self.isWithinCodexRoots(fileURL: fileURL, roots: roots) else { return nil } return fileURL } } private static func cachedCodexSessionIndex( cache: CostUsageCache, roots: [URL], knownExistingPaths: Set) -> [String: URL] { var out: [String: URL] = [:] for (path, usage) in cache.files { guard let sessionId = usage.sessionId, !sessionId.isEmpty else { continue } if knownExistingPaths.contains(path) { out[sessionId] = URL(fileURLWithPath: path) continue } guard FileManager.default.fileExists(atPath: path) else { continue } let fileURL = URL(fileURLWithPath: path) guard Self.isWithinCodexRoots(fileURL: fileURL, roots: roots) else { continue } out[sessionId] = fileURL } return out } private static func codexRootsFingerprint(_ roots: [URL]) -> [String: Int64] { var out: [String: Int64] = [:] for root in roots { out[root.standardizedFileURL.path] = 0 } return out } static func codexRootsFingerprint(options: Options) -> [String: Int64] { self.codexRootsFingerprint(self.codexSessionsRoots(options: options)) } /// Bump when the cost FORMULA changes (not the rates) so caches written by an older formula /// are invalidated and repriced. The pricing fingerprints below only capture rate constants, /// so formula-only fixes would otherwise reuse stale precomputed costs. private static let codexCostFormulaVersion = 2 private static func codexPricingKey(modelsDevArtifact: ModelsDevCacheArtifact?) -> String { CostUsagePricingKey.codex( modelsDevArtifact: modelsDevArtifact, formulaVersion: self.codexCostFormulaVersion) } private static func codexPriorityMetadataKey(databaseURL: URL?) -> String { let url = databaseURL ?? self.defaultCodexPriorityDatabaseURL() let path = url.standardizedFileURL.path return FileManager.default.fileExists(atPath: path) ? "sqlite:\(path)" : "missing:\(path)" } private static func codexPriorityMetadataChanged(old: String?, new: String) -> Bool { guard let old, old != new else { return false } return new.hasPrefix("sqlite:") } private static func codexPriorityTurnKeys( _ priorityTurns: [String: CodexPriorityTurnMetadata]) -> [String: String] { var partsByDay: [String: [String]] = [:] for (turnID, turn) in priorityTurns { guard let dayKey = self.codexPriorityDayKey(turn) else { continue } partsByDay[dayKey, default: []].append([ turnID, turn.model ?? "", turn.timestamp ?? "", turn.threadID ?? "", ].joined(separator: "|")) } var out: [String: String] = [:] for (dayKey, parts) in partsByDay { out[dayKey] = self.sha256Hex(Data(parts.sorted().joined(separator: "\n").utf8)) } return out } private static func codexPriorityTurnIDsByDay( _ priorityTurns: [String: CodexPriorityTurnMetadata]) -> [String: [String]] { var out: [String: Set] = [:] for (turnID, turn) in priorityTurns { guard let dayKey = self.codexPriorityDayKey(turn) else { continue } out[dayKey, default: []].insert(turnID) } return out.mapValues { $0.sorted() } } private static func codexPriorityDayKey(_ turn: CodexPriorityTurnMetadata) -> String? { guard let timestamp = turn.timestamp else { return nil } let dayKeyFromEpoch = Int64(timestamp).map { CostUsageDayRange.dayKey(from: Date(timeIntervalSince1970: TimeInterval($0))) } return dayKeyFromEpoch ?? self.dayKeyFromTimestamp(timestamp) ?? self.dayKeyFromParsedISO(timestamp) } private static func codexPriorityTurnKeysChanged( old: [String: String]?, new: [String: String], range: CostUsageDayRange) -> Bool { for dayKey in self.dayKeys(sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey) where old?[dayKey] != new[dayKey] { return true } return false } private static func changedPriorityTurnIDs( old: [String: [String]]?, new: [String: [String]], oldKeys: [String: String]?, newKeys: [String: String], range: CostUsageDayRange) -> Set { var out = Set() for dayKey in self.dayKeys(sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey) { let oldIDs = Set(old?[dayKey] ?? []) let newIDs = Set(new[dayKey] ?? []) if oldIDs != newIDs || oldKeys?[dayKey] != newKeys[dayKey] { out.formUnion(oldIDs) out.formUnion(newIDs) } } return out } private static func mergePriorityTurnKeys( existing: [String: String]?, new: [String: String], range: CostUsageDayRange, retainedSinceKey: String, retainedUntilKey: String) -> [String: String]? { var out = existing ?? [:] for dayKey in self.dayKeys(sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey) { out[dayKey] = new[dayKey] } out = out.filter { key, _ in CostUsageDayRange.isInRange(dayKey: key, since: retainedSinceKey, until: retainedUntilKey) } return out.isEmpty ? nil : out } private static func mergePriorityTurnIDsByDay( existing: [String: [String]]?, new: [String: [String]], range: CostUsageDayRange, retainedSinceKey: String, retainedUntilKey: String) -> [String: [String]]? { var out = existing ?? [:] for dayKey in self.dayKeys(sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey) { out[dayKey] = new[dayKey] ?? [] } out = out.filter { key, _ in CostUsageDayRange.isInRange(dayKey: key, since: retainedSinceKey, until: retainedUntilKey) } return out.isEmpty ? nil : out } private static func sha256Hex(_ data: Data) -> String { SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() } private static func listCodexRecentlyModifiedFiles( root: URL, scanSinceKey: String, scanUntilKey: String, modifiedSince: Date) -> [URL] { let lookbackSinceKey = self.dayKey(scanSinceKey, addingDays: -self.codexActiveSessionLookbackDays) ?? scanSinceKey let partitioned = self.listCodexSessionFilesByDatePartition( root: root, scanSinceKey: lookbackSinceKey, scanUntilKey: scanUntilKey) let partitionedModified = self.filterRecentlyModified(files: partitioned, modifiedSince: modifiedSince) let legacyRecursive = self.listCodexRecentlyModifiedFilesRecursive(root: root, modifiedSince: modifiedSince) var seen = Set(partitionedModified.map(\.path)) var out = partitionedModified for fileURL in legacyRecursive where !seen.contains(fileURL.path) { seen.insert(fileURL.path) out.append(fileURL) } return out } private static func filterRecentlyModified(files: [URL], modifiedSince: Date) -> [URL] { files.filter { fileURL in let values = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .contentModificationDateKey]) guard values?.isRegularFile == true else { return false } guard let modifiedAt = values?.contentModificationDate else { return false } return modifiedAt >= modifiedSince } } private static func isDatePartitionComponent(_ value: String, length: Int) -> Bool { value.count == length && value.allSatisfy(\.isNumber) } private static func dayKey(_ dayKey: String, addingDays days: Int) -> String? { guard let date = self.parseDayKey(dayKey) else { return nil } guard let shifted = Calendar.current.date(byAdding: .day, value: days, to: date) else { return nil } return CostUsageDayRange.dayKey(from: shifted) } private static func dayKeys(sinceKey: String, untilKey: String) -> [String] { guard let since = self.parseDayKey(sinceKey), self.parseDayKey(untilKey) != nil else { return sinceKey <= untilKey ? [sinceKey] : [] } var out: [String] = [] var cursor = since let calendar = Calendar.current while CostUsageDayRange.dayKey(from: cursor) <= untilKey { out.append(CostUsageDayRange.dayKey(from: cursor)) guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break } if next <= cursor { break } cursor = next } return out } private static func listCodexRecentlyModifiedFilesRecursive(root: URL, modifiedSince: Date) -> [URL] { guard let enumerator = FileManager.default.enumerator( at: root, includingPropertiesForKeys: [.isRegularFileKey, .contentModificationDateKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { return [] } var out: [URL] = [] while let fileURL = enumerator.nextObject() as? URL { guard fileURL.pathExtension.lowercased() == "jsonl" else { continue } let values = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .contentModificationDateKey]) guard values?.isRegularFile == true else { continue } guard let modifiedAt = values?.contentModificationDate, modifiedAt >= modifiedSince else { continue } out.append(fileURL) } return out } private static func isWithinCodexRoots(fileURL: URL, roots: [URL]) -> Bool { let filePath = fileURL.standardizedFileURL.path return roots.contains { root in let rootPath = root.standardizedFileURL.path if filePath == rootPath { return true } let prefix = rootPath.hasSuffix("/") ? rootPath : rootPath + "/" return filePath.hasPrefix(prefix) } } private static func listCodexSessionFilesByDatePartition( root: URL, scanSinceKey: String, scanUntilKey: String) -> [URL] { guard FileManager.default.fileExists(atPath: root.path) else { return [] } var out: [URL] = [] var date = Self.parseDayKey(scanSinceKey) ?? Date() let untilDate = Self.parseDayKey(scanUntilKey) ?? date while date <= untilDate { let comps = Calendar.current.dateComponents([.year, .month, .day], from: date) let y = String(format: "%04d", comps.year ?? 1970) let m = String(format: "%02d", comps.month ?? 1) let d = String(format: "%02d", comps.day ?? 1) let dayDir = root.appendingPathComponent(y, isDirectory: true) .appendingPathComponent(m, isDirectory: true) .appendingPathComponent(d, isDirectory: true) if let items = try? FileManager.default.contentsOfDirectory( at: dayDir, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles]) { for item in items where item.pathExtension.lowercased() == "jsonl" { out.append(item) } } date = Calendar.current.date(byAdding: .day, value: 1, to: date) ?? untilDate.addingTimeInterval(1) } return out } private static func listCodexSessionFilesFlat(root: URL, scanSinceKey: String, scanUntilKey: String) -> [URL] { guard FileManager.default.fileExists(atPath: root.path) else { return [] } guard let items = try? FileManager.default.contentsOfDirectory( at: root, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { return [] } var out: [URL] = [] for item in items where item.pathExtension.lowercased() == "jsonl" { if let dayKey = Self.dayKeyFromFilename(item.lastPathComponent) { if !CostUsageDayRange.isInRange(dayKey: dayKey, since: scanSinceKey, until: scanUntilKey) { continue } } out.append(item) } return out } private static func listCodexLegacySessionFilesRecursive(root: URL) -> [URL] { guard FileManager.default.fileExists(atPath: root.path) else { return [] } let rootPath = root.standardizedFileURL.path guard let enumerator = FileManager.default.enumerator( at: root, includingPropertiesForKeys: [.isDirectoryKey, .isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { return [] } var out: [URL] = [] while let item = enumerator.nextObject() as? URL { if Self.isCodexDatePartitionAncestor(item, rootPath: rootPath) { enumerator.skipDescendants() continue } guard item.pathExtension.lowercased() == "jsonl" else { continue } out.append(item) } return out } private static func isCodexDatePartitionAncestor(_ url: URL, rootPath: String) -> Bool { let path = url.standardizedFileURL.path guard path.hasPrefix(rootPath + "/") else { return false } let relative = String(path.dropFirst(rootPath.count + 1)) let parts = relative.split(separator: "/") guard parts.count == 1 else { return false } return Self.isDatePartitionComponent(String(parts[0]), length: 4) } private static let codexFilenameDateRegex = try? NSRegularExpression(pattern: "(\\d{4}-\\d{2}-\\d{2})") private static func dayKeyFromFilename(_ filename: String) -> String? { guard let regex = self.codexFilenameDateRegex else { return nil } let range = NSRange(filename.startIndex.. String? { guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else { return nil } return trimmed } static func codexTurnContextModel( payloadModel: String?, payloadModelName: String?, infoModel: String?, infoModelName: String?) -> String? { var sawCandidate = false for candidate in [payloadModel, payloadModelName, infoModel, infoModelName] { guard let candidate else { continue } sawCandidate = true if let model = self.codexModelEvidence(candidate) { return model } } // nil means the context omitted every model field; an empty value explicitly clears stale context. return sawCandidate ? "" : nil } private static func codexForkParentId(from payload: [String: Any]?) -> String? { guard let payload else { return nil } for key in ["forked_from_id", "forkedFromId", "parent_session_id", "parentSessionId"] { guard let value = payload[key] as? String else { continue } let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { return trimmed } } return nil } private static func codexForkParentId( from bytes: UnsafeBufferPointer, in payloadRange: Range) -> String? { for key in [ self.codexJSONFieldForkedFromId, self.codexJSONFieldForkedFromIdCamel, self.codexJSONFieldParentSessionId, self.codexJSONFieldParentSessionIdCamel, ] { guard let value = extractJSONByteStringField(key, from: bytes, in: payloadRange, atDepth: 1)? .trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { continue } return value } return nil } private static func codexTurnID(from bytes: UnsafeBufferPointer, in payloadRange: Range) -> String? { for key in [self.codexJSONFieldTurnId, self.codexJSONFieldTurnIdCamel, self.codexJSONFieldId] { if let value = extractJSONByteStringField(key, from: bytes, in: payloadRange, atDepth: 1), !value.isEmpty { return value } } if let infoRange = extractJSONByteObjectField(codexJSONFieldInfo, from: bytes, in: payloadRange, atDepth: 1) { for key in [self.codexJSONFieldTurnId, self.codexJSONFieldTurnIdCamel, self.codexJSONFieldId] { if let value = extractJSONByteStringField(key, from: bytes, in: infoRange, atDepth: 1), !value.isEmpty { return value } } } return nil } private static func codexSessionId( from bytes: UnsafeBufferPointer, in rootRange: Range, payloadRange: Range?) -> String? { if let payloadRange { for key in [self.codexJSONFieldSessionId, self.codexJSONFieldSessionIdCamel, self.codexJSONFieldId] { if let value = extractJSONByteStringField(key, from: bytes, in: payloadRange, atDepth: 1), !value.isEmpty { return value } } } for key in [Self.codexJSONFieldSessionId, Self.codexJSONFieldSessionIdCamel, Self.codexJSONFieldId] { if let value = Self.extractJSONByteStringField(key, from: bytes, in: rootRange, atDepth: 1), !value.isEmpty { return value } } return nil } static func normalizedCodexProjectPath(_ rawPath: String?) -> String? { guard let rawPath = rawPath?.trimmingCharacters(in: .whitespacesAndNewlines), !rawPath.isEmpty else { return nil } let expanded = (rawPath as NSString).expandingTildeInPath guard expanded.hasPrefix("/") else { return nil } return URL(fileURLWithPath: expanded, isDirectory: true).standardizedFileURL.path } private static func codexProjectPath( from bytes: UnsafeBufferPointer, payloadRange: Range?) -> String? { guard let payloadRange else { return nil } return Self.normalizedCodexProjectPath( Self.extractJSONByteStringField(Self.codexJSONFieldCwd, from: bytes, in: payloadRange, atDepth: 1)) } private static func codexTotals( from bytes: UnsafeBufferPointer, in objectRange: Range?) -> CostUsageCodexTotals? { guard let objectRange else { return nil } let input = max( 0, Self.extractJSONByteIntField(Self.codexJSONFieldInputTokens, from: bytes, in: objectRange, atDepth: 1) ?? 0) let cached = max( 0, Self.extractJSONByteIntField(Self.codexJSONFieldCachedInputTokens, from: bytes, in: objectRange, atDepth: 1) ?? Self.extractJSONByteIntField( Self.codexJSONFieldCacheReadInputTokens, from: bytes, in: objectRange, atDepth: 1) ?? 0) let output = max( 0, Self .extractJSONByteIntField(Self.codexJSONFieldOutputTokens, from: bytes, in: objectRange, atDepth: 1) ?? 0) return CostUsageCodexTotals(input: input, cached: cached, output: output) } private static func parseCodexFastLine(_ bytes: Data) -> CodexFastLine? { bytes.withUnsafeBytes { rawBytes in let rawBuffer = rawBytes.bindMemory(to: UInt8.self) guard !rawBuffer.isEmpty else { return nil } let objectRange = 0.. String? { try self.parseCodexSessionMetadata(fileURL: fileURL, checkCancellation: checkCancellation)?.sessionId } static let codexSessionMetadataMaxLineBytes = 256 * 1024 private static func parseCodexSessionMetadata( fileURL: URL, checkCancellation: CancellationCheck? = nil) throws -> CodexSessionMetadata? { let handle: FileHandle do { handle = try FileHandle(forReadingFrom: fileURL) } catch { self.log.warning( "Codex cost usage failed to open session file for session id parsing", metadata: ["path": fileURL.path, "error": error.localizedDescription]) return nil } defer { try? handle.close() } var buffer = Data() var discardingOversizedLine = false func parseSessionMetadata(from lineData: Data) -> CodexSessionMetadata? { guard !lineData.isEmpty else { return nil } if case let .sessionMeta(metadata) = Self.parseCodexFastLine(lineData) { return metadata } return autoreleasepool { guard let obj = (try? JSONSerialization.jsonObject(with: lineData)) as? [String: Any] else { return nil } guard obj["type"] as? String == "session_meta" else { return nil } let payload = obj["payload"] as? [String: Any] return CodexSessionMetadata( sessionId: payload?["session_id"] as? String ?? payload?["sessionId"] as? String ?? payload?["id"] as? String ?? obj["session_id"] as? String ?? obj["sessionId"] as? String ?? obj["id"] as? String, forkedFromId: Self.codexForkParentId(from: payload), forkTimestamp: payload?["timestamp"] as? String ?? obj["timestamp"] as? String, projectPath: Self.normalizedCodexProjectPath(payload?["cwd"] as? String)) } } do { var matchedMetadata: CodexSessionMetadata? while true { let reachedEOF = try autoreleasepool { () throws -> Bool in guard let chunk = try handle.read(upToCount: 64 * 1024), !chunk.isEmpty else { return true } try checkCancellation?() var segmentStart = chunk.startIndex while segmentStart < chunk.endIndex { let newlineIndex = chunk[segmentStart...].firstIndex(of: 0x0A) let segmentEnd = newlineIndex ?? chunk.endIndex if !discardingOversizedLine { let segmentCount = chunk.distance(from: segmentStart, to: segmentEnd) let remainingBytes = Self.codexSessionMetadataMaxLineBytes - buffer.count if segmentCount <= remainingBytes { buffer.append(contentsOf: chunk[segmentStart.. ( sessionId: String?, snapshots: [CodexTimestampedTotals]) { var sessionId: String? var accumulator = CodexSnapshotAccumulator() var snapshots: [CodexTimestampedTotals] = [] var warnedAboutUnparsedTimestamp = false func parsedSnapshotDate(timestamp: String) -> Date? { let date = Self.dateFromTimestamp(timestamp) if date == nil, !warnedAboutUnparsedTimestamp { warnedAboutUnparsedTimestamp = true self.log.warning( "Codex cost usage could not parse parent token snapshot timestamp; " + "falling back to lexical comparison", metadata: ["path": fileURL.path, "timestamp": timestamp]) } return date } func appendSnapshot(timestamp: String, last: CostUsageCodexTotals?, total: CostUsageCodexTotals?) { guard last != nil || total != nil else { return } let counted = accumulator.apply(last: last, total: total) snapshots.append(CodexTimestampedTotals( timestamp: timestamp, date: parsedSnapshotDate(timestamp: timestamp), totals: counted)) } do { _ = try CostUsageJsonl.scan( fileURL: fileURL, maxLineBytes: 512 * 1024, prefixBytes: 512 * 1024, checkCancellation: checkCancellation, onLine: { line in guard !line.bytes.isEmpty, !line.wasTruncated else { return } if let fastLine = Self.parseCodexFastLine(line.bytes) { switch fastLine { case let .sessionMeta(metadata): if sessionId == nil { sessionId = metadata.sessionId } case let .tokenCount(record): appendSnapshot(timestamp: record.timestamp, last: record.last, total: record.total) case .turnContext, .taskStarted: break } return } autoreleasepool { guard let obj = (try? JSONSerialization.jsonObject(with: line.bytes)) as? [String: Any] else { return } if obj["type"] as? String == "session_meta" { let payload = obj["payload"] as? [String: Any] if sessionId == nil { sessionId = payload?["session_id"] as? String ?? payload?["sessionId"] as? String ?? payload?["id"] as? String ?? obj["session_id"] as? String ?? obj["sessionId"] as? String ?? obj["id"] as? String } return } guard obj["type"] as? String == "event_msg" else { return } guard let payload = obj["payload"] as? [String: Any] else { return } guard payload["type"] as? String == "token_count" else { return } guard let info = payload["info"] as? [String: Any] else { return } guard let timestamp = obj["timestamp"] as? String else { return } func toInt(_ value: Any?) -> Int { if let number = value as? NSNumber { return number.intValue } return 0 } let total = (info["total_token_usage"] as? [String: Any]).map { CostUsageCodexTotals( input: toInt($0["input_tokens"]), cached: toInt($0["cached_input_tokens"] ?? $0["cache_read_input_tokens"]), output: toInt($0["output_tokens"])) } let last = (info["last_token_usage"] as? [String: Any]).map { CostUsageCodexTotals( input: max(0, toInt($0["input_tokens"])), cached: max(0, toInt($0["cached_input_tokens"] ?? $0["cache_read_input_tokens"])), output: max(0, toInt($0["output_tokens"]))) } appendSnapshot(timestamp: timestamp, last: last, total: total) } }) } catch is CancellationError { throw CancellationError() } catch { self.log.warning( "Codex cost usage failed while scanning parent token snapshots", metadata: ["path": fileURL.path, "error": error.localizedDescription]) } return (sessionId, snapshots) } static func parseCodexFile( fileURL: URL, range: CostUsageDayRange, startOffset: Int64 = 0, initialModel: String? = nil, initialTotals: CostUsageCodexTotals? = nil, initialRawTotalsBaseline: CostUsageCodexTotals? = nil, initialHasDivergentTotals: Bool = false, initialCodexTurnID: String? = nil, initialCodexUsageRowIndex: Int = 0, inheritedTotalsResolver: ((String, String) -> CodexForkBaseline)? = nil) -> CodexParseResult { let throwingResolver: ((String, String) throws -> CodexForkBaseline)? = inheritedTotalsResolver .map { resolver in { sessionId, timestamp in resolver(sessionId, timestamp) } } return ( try? Self.parseCodexFileCancellable( fileURL: fileURL, range: range, startOffset: startOffset, initialModel: initialModel, initialTotals: initialTotals, initialRawTotalsBaseline: initialRawTotalsBaseline, initialHasDivergentTotals: initialHasDivergentTotals, initialCodexTurnID: initialCodexTurnID, initialCodexUsageRowIndex: initialCodexUsageRowIndex, inheritedTotalsResolver: throwingResolver, checkCancellation: nil)) ?? CodexParseResult( days: [:], parsedBytes: startOffset, lastModel: initialModel, lastTotals: initialTotals, lastCountedTotals: initialTotals, lastRawTotalsBaseline: initialRawTotalsBaseline, lastRawTotalsWatermark: initialRawTotalsBaseline, seenRawTotals: [], hasDivergentTotals: initialHasDivergentTotals, hasInterleavedTotals: false, lastCodexTurnID: initialCodexTurnID, sessionId: nil, forkedFromId: nil, projectPath: nil, rows: []) } // swiftlint:disable:next cyclomatic_complexity function_body_length static func parseCodexFileCancellable( fileURL: URL, range: CostUsageDayRange, startOffset: Int64 = 0, initialModel: String? = nil, initialTotals: CostUsageCodexTotals? = nil, initialRawTotalsBaseline: CostUsageCodexTotals? = nil, initialRawTotalsWatermark: CostUsageCodexTotals? = nil, initialSeenRawTotals: [CostUsageCodexTotals] = [], initialHasDivergentTotals: Bool = false, initialHasInterleavedTotals: Bool = false, initialCodexTurnID: String? = nil, initialCodexUsageRowIndex: Int = 0, inheritedTotalsResolver: ((String, String) throws -> CodexForkBaseline)? = nil, checkCancellation: CancellationCheck? = nil) throws -> CodexParseResult { var currentModel = initialModel var previousTotals = initialTotals var sessionId: String? var forkedFromId: String? var projectPath: String? var inheritedTotals: CostUsageCodexTotals? var remainingInheritedTotals: CostUsageCodexTotals? var forkBaselineResolved = false var hasUnresolvedForkBaseline = false var unresolvedForkTotalWatermark: CostUsageCodexTotals? var currentTurnID = initialCodexTurnID var codexUsageRowIndex = initialCodexUsageRowIndex var rawTotalsBaseline = initialRawTotalsBaseline ?? initialTotals var sawDivergentTotals = initialHasDivergentTotals var tracker = CodexTotalsTracker( watermark: initialRawTotalsWatermark ?? initialRawTotalsBaseline ?? initialTotals, seenRawTotals: initialSeenRawTotals, sawInterleavedTotals: initialHasInterleavedTotals) var deferredError: Error? var days: [String: [String: [Int]]] = [:] var rows: [CodexUsageRow] = [] func add(dayKey: String, model: String, input: Int, cached: Int, output: Int) { guard CostUsageDayRange.isInRange(dayKey: dayKey, since: range.scanSinceKey, until: range.scanUntilKey) else { return } let normModel = CostUsagePricing.normalizeCodexModel(model) var dayModels = days[dayKey] ?? [:] var packed = dayModels[normModel] ?? [0, 0, 0] packed[0] = (packed[safe: 0] ?? 0) + input packed[1] = (packed[safe: 1] ?? 0) + cached packed[2] = (packed[safe: 2] ?? 0) + output dayModels[normModel] = packed days[dayKey] = dayModels } func resolveForkBaseline(parentSessionId: String, forkedAt: String) throws { guard !forkBaselineResolved else { return } guard let inheritedTotalsResolver else { return } forkBaselineResolved = true switch try inheritedTotalsResolver(parentSessionId, forkedAt) { case let .resolved(totals): inheritedTotals = totals remainingInheritedTotals = totals hasUnresolvedForkBaseline = false case .unresolved: hasUnresolvedForkBaseline = true } } func handleSessionMetadata(_ metadata: CodexSessionMetadata) throws { if sessionId == nil { sessionId = metadata.sessionId } if forkedFromId == nil { forkedFromId = metadata.forkedFromId } if projectPath == nil { projectPath = metadata.projectPath } if let forkedFromId { try resolveForkBaseline(parentSessionId: forkedFromId, forkedAt: metadata.forkTimestamp ?? "") } } // swiftlint:disable:next function_body_length func handleTokenCount(_ record: CodexTokenCountRecord) throws { guard let dayKey = Self.dayKeyFromTimestamp(record.timestamp) ?? Self.dayKeyFromParsedISO(record.timestamp) else { return } let model = Self.codexModelEvidence(currentModel) ?? Self.codexModelEvidence(record.model) ?? CostUsagePricing.codexUnattributedModel let total = record.total let last = record.last var deltaInput = 0 var deltaCached = 0 var deltaOutput = 0 func adjustedLastDelta(_ rawDelta: CostUsageCodexTotals) -> CostUsageCodexTotals { guard var remaining = remainingInheritedTotals else { return rawDelta } let adjusted = CostUsageCodexTotals( input: max(0, rawDelta.input - remaining.input), cached: max(0, rawDelta.cached - remaining.cached), output: max(0, rawDelta.output - remaining.output)) remaining.input = max(0, remaining.input - rawDelta.input) remaining.cached = max(0, remaining.cached - rawDelta.cached) remaining.output = max(0, remaining.output - rawDelta.output) remainingInheritedTotals = if remaining.input == 0, remaining.cached == 0, remaining.output == 0 { nil } else { remaining } return adjusted } // Fork children are measured against the parent-inherited baseline so every cumulative // comparison in this file happens on a single scale. let adjustedTotal: CostUsageCodexTotals? = total.map { rawTotals in guard let inheritedTotals, !hasUnresolvedForkBaseline else { return rawTotals } return CostUsageCodexTotals( input: max(0, rawTotals.input - inheritedTotals.input), cached: max(0, rawTotals.cached - inheritedTotals.cached), output: max(0, rawTotals.output - inheritedTotals.output)) } if let adjustedTotal { // Only committed observations enter the seen set. Replacing this with a bare // watermark-equality check would skip first-time fork baseline bookkeeping. // Post-latch containment remains the load-bearing overcount guard. if tracker.isSeen(adjustedTotal) { return } tracker.latchIfBelowWatermark(adjustedTotal) } let watermarkBaseline = tracker.watermark ?? rawTotalsBaseline defer { if let adjustedTotal { tracker.commitObserved(adjustedTotal) } } func totalsDerivedDelta(to currentTotals: CostUsageCodexTotals) -> CostUsageCodexTotals { if tracker.sawInterleavedTotals { return Self.codexContainedTotalDelta( watermark: watermarkBaseline, counted: previousTotals, current: currentTotals) } if sawDivergentTotals { return Self.codexDivergentTotalDelta( rawBaseline: watermarkBaseline, countedBaseline: previousTotals, current: currentTotals) } return Self.codexTotalDelta(from: watermarkBaseline, to: currentTotals) } func commitDelta(_ delta: CostUsageCodexTotals, rawBaseline: CostUsageCodexTotals) { deltaInput = delta.input deltaCached = delta.cached deltaOutput = delta.output let prev = previousTotals ?? .init(input: 0, cached: 0, output: 0) previousTotals = Self.codexAddTotals(prev, delta) rawTotalsBaseline = rawBaseline if !Self.codexTotalsEqual(rawTotalsBaseline, previousTotals) { sawDivergentTotals = true } } let handledUnresolvedForkTotal = hasUnresolvedForkBaseline && total != nil if hasUnresolvedForkBaseline, let total { // `unresolvedForkTotalWatermark` is a presence sentinel for "skip the first // unresolved-fork totals row"; delta baselines come from the global tracker. let currentRawTotals = total defer { unresolvedForkTotalWatermark = currentRawTotals } guard let last, unresolvedForkTotalWatermark != nil else { return } let adjustedDelta = Self.codexMinTotals( last, Self.codexTotalDelta(from: watermarkBaseline, to: currentRawTotals)) deltaInput = adjustedDelta.input deltaCached = adjustedDelta.cached deltaOutput = adjustedDelta.output let prev = previousTotals ?? .init(input: 0, cached: 0, output: 0) previousTotals = Self.codexAddTotals(prev, adjustedDelta) rawTotalsBaseline = previousTotals } if !handledUnresolvedForkTotal, let currentTotals = adjustedTotal, forkedFromId != nil, !hasUnresolvedForkBaseline { // Non-interleaved forks keep totals-only accounting (#1164 / 45b68c34). // After latch, use post-latch containment capped by last when present. let delta: CostUsageCodexTotals = if tracker.sawInterleavedTotals { Self.codexPostLatchEventDelta( watermark: watermarkBaseline, counted: previousTotals, current: currentTotals, adjustedLast: last.map { adjustedLastDelta($0) }) } else { totalsDerivedDelta(to: currentTotals) } commitDelta(delta, rawBaseline: currentTotals) remainingInheritedTotals = nil } else if !handledUnresolvedForkTotal, let last { let rawDelta = last let hadRemainingInheritedTotals = remainingInheritedTotals != nil var adjustedDelta = adjustedLastDelta(rawDelta) let prev = previousTotals ?? .init(input: 0, cached: 0, output: 0) if let currentTotals = adjustedTotal, !hasUnresolvedForkBaseline { if tracker.sawInterleavedTotals { adjustedDelta = Self.codexPostLatchEventDelta( watermark: watermarkBaseline, counted: previousTotals, current: currentTotals, adjustedLast: adjustedDelta) remainingInheritedTotals = nil } else { let totalDelta = Self.codexTotalDelta(from: watermarkBaseline, to: currentTotals) if !hadRemainingInheritedTotals, Self.codexShouldPreferTotalDelta( rawBaseline: watermarkBaseline, currentTotal: currentTotals, totalDelta: totalDelta, lastDelta: rawDelta, sawDivergentTotals: sawDivergentTotals) { adjustedDelta = totalDelta remainingInheritedTotals = nil } } commitDelta(adjustedDelta, rawBaseline: currentTotals) } else { let countedTotals = Self.codexAddTotals(prev, adjustedDelta) deltaInput = adjustedDelta.input deltaCached = adjustedDelta.cached deltaOutput = adjustedDelta.output previousTotals = countedTotals rawTotalsBaseline = countedTotals tracker.raiseWatermark(to: countedTotals) } } else if !handledUnresolvedForkTotal, let currentTotals = adjustedTotal { commitDelta(totalsDerivedDelta(to: currentTotals), rawBaseline: currentTotals) remainingInheritedTotals = nil } else if !handledUnresolvedForkTotal { return } if deltaInput == 0, deltaCached == 0, deltaOutput == 0 { return } let eventIndex = codexUsageRowIndex codexUsageRowIndex += 1 let normModel = CostUsagePricing.normalizeCodexModel(model) add( dayKey: dayKey, model: normModel, input: deltaInput, cached: deltaCached, output: deltaOutput) if CostUsageDayRange.isInRange( dayKey: dayKey, since: range.scanSinceKey, until: range.scanUntilKey) { rows.append(CodexUsageRow( day: dayKey, model: normModel, turnID: record.turnID ?? currentTurnID, eventIndex: eventIndex, input: deltaInput, cached: deltaCached, output: deltaOutput)) } } func handleFastLine(_ fastLine: CodexFastLine) throws { switch fastLine { case let .sessionMeta(metadata): try handleSessionMetadata(metadata) case let .turnContext(model): if let model { currentModel = model } case let .taskStarted(turnID): currentTurnID = turnID case let .tokenCount(record): try handleTokenCount(record) } } let maxLineBytes = 256 * 1024 let prefixBytes = maxLineBytes if startOffset == 0, let metadata = try Self.parseCodexSessionMetadata( fileURL: fileURL, checkCancellation: checkCancellation) { sessionId = metadata.sessionId forkedFromId = metadata.forkedFromId projectPath = metadata.projectPath if let forkedFromId = metadata.forkedFromId, inheritedTotals == nil { let forkedAt = metadata.forkTimestamp ?? "" try resolveForkBaseline(parentSessionId: forkedFromId, forkedAt: forkedAt) } } var parsedBytes: Int64 do { parsedBytes = try CostUsageJsonl.scan( fileURL: fileURL, offset: startOffset, maxLineBytes: maxLineBytes, prefixBytes: prefixBytes, checkCancellation: checkCancellation, onLine: { line in if deferredError != nil { return } guard !line.bytes.isEmpty else { return } if line.wasTruncated { // `turn_context` can carry very large prompts, but its model usually appears near the start. if let model = Self.extractCodexTurnContextModel(from: line.bytes) { currentModel = model } return } guard line.bytes.containsAscii(#""type":"event_msg""#) || line.bytes.containsAscii(#""type":"turn_context""#) || line.bytes.containsAscii(#""type":"session_meta""#) else { return } if line.bytes.containsAscii(#""type":"event_msg""#), !line.bytes.containsAscii(#""token_count""#), !line.bytes.containsAscii(#""task_started""#) { return } if let fastLine = Self.parseCodexFastLine(line.bytes) { do { try handleFastLine(fastLine) } catch { deferredError = error } return } autoreleasepool { guard let obj = (try? JSONSerialization.jsonObject(with: line.bytes)) as? [String: Any], let type = obj["type"] as? String else { return } if type == "session_meta" { let payload = obj["payload"] as? [String: Any] if sessionId == nil { sessionId = payload?["session_id"] as? String ?? payload?["sessionId"] as? String ?? payload?["id"] as? String ?? obj["session_id"] as? String ?? obj["sessionId"] as? String ?? obj["id"] as? String } if forkedFromId == nil { forkedFromId = Self.codexForkParentId(from: payload) } if projectPath == nil { projectPath = Self.normalizedCodexProjectPath(payload?["cwd"] as? String) } if let forkedFromId { let forkedAt = payload?["timestamp"] as? String ?? obj["timestamp"] as? String ?? "" do { try resolveForkBaseline(parentSessionId: forkedFromId, forkedAt: forkedAt) } catch { deferredError = error return } } return } guard let tsText = obj["timestamp"] as? String else { return } guard Self.dayKeyFromTimestamp(tsText) ?? Self.dayKeyFromParsedISO(tsText) != nil else { return } if type == "turn_context" { if let payload = obj["payload"] as? [String: Any] { let info = payload["info"] as? [String: Any] if let model = Self.codexTurnContextModel( payloadModel: payload["model"] as? String, payloadModelName: payload["model_name"] as? String, infoModel: info?["model"] as? String, infoModelName: info?["model_name"] as? String) { currentModel = model } } return } guard type == "event_msg" else { return } guard let payload = obj["payload"] as? [String: Any] else { return } if (payload["type"] as? String) == "task_started" { currentTurnID = Self.codexTurnID(from: payload) return } guard (payload["type"] as? String) == "token_count" else { return } let info = payload["info"] as? [String: Any] let modelFromInfo = Self.codexModelEvidence(info?["model"] as? String) ?? Self.codexModelEvidence(info?["model_name"] as? String) ?? Self.codexModelEvidence(payload["model"] as? String) ?? Self.codexModelEvidence(obj["model"] as? String) func toInt(_ v: Any?) -> Int { if let n = v as? NSNumber { return n.intValue } return 0 } func tokenTotals(_ usage: [String: Any]) -> CostUsageCodexTotals { CostUsageCodexTotals( input: max(0, toInt(usage["input_tokens"])), cached: max(0, toInt(usage["cached_input_tokens"] ?? usage["cache_read_input_tokens"])), output: max(0, toInt(usage["output_tokens"]))) } let record = CodexTokenCountRecord( timestamp: tsText, model: modelFromInfo, turnID: Self.codexTurnID(from: payload), last: (info?["last_token_usage"] as? [String: Any]).map(tokenTotals), total: (info?["total_token_usage"] as? [String: Any]).map(tokenTotals)) do { try handleTokenCount(record) } catch { deferredError = error } } }) if let deferredError { throw deferredError } } catch is CancellationError { throw CancellationError() } catch { self.log.warning( "Codex cost usage failed while scanning session file", metadata: ["path": fileURL.path, "error": error.localizedDescription]) parsedBytes = startOffset } return CodexParseResult( days: days, parsedBytes: parsedBytes, lastModel: currentModel, lastTotals: sawDivergentTotals && !Self.codexTotalsEqual(rawTotalsBaseline, previousTotals) ? nil : previousTotals, lastCountedTotals: previousTotals, lastRawTotalsBaseline: rawTotalsBaseline, lastRawTotalsWatermark: tracker.watermark, seenRawTotals: tracker.seenRawTotals, hasDivergentTotals: sawDivergentTotals && !Self.codexTotalsEqual(rawTotalsBaseline, previousTotals), hasInterleavedTotals: tracker.sawInterleavedTotals, lastCodexTurnID: currentTurnID, sessionId: sessionId, forkedFromId: forkedFromId, projectPath: projectPath, rows: rows) } private static func codexTurnID(from payload: [String: Any]) -> String? { if let turnID = payload["turn_id"] as? String ?? payload["turnId"] as? String ?? payload["id"] as? String { return turnID } if let info = payload["info"] as? [String: Any] { return info["turn_id"] as? String ?? info["turnId"] as? String ?? info["id"] as? String } return nil } private static func scanCodexFile( fileURL: URL, context: CodexFileScanContext, cache: inout CostUsageCache, state: inout CodexScanState) throws { try context.checkCancellation?() let metadata = Self.codexFileMetadata(fileURL: fileURL) if let fileId = metadata.fileId, state.seenFileIds.contains(fileId) { Self.dropCachedCodexFile(path: metadata.path, cached: cache.files[metadata.path], cache: &cache) return } let cached = cache.files[metadata.path] let input = CodexFileScanInput(fileURL: fileURL, metadata: metadata, cached: cached) if Self.keepCachedCodexFileIfFresh(input: input, context: context, cache: &cache, state: &state) { return } if try Self.appendCodexFileIncrementIfPossible(input: input, context: context, cache: &cache, state: &state) { return } try Self.rescanCodexFile(input: input, context: context, cache: &cache, state: &state) } private static func makeCodexRefreshPlan( cache: CostUsageCache, range: CostUsageDayRange, now: Date, nowMs: Int64, options: Options) -> CodexRefreshPlan { let refreshMs = Int64(max(0, options.refreshMinIntervalSeconds) * 1000) let roots = self.codexSessionsRoots(options: options) let rootsFingerprint = Self.codexRootsFingerprint(roots) let rootsChanged = cache.roots != rootsFingerprint let windowExpanded = Self.requestedWindowExpandsCache(range: range, cache: cache) let needsCostCacheMigration = cache.files.values.contains { Self.needsCodexCostCache($0, range: range) } let needsProjectMetadataMigration = cache.codexProjectMetadataVersion != Self.codexProjectMetadataVersion let modelsDevLoad = ModelsDevCache.load(now: now, cacheRoot: options.cacheRoot) let modelsDevCatalog = modelsDevLoad.artifact?.catalog let codexPricingKey = Self.codexPricingKey(modelsDevArtifact: modelsDevLoad.artifact) let codexPriorityMetadataKey = Self.codexPriorityMetadataKey(databaseURL: options.codexTraceDatabaseURL) let hasPriorityMetadata = codexPriorityMetadataKey.hasPrefix("sqlite:") let pricingChanged = cache.codexPricingKey != nil && cache.codexPricingKey != codexPricingKey let priorityMetadataChanged = Self.codexPriorityMetadataChanged( old: cache.codexPriorityMetadataKey, new: codexPriorityMetadataKey) let needsTurnIDCacheMigration = hasPriorityMetadata && cache.files.values.contains { $0.codexTurnIDs == nil && $0.touchesCodexScanWindow( sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey) } let shouldInspectPriorityTurns = options.forceRescan || windowExpanded || rootsChanged || needsCostCacheMigration || needsProjectMetadataMigration || needsTurnIDCacheMigration || pricingChanged || priorityMetadataChanged || refreshMs == 0 || cache.lastScanUnixMs == 0 || nowMs - cache.lastScanUnixMs > refreshMs let priorityTurns = shouldInspectPriorityTurns ? Self.codexPriorityTurns( databaseURL: options.codexTraceDatabaseURL, sinceDayKey: range.scanSinceKey, untilDayKey: range.scanUntilKey) : [:] let priorityTurnKeys = Self.codexPriorityTurnKeys(priorityTurns) let priorityTurnIDsByDay = Self.codexPriorityTurnIDsByDay(priorityTurns) let priorityTurnsChanged = shouldInspectPriorityTurns && hasPriorityMetadata && Self.codexPriorityTurnKeysChanged( old: cache.codexPriorityTurnKeys, new: priorityTurnKeys, range: range) let changedPriorityTurnIDs = shouldInspectPriorityTurns && hasPriorityMetadata ? Self.changedPriorityTurnIDs( old: cache.codexPriorityTurnIDsByDay, new: priorityTurnIDsByDay, oldKeys: cache.codexPriorityTurnKeys, newKeys: priorityTurnKeys, range: range) : [] let shouldRefresh = options.forceRescan || windowExpanded || rootsChanged || needsCostCacheMigration || needsProjectMetadataMigration || needsTurnIDCacheMigration || pricingChanged || priorityMetadataChanged || priorityTurnsChanged || refreshMs == 0 || cache.lastScanUnixMs == 0 || nowMs - cache.lastScanUnixMs > refreshMs return CodexRefreshPlan( refreshMs: refreshMs, roots: roots, rootsFingerprint: rootsFingerprint, rootsChanged: rootsChanged, windowExpanded: windowExpanded, needsCostCacheMigration: needsCostCacheMigration, needsProjectMetadataMigration: needsProjectMetadataMigration, modelsDevCatalog: modelsDevCatalog, codexPricingKey: codexPricingKey, codexPriorityMetadataKey: codexPriorityMetadataKey, hasPriorityMetadata: hasPriorityMetadata, priorityTurns: priorityTurns, priorityTurnKeys: priorityTurnKeys, priorityTurnIDsByDay: priorityTurnIDsByDay, pricingChanged: pricingChanged, priorityMetadataChanged: priorityMetadataChanged, priorityTurnsChanged: priorityTurnsChanged, needsTurnIDCacheMigration: needsTurnIDCacheMigration, changedPriorityTurnIDs: changedPriorityTurnIDs, shouldRefresh: shouldRefresh) } private static func loadCodexDaily( range: CostUsageDayRange, now: Date, options: Options, checkCancellation: CancellationCheck?) throws -> CostUsageDailyReport { var cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: options.cacheRoot) let nowMs = Int64(now.timeIntervalSince1970 * 1000) let plan = Self.makeCodexRefreshPlan(cache: cache, range: range, now: now, nowMs: nowMs, options: options) if plan.shouldRefresh { try checkCancellation?() if options.forceRescan { cache = CostUsageCache() } let cachedSinceKey = cache.scanSinceKey let cachedUntilKey = cache.scanUntilKey let shouldRunColdCacheLookback = cache.files.isEmpty || plan.rootsChanged let coldCacheLookbackStart = Self.parseDayKey(range.scanSinceKey) .map { Calendar.current.startOfDay(for: $0) } var seenPaths: Set = [] var files: [URL] = [] for root in plan.roots { let rootFiles = Self.listCodexSessionFiles( root: root, scanSinceKey: range.scanSinceKey, scanUntilKey: range.scanUntilKey, includeRecursive: options.forceRescan) for fileURL in rootFiles.sorted(by: { $0.path < $1.path }) where !seenPaths.contains(fileURL.path) { seenPaths.insert(fileURL.path) files.append(fileURL) } if shouldRunColdCacheLookback, let coldCacheLookbackStart { let recentlyModifiedFiles = Self.listCodexRecentlyModifiedFiles( root: root, scanSinceKey: range.scanSinceKey, scanUntilKey: range.scanUntilKey, modifiedSince: coldCacheLookbackStart) for fileURL in recentlyModifiedFiles.sorted(by: { $0.path < $1.path }) where !seenPaths.contains(fileURL.path) { seenPaths.insert(fileURL.path) files.append(fileURL) } } } for fileURL in Self.cachedCodexSessionFiles( cache: cache, range: range, roots: plan.roots, excludingPaths: seenPaths) .sorted(by: { $0.path < $1.path }) { seenPaths.insert(fileURL.path) files.append(fileURL) } let filePathsInScan = Set(files.map(\.path)) var scanState = CodexScanState() let fileIndex = CodexSessionFileIndex( files: files, roots: plan.roots, cachedSessionFiles: Self.cachedCodexSessionIndex( cache: cache, roots: plan.roots, knownExistingPaths: filePathsInScan), checkCancellation: checkCancellation) let inheritedResolver = CodexInheritedTotalsResolver( fileIndex: fileIndex, checkCancellation: checkCancellation) let resources = CodexScanResources( fileIndex: fileIndex, inheritedResolver: inheritedResolver, projectPathResolver: CodexCanonicalProjectPathResolver(), modelsDevCatalog: plan.modelsDevCatalog, modelsDevCacheRoot: options.cacheRoot, priorityTurns: plan.priorityTurns) let scanContext = Self.codexFileScanContext( range: range, options: options, plan: plan, resources: resources, checkCancellation: checkCancellation) for fileURL in files { try Self.scanCodexFile( fileURL: fileURL, context: scanContext, cache: &cache, state: &scanState) } try checkCancellation?() Self.pruneForceRescanFilesOutsideWindow( cache: &cache, range: range, isForceRescan: options.forceRescan) let shouldDropAllUnscannedFiles = options.forceRescan || plan.rootsChanged || cache.files.isEmpty || plan.needsProjectMetadataMigration for key in cache.files.keys where !filePathsInScan.contains(key) { guard let old = cache.files[key] else { continue } let shouldDrop = shouldDropAllUnscannedFiles || old.touchesCodexScanWindow(sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey) guard shouldDrop else { continue } Self.applyFileDays(cache: &cache, fileDays: old.days, sign: -1) cache.files.removeValue(forKey: key) } if !shouldDropAllUnscannedFiles { for key in cache.files.keys { guard let old = cache.files[key] else { continue } guard old.touchesCodexScanWindow(sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey) else { continue } guard FileManager.default.fileExists(atPath: key) else { Self.applyFileDays(cache: &cache, fileDays: old.days, sign: -1) cache.files.removeValue(forKey: key) continue } } } let shouldRetainWiderWindow = !options.forceRescan && !plan.pricingChanged && !plan .priorityMetadataChanged && !plan.needsTurnIDCacheMigration && !plan.needsProjectMetadataMigration let retainedSinceKey = shouldRetainWiderWindow ? [cachedSinceKey, range.scanSinceKey].compactMap(\.self).min() ?? range.scanSinceKey : range.scanSinceKey let retainedUntilKey = shouldRetainWiderWindow ? [cachedUntilKey, range.scanUntilKey].compactMap(\.self).max() ?? range.scanUntilKey : range.scanUntilKey Self.pruneDays(cache: &cache, sinceKey: retainedSinceKey, untilKey: retainedUntilKey) cache.roots = plan.rootsFingerprint cache.scanSinceKey = retainedSinceKey cache.scanUntilKey = retainedUntilKey cache.codexPricingKey = plan.codexPricingKey cache.codexPriorityMetadataKey = plan.codexPriorityMetadataKey cache.codexProjectMetadataVersion = Self.codexProjectMetadataVersion if plan.hasPriorityMetadata { cache.codexPriorityTurnKeys = Self.mergePriorityTurnKeys( existing: shouldRetainWiderWindow ? cache.codexPriorityTurnKeys : nil, new: plan.priorityTurnKeys, range: range, retainedSinceKey: retainedSinceKey, retainedUntilKey: retainedUntilKey) cache.codexPriorityTurnIDsByDay = Self.mergePriorityTurnIDsByDay( existing: shouldRetainWiderWindow ? cache.codexPriorityTurnIDsByDay : nil, new: plan.priorityTurnIDsByDay, range: range, retainedSinceKey: retainedSinceKey, retainedUntilKey: retainedUntilKey) } cache.lastScanUnixMs = nowMs try checkCancellation?() CostUsageCacheIO.save(provider: .codex, cache: cache, cacheRoot: options.cacheRoot) } return Self.buildCodexReportFromCache( cache: cache, range: range, modelsDevCatalog: plan.modelsDevCatalog, modelsDevCacheRoot: options.cacheRoot, priorityTurns: plan.priorityTurns) } private static func codexFileScanContext( range: CostUsageDayRange, options: Options, plan: CodexRefreshPlan, resources: CodexScanResources, checkCancellation: CancellationCheck?) -> CodexFileScanContext { CodexFileScanContext( range: range, forceFullScan: options.forceRescan || plan.windowExpanded || plan.pricingChanged || plan.priorityMetadataChanged || plan.needsProjectMetadataMigration, dropDeferredCodexRows: options.forceRescan || plan.pricingChanged || plan.priorityMetadataChanged || plan.needsTurnIDCacheMigration, requiresTurnIDCache: plan.needsTurnIDCacheMigration, changedPriorityTurnIDs: plan.changedPriorityTurnIDs, resources: resources, checkCancellation: checkCancellation) } } // swiftlint:enable type_body_length