import CodexBarCore import Foundation enum HistoricalUsageWindowKind: String, Codable { case secondary } enum HistoricalUsageRecordSource: String, Codable { case live case backfill } struct HistoricalUsageRecord: Codable { let v: Int let provider: UsageProvider let windowKind: HistoricalUsageWindowKind let source: HistoricalUsageRecordSource let accountKey: String? let sampledAt: Date let usedPercent: Double let resetsAt: Date let windowMinutes: Int init( v: Int, provider: UsageProvider, windowKind: HistoricalUsageWindowKind, source: HistoricalUsageRecordSource, accountKey: String?, sampledAt: Date, usedPercent: Double, resetsAt: Date, windowMinutes: Int) { self.v = v self.provider = provider self.windowKind = windowKind self.source = source self.accountKey = accountKey self.sampledAt = sampledAt self.usedPercent = usedPercent self.resetsAt = resetsAt self.windowMinutes = windowMinutes } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.v = try container.decodeIfPresent(Int.self, forKey: .v) ?? 1 self.provider = try container.decode(UsageProvider.self, forKey: .provider) self.windowKind = try container.decode(HistoricalUsageWindowKind.self, forKey: .windowKind) self.source = try container.decodeIfPresent(HistoricalUsageRecordSource.self, forKey: .source) ?? .live self.accountKey = try container.decodeIfPresent(String.self, forKey: .accountKey) self.sampledAt = try container.decode(Date.self, forKey: .sampledAt) self.usedPercent = try container.decode(Double.self, forKey: .usedPercent) self.resetsAt = try container.decode(Date.self, forKey: .resetsAt) self.windowMinutes = try container.decode(Int.self, forKey: .windowMinutes) } } struct HistoricalWeekProfile { let resetsAt: Date let windowMinutes: Int let curve: [Double] } struct CodexHistoricalDataset { static let gridPointCount = 169 let weeks: [HistoricalWeekProfile] } actor HistoricalUsageHistoryStore { private static let schemaVersion = 1 private static let writeInterval: TimeInterval = 30 * 60 private static let writeDeltaThreshold: Double = 1 private static let retentionDays: TimeInterval = 56 * 24 * 60 * 60 private static let minimumWeekSamples = 6 private static let boundaryCoverageWindow: TimeInterval = 24 * 60 * 60 private static let backfillWindowCapWeeks = 8 private static let backfillCalibrationMinimumUsedPercent = 1.0 private static let backfillCalibrationMinimumCredits = 0.001 private static let backfillSampleFractions: [Double] = (0...14).map { Double($0) / 14.0 } private static let coverageTolerance: TimeInterval = 16 * 60 * 60 private static let resetBucketSeconds: TimeInterval = 5 * 60 private let fileURL: URL private var records: [HistoricalUsageRecord] = [] private var loaded = false init(fileURL: URL? = nil) { self.fileURL = fileURL ?? HistoricalUsageHistoryStore.defaultFileURL() } func loadCodexDataset(accountKey: String?) -> CodexHistoricalDataset? { self.ensureLoaded() return self.buildDataset(accountKey: accountKey) } func loadCodexDataset( canonicalAccountKey: String?, canonicalEmailHashKey: String?, legacyEmailHash: String?, hasAdjacentMultiAccountVeto: Bool) -> CodexHistoricalDataset? { self.ensureLoaded() return self.buildDataset( canonicalAccountKey: canonicalAccountKey, canonicalEmailHashKey: canonicalEmailHashKey, legacyEmailHash: legacyEmailHash, hasAdjacentMultiAccountVeto: hasAdjacentMultiAccountVeto) } func recordCodexWeekly( window: RateWindow, sampledAt: Date = .init(), accountKey: String?) -> CodexHistoricalDataset? { guard let rawResetsAt = window.resetsAt else { return self.loadCodexDataset(accountKey: accountKey) } guard let windowMinutes = window.windowMinutes, windowMinutes > 0 else { return self.loadCodexDataset(accountKey: accountKey) } self.ensureLoaded() let resetsAt = Self.normalizeReset(rawResetsAt) let sample = HistoricalUsageRecord( v: Self.schemaVersion, provider: .codex, windowKind: .secondary, source: .live, accountKey: accountKey, sampledAt: sampledAt, usedPercent: Self.clamp(window.usedPercent, lower: 0, upper: 100), resetsAt: resetsAt, windowMinutes: windowMinutes) if !self.shouldAccept(sample) { return self.buildDataset(accountKey: accountKey) } self.records.append(sample) self.pruneOldRecords(now: sampledAt) self.records.sort { lhs, rhs in if lhs.sampledAt == rhs.sampledAt { if lhs.resetsAt == rhs.resetsAt { return lhs.usedPercent < rhs.usedPercent } return lhs.resetsAt < rhs.resetsAt } return lhs.sampledAt < rhs.sampledAt } self.persist() return self.buildDataset(accountKey: accountKey) } func backfillCodexWeeklyFromUsageBreakdown( _ breakdown: [OpenAIDashboardDailyBreakdown], referenceWindow: RateWindow, now: Date = .init(), accountKey: String?) -> CodexHistoricalDataset? { self.ensureLoaded() let existingDataset = self.buildDataset(accountKey: accountKey) guard let rawResetsAt = referenceWindow.resetsAt else { return existingDataset } guard let windowMinutes = referenceWindow.windowMinutes, windowMinutes > 0 else { return existingDataset } let resetsAt = Self.normalizeReset(rawResetsAt) let duration = TimeInterval(windowMinutes) * 60 guard duration > 0 else { return existingDataset } let windowStart = resetsAt.addingTimeInterval(-duration) let calibrationEnd = Self.clampDate(now, lower: windowStart, upper: resetsAt) let dayUsages = Self.parseDayUsages( from: breakdown, asOf: calibrationEnd, fillingFrom: windowStart) guard !dayUsages.isEmpty else { return existingDataset } guard let coverageStart = dayUsages.first?.start, let coverageEnd = dayUsages.last?.end else { return existingDataset } guard coverageStart <= windowStart.addingTimeInterval(Self.coverageTolerance) else { return existingDataset } guard coverageEnd >= calibrationEnd.addingTimeInterval(-Self.coverageTolerance) else { return existingDataset } let currentUsedPercent = Self.clamp(referenceWindow.usedPercent, lower: 0, upper: 100) guard currentUsedPercent >= Self.backfillCalibrationMinimumUsedPercent else { return existingDataset } let currentCredits = Self.creditsUsed( from: dayUsages, between: windowStart, and: calibrationEnd) guard currentCredits > Self.backfillCalibrationMinimumCredits else { return existingDataset } let estimatedCreditsAtLimit = currentCredits / (currentUsedPercent / 100) guard estimatedCreditsAtLimit.isFinite, estimatedCreditsAtLimit > Self.backfillCalibrationMinimumCredits else { return existingDataset } struct RecordKey: Hashable { let resetsAt: Date let sampledAt: Date let windowMinutes: Int let accountKey: String? } var synthesized: [HistoricalUsageRecord] = [] synthesized.reserveCapacity(Self.backfillWindowCapWeeks * Self.backfillSampleFractions.count) for weeksBack in 1...Self.backfillWindowCapWeeks { let reset = Self.normalizeReset(resetsAt.addingTimeInterval(-duration * Double(weeksBack))) let start = reset.addingTimeInterval(-duration) guard start >= coverageStart.addingTimeInterval(-Self.coverageTolerance), reset <= coverageEnd.addingTimeInterval(Self.coverageTolerance) else { continue } let existingForWeek = self.records.filter { $0.provider == .codex && $0.windowKind == .secondary && $0.windowMinutes == windowMinutes && $0.accountKey == accountKey && $0.resetsAt == reset } if Self.isCompleteWeek(samples: existingForWeek, windowStart: start, resetsAt: reset) { continue } var existingRecordKeys = Set(existingForWeek.map { RecordKey( resetsAt: $0.resetsAt, sampledAt: $0.sampledAt, windowMinutes: $0.windowMinutes, accountKey: $0.accountKey) }) let weekCredits = Self.creditsUsed(from: dayUsages, between: start, and: reset) guard weekCredits > Self.backfillCalibrationMinimumCredits else { continue } for fraction in Self.backfillSampleFractions { let sampledAt = start.addingTimeInterval(duration * fraction) let recordKey = RecordKey( resetsAt: reset, sampledAt: sampledAt, windowMinutes: windowMinutes, accountKey: accountKey) guard !existingRecordKeys.contains(recordKey) else { continue } let cumulativeCredits = Self.creditsUsed(from: dayUsages, between: start, and: sampledAt) let usedPercent = Self.clamp((cumulativeCredits / estimatedCreditsAtLimit) * 100, lower: 0, upper: 100) synthesized.append(HistoricalUsageRecord( v: Self.schemaVersion, provider: .codex, windowKind: .secondary, source: .backfill, accountKey: accountKey, sampledAt: sampledAt, usedPercent: usedPercent, resetsAt: reset, windowMinutes: windowMinutes)) existingRecordKeys.insert(recordKey) } } guard !synthesized.isEmpty else { return existingDataset } self.records.append(contentsOf: synthesized) self.pruneOldRecords(now: now) self.records.sort { lhs, rhs in if lhs.sampledAt == rhs.sampledAt { if lhs.resetsAt == rhs.resetsAt { return lhs.usedPercent < rhs.usedPercent } return lhs.resetsAt < rhs.resetsAt } return lhs.sampledAt < rhs.sampledAt } self.persist() return self.buildDataset(accountKey: accountKey) } private func shouldAccept(_ sample: HistoricalUsageRecord) -> Bool { guard let prior = self.records .last(where: { $0.provider == sample.provider && $0.windowKind == sample.windowKind && $0.accountKey == sample.accountKey && $0.windowMinutes == sample.windowMinutes }) else { return true } if prior.resetsAt != sample.resetsAt { return true } if sample.sampledAt.timeIntervalSince(prior.sampledAt) >= Self.writeInterval { return true } if abs(sample.usedPercent - prior.usedPercent) >= Self.writeDeltaThreshold { return true } return false } private func pruneOldRecords(now: Date) { let cutoff = now.addingTimeInterval(-Self.retentionDays) self.records.removeAll { $0.sampledAt < cutoff } } private func ensureLoaded() { guard !self.loaded else { return } self.loaded = true self.records = self.readRecordsFromDisk() self.pruneOldRecords(now: .init()) } private func readRecordsFromDisk() -> [HistoricalUsageRecord] { guard let data = try? Data(contentsOf: self.fileURL), !data.isEmpty else { return [] } guard let text = String(data: data, encoding: .utf8) else { return [] } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 var decoded: [HistoricalUsageRecord] = [] decoded.reserveCapacity(text.count / 80) for rawLine in text.split(separator: "\n", omittingEmptySubsequences: true) { let line = String(rawLine).trimmingCharacters(in: .whitespacesAndNewlines) guard !line.isEmpty, let lineData = line.data(using: .utf8) else { continue } guard var record = try? decoder.decode(HistoricalUsageRecord.self, from: lineData) else { continue } record = HistoricalUsageRecord( v: record.v, provider: record.provider, windowKind: record.windowKind, source: record.source, accountKey: record.accountKey?.isEmpty == false ? record.accountKey : nil, sampledAt: record.sampledAt, usedPercent: Self.clamp(record.usedPercent, lower: 0, upper: 100), resetsAt: Self.normalizeReset(record.resetsAt), windowMinutes: record.windowMinutes) decoded.append(record) } return decoded } private func persist() { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = [.sortedKeys] var lines: [String] = [] lines.reserveCapacity(self.records.count) for record in self.records { guard let data = try? encoder.encode(record), let line = String(data: data, encoding: .utf8) else { continue } lines.append(line) } let payload = (lines.joined(separator: "\n") + "\n").data(using: .utf8) ?? Data() let directory = self.fileURL.deletingLastPathComponent() do { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) try payload.write(to: self.fileURL, options: [.atomic]) } catch { // Best-effort cache file; ignore write failures. } } private func buildDataset(accountKey: String?) -> CodexHistoricalDataset? { let scoped = self.records.filter { record in guard Self.isCodexSecondaryRecord(record) else { return false } if let accountKey { return record.accountKey == accountKey } return record.accountKey == nil } return self.buildDataset(from: scoped) } private func buildDataset( canonicalAccountKey: String?, canonicalEmailHashKey: String?, legacyEmailHash: String?, hasAdjacentMultiAccountVeto: Bool) -> CodexHistoricalDataset? { guard let canonicalAccountKey else { return self.buildDataset(accountKey: nil) } let shouldIncludeUnscoped = CodexHistoryOwnership.hasStrictSingleAccountContinuity( scopedRawKeys: Self.scopedRawKeysRelevantToCodexUnscopedHistory(self.records), targetCanonicalKey: canonicalAccountKey, canonicalEmailHashKey: canonicalEmailHashKey, legacyEmailHash: legacyEmailHash, hasAdjacentMultiAccountVeto: hasAdjacentMultiAccountVeto) let scoped = self.records.filter { record in guard Self.isCodexSecondaryRecord(record) else { return false } guard let rawKey = record.accountKey else { return shouldIncludeUnscoped } let owner = CodexHistoryOwnership.classifyPersistedKey(rawKey, legacyEmailHash: legacyEmailHash) return CodexHistoryOwnership.belongsToTargetContinuity( owner, targetCanonicalKey: canonicalAccountKey, canonicalEmailHashKey: canonicalEmailHashKey) } return self.buildDataset(from: scoped) } private func buildDataset(from scoped: [HistoricalUsageRecord]) -> CodexHistoricalDataset? { struct WeekKey: Hashable { let resetsAt: Date let windowMinutes: Int } if scoped.isEmpty { return nil } let grouped = Dictionary(grouping: scoped) { WeekKey(resetsAt: $0.resetsAt, windowMinutes: $0.windowMinutes) } var weeks: [HistoricalWeekProfile] = [] weeks.reserveCapacity(grouped.count) for (key, samples) in grouped { let duration = TimeInterval(key.windowMinutes) * 60 guard duration > 0 else { continue } let windowStart = key.resetsAt.addingTimeInterval(-duration) guard Self.isCompleteWeek(samples: samples, windowStart: windowStart, resetsAt: key.resetsAt) else { continue } guard let curve = Self.reconstructWeekCurve( samples: samples, windowStart: windowStart, windowDuration: duration, gridPointCount: CodexHistoricalDataset.gridPointCount) else { continue } weeks.append(HistoricalWeekProfile( resetsAt: key.resetsAt, windowMinutes: key.windowMinutes, curve: curve)) } weeks.sort { $0.resetsAt < $1.resetsAt } if weeks.isEmpty { return nil } return CodexHistoricalDataset(weeks: weeks) } private nonisolated static func isCodexSecondaryRecord(_ record: HistoricalUsageRecord) -> Bool { record.provider == .codex && record.windowKind == .secondary && record.windowMinutes > 0 } private static func reconstructWeekCurve( samples: [HistoricalUsageRecord], windowStart: Date, windowDuration: TimeInterval, gridPointCount: Int) -> [Double]? { guard gridPointCount >= 2 else { return nil } var points = samples.map { sample -> (u: Double, value: Double) in let offset = sample.sampledAt.timeIntervalSince(windowStart) let u = Self.clamp(offset / windowDuration, lower: 0, upper: 1) return (u: u, value: Self.clamp(sample.usedPercent, lower: 0, upper: 100)) } points.sort { lhs, rhs in if lhs.u == rhs.u { return lhs.value < rhs.value } return lhs.u < rhs.u } guard !points.isEmpty else { return nil } // Enforce monotonicity on observed samples before interpolation. var monotonePoints: [(u: Double, value: Double)] = [] monotonePoints.reserveCapacity(points.count) var runningMax = 0.0 for point in points { runningMax = max(runningMax, point.value) monotonePoints.append((u: point.u, value: runningMax)) } // Anchor reconstructed curves to reset start and end-of-week plateau. let endValue = monotonePoints.last?.value ?? 0 monotonePoints.append((u: 0, value: 0)) monotonePoints.append((u: 1, value: endValue)) monotonePoints.sort { lhs, rhs in if lhs.u == rhs.u { return lhs.value < rhs.value } return lhs.u < rhs.u } runningMax = 0 for index in monotonePoints.indices { runningMax = max(runningMax, monotonePoints[index].value) monotonePoints[index].value = runningMax } var curve = Array(repeating: 0.0, count: gridPointCount) let first = monotonePoints[0] let last = monotonePoints[monotonePoints.count - 1] var upperIndex = 1 let denominator = Double(gridPointCount - 1) for index in 0..= last.u { curve[index] = last.value continue } while upperIndex < monotonePoints.count, monotonePoints[upperIndex].u < u { upperIndex += 1 } let hi = monotonePoints[min(upperIndex, monotonePoints.count - 1)] let lo = monotonePoints[max(0, upperIndex - 1)] if hi.u <= lo.u { curve[index] = max(lo.value, hi.value) continue } let ratio = Self.clamp((u - lo.u) / (hi.u - lo.u), lower: 0, upper: 1) curve[index] = lo.value + (hi.value - lo.value) * ratio } // Re-enforce monotonicity on reconstructed grid. var curveMax = 0.0 for index in curve.indices { curve[index] = Self.clamp(curve[index], lower: 0, upper: 100) curveMax = max(curveMax, curve[index]) curve[index] = curveMax } return curve } private static func isCompleteWeek(samples: [HistoricalUsageRecord], windowStart: Date, resetsAt: Date) -> Bool { guard samples.count >= self.minimumWeekSamples else { return false } let startBoundary = windowStart.addingTimeInterval(Self.boundaryCoverageWindow) let endBoundary = resetsAt.addingTimeInterval(-Self.boundaryCoverageWindow) let hasStartCoverage = samples.contains { sample in sample.sampledAt >= windowStart && sample.sampledAt <= startBoundary } let hasEndCoverage = samples.contains { sample in sample.sampledAt >= endBoundary && sample.sampledAt <= resetsAt } return hasStartCoverage && hasEndCoverage } private nonisolated static func scopedRawKeysRelevantToCodexUnscopedHistory( _ records: [HistoricalUsageRecord]) -> [String] { let unscopedRecords = records.filter { record in Self.isCodexSecondaryRecord(record) && record.accountKey == nil } guard let continuityWindow = self.historicalContinuityWindow(for: unscopedRecords) else { return [] } return records.compactMap { record in guard Self.isCodexSecondaryRecord(record), let accountKey = record.accountKey, continuityWindow.contains(record.sampledAt) else { return nil } return accountKey } } private nonisolated static func historicalContinuityWindow( for records: [HistoricalUsageRecord]) -> ClosedRange? { let sampledDates = records.map(\.sampledAt) guard let lowerBound = sampledDates.min(), let upperBound = sampledDates.max() else { return nil } let expansion = TimeInterval(records.map(\.windowMinutes).max() ?? 0) * 60 return lowerBound.addingTimeInterval(-expansion)...upperBound.addingTimeInterval(expansion) } private struct DayUsage { let start: Date let end: Date let creditsUsed: Double } private static func parseDayUsages( from breakdown: [OpenAIDashboardDailyBreakdown], asOf: Date, fillingFrom expectedCoverageStart: Date? = nil) -> [DayUsage] { var creditsByStart: [Date: Double] = [:] creditsByStart.reserveCapacity(breakdown.count) for day in breakdown { guard let dayStart = Self.dayStart(for: day.day) else { continue } creditsByStart[dayStart, default: 0] += max(0, day.totalCreditsUsed) } let calendar = Self.gregorianCalendar() var dayUsages: [DayUsage] = [] dayUsages.reserveCapacity(creditsByStart.count) for (dayStart, credits) in creditsByStart { guard let nominalEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { continue } let effectiveEnd: Date = if dayStart <= asOf, asOf < nominalEnd { asOf } else { nominalEnd } guard effectiveEnd > dayStart else { continue } dayUsages.append(DayUsage(start: dayStart, end: effectiveEnd, creditsUsed: credits)) } dayUsages.sort { lhs, rhs in lhs.start < rhs.start } return Self.fillMissingZeroUsageDays( in: dayUsages, through: asOf, fillingFrom: expectedCoverageStart) } private static func fillMissingZeroUsageDays( in dayUsages: [DayUsage], through asOf: Date, fillingFrom expectedCoverageStart: Date? = nil) -> [DayUsage] { guard let firstStart = dayUsages.first?.start else { return [] } let calendar = Self.gregorianCalendar() let fillStart: Date = if let expectedCoverageStart { min(firstStart, calendar.startOfDay(for: expectedCoverageStart)) } else { firstStart } let finalDayStart = calendar.startOfDay(for: asOf) guard fillStart <= finalDayStart else { return dayUsages } let creditsByStart = Dictionary(uniqueKeysWithValues: dayUsages.map { ($0.start, $0.creditsUsed) }) let daySpan = max(0, calendar.dateComponents([.day], from: fillStart, to: finalDayStart).day ?? 0) var filled: [DayUsage] = [] filled.reserveCapacity(daySpan + 1) var cursor = fillStart while cursor <= finalDayStart { guard let nominalEnd = calendar.date(byAdding: .day, value: 1, to: cursor) else { break } let effectiveEnd: Date = if cursor <= asOf, asOf < nominalEnd { asOf } else { nominalEnd } guard effectiveEnd > cursor else { break } filled.append(DayUsage( start: cursor, end: effectiveEnd, creditsUsed: creditsByStart[cursor] ?? 0)) guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break } cursor = next } return filled } private static func dayStart(for key: String) -> Date? { let components = key.split(separator: "-", omittingEmptySubsequences: true) guard components.count == 3, let year = Int(components[0]), let month = Int(components[1]), let day = Int(components[2]) else { return nil } let calendar = Self.gregorianCalendar() var dateComponents = DateComponents() dateComponents.calendar = calendar dateComponents.timeZone = calendar.timeZone dateComponents.year = year dateComponents.month = month dateComponents.day = day dateComponents.hour = 0 dateComponents.minute = 0 dateComponents.second = 0 return dateComponents.date } private static func creditsUsed(from dayUsages: [DayUsage], between start: Date, and end: Date) -> Double { guard end > start else { return 0 } var total = 0.0 for day in dayUsages { if day.end <= start { continue } if day.start >= end { break } let overlapStart = max(day.start, start) let overlapEnd = min(day.end, end) guard overlapEnd > overlapStart else { continue } let dayDuration = day.end.timeIntervalSince(day.start) guard dayDuration > 0 else { continue } let overlap = overlapEnd.timeIntervalSince(overlapStart) total += day.creditsUsed * (overlap / dayDuration) } return max(0, total) } nonisolated static func defaultFileURL() -> URL { let root = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? FileManager.default.homeDirectoryForCurrentUser return root .appendingPathComponent("CodexBar", isDirectory: true) .appendingPathComponent("usage-history.jsonl", isDirectory: false) } private nonisolated static func clamp(_ value: Double, lower: Double, upper: Double) -> Double { min(upper, max(lower, value)) } private nonisolated static func clampDate(_ value: Date, lower: Date, upper: Date) -> Date { min(upper, max(lower, value)) } private nonisolated static func normalizeReset(_ value: Date) -> Date { let bucket = Self.resetBucketSeconds guard bucket > 0 else { return value } let rounded = (value.timeIntervalSinceReferenceDate / bucket).rounded() * bucket return Date(timeIntervalSinceReferenceDate: rounded) } private nonisolated static func gregorianCalendar() -> Calendar { var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone.current return calendar } #if DEBUG nonisolated static func _dayStartForTesting(_ key: String) -> Date? { self.dayStart(for: key) } nonisolated static func _creditsUsedForTesting( breakdown: [OpenAIDashboardDailyBreakdown], asOf: Date, start: Date, end: Date) -> Double { let dayUsages = Self.parseDayUsages(from: breakdown, asOf: asOf) return Self.creditsUsed(from: dayUsages, between: start, and: end) } #endif } enum CodexHistoricalPaceEvaluator { static let minimumCompleteWeeksForHistorical = 3 static let minimumWeeksForRisk = 5 private static let recencyTauWeeks: Double = 3 private static let epsilon: Double = 1e-9 private static let resetBucketSeconds: TimeInterval = 5 * 60 static func evaluate(window: RateWindow, now: Date, dataset: CodexHistoricalDataset?) -> UsagePace? { guard let dataset else { return nil } guard let resetsAt = window.resetsAt else { return nil } let minutes = window.windowMinutes ?? 10080 guard minutes > 0 else { return nil } let duration = TimeInterval(minutes) * 60 guard duration > 0 else { return nil } let timeUntilReset = resetsAt.timeIntervalSince(now) guard timeUntilReset > 0, timeUntilReset <= duration else { return nil } let normalizedResetsAt = Self.normalizeReset(resetsAt) let elapsed = Self.clamp(duration - timeUntilReset, lower: 0, upper: duration) let actual = Self.clamp(window.usedPercent, lower: 0, upper: 100) if elapsed == 0, actual > 0 { return nil } let uNow = Self.clamp(elapsed / duration, lower: 0, upper: 1) let scopedWeeks = dataset.weeks.filter { week in week.windowMinutes == minutes && week.resetsAt < normalizedResetsAt } guard scopedWeeks.count >= Self.minimumCompleteWeeksForHistorical else { return nil } let weightedWeeks = scopedWeeks.map { week in let ageWeeks = Self.clamp( normalizedResetsAt.timeIntervalSince(week.resetsAt) / duration, lower: 0, upper: Double.greatestFiniteMagnitude) let weight = exp(-ageWeeks / Self.recencyTauWeeks) return (week: week, weight: weight) } let totalWeight = weightedWeeks.reduce(0.0) { $0 + $1.weight } guard totalWeight > Self.epsilon else { return nil } let totalWeightSquared = weightedWeeks.reduce(0.0) { $0 + ($1.weight * $1.weight) } let nEff = totalWeightSquared > Self.epsilon ? (totalWeight * totalWeight) / totalWeightSquared : 0 let lambda = Self.clamp((nEff - 2) / 6, lower: 0, upper: 1) let gridCount = CodexHistoricalDataset.gridPointCount let denominator = Double(gridCount - 1) var expectedCurve = Array(repeating: 0.0, count: gridCount) for index in 0..= 100 - Self.epsilon }), capIndex > 0, capIndex < extendedCurve.count - 1 { let gridCount = CodexHistoricalDataset.gridPointCount let uCap = Double(capIndex) / Double(gridCount - 1) let valCap = extendedCurve[capIndex] let slope: Double = valCap / uCap for i in capIndex..= 100 - Self.epsilon if runOut { weightedRunOutMass += weight if let crossingU = Self.firstCrossing( after: uNow, curve: extendedCurve, shift: shift, actualAtNow: actual) { let etaSeconds = max(0, (crossingU - uNow) * duration) crossingCandidates.append((etaSeconds: etaSeconds, weight: weight)) } } } let smoothedProbability = Self.clamp( (weightedRunOutMass + 0.5) / (totalWeight + 1), lower: 0, upper: 1) var runOutProbability: Double? = scopedWeeks.count >= Self.minimumWeeksForRisk ? smoothedProbability : nil var willLastToReset = smoothedProbability < 0.5 var etaSeconds: TimeInterval? if actual >= 100 { willLastToReset = false etaSeconds = 0 runOutProbability = 1 } else if !willLastToReset { let values = crossingCandidates.map(\.etaSeconds) let weights = crossingCandidates.map(\.weight) if values.isEmpty { willLastToReset = true } else { etaSeconds = max(0, Self.weightedMedian(values: values, weights: weights)) } } return UsagePace.historical( expectedUsedPercent: expectedNow, actualUsedPercent: actual, etaSeconds: etaSeconds, willLastToReset: willLastToReset, runOutProbability: runOutProbability, projectedRemainingUsage: max(0, (expectedCurve.last ?? expectedNow) - expectedNow)) } private static func firstCrossing( after uNow: Double, curve: [Double], shift: Double, actualAtNow: Double) -> Double? { let gridCount = curve.count guard gridCount >= 2 else { return nil } let denominator = Double(gridCount - 1) var previousU = uNow var previousValue = actualAtNow let startIndex = min(gridCount - 1, max(1, Int(floor(uNow * denominator)) + 1)) for index in startIndex..= 100 - Self.epsilon { let delta = value - previousValue if abs(delta) <= Self.epsilon { return u } let ratio = Self.clamp((100 - previousValue) / delta, lower: 0, upper: 1) return Self.clamp(previousU + ratio * (u - previousU), lower: uNow, upper: 1) } previousU = u previousValue = value } return nil } private static func interpolate(curve: [Double], at u: Double) -> Double { guard !curve.isEmpty else { return 0 } if curve.count == 1 { return curve[0] } let clipped = Self.clamp(u, lower: 0, upper: 1) let scaled = clipped * Double(curve.count - 1) let lower = Int(floor(scaled)) let upper = min(curve.count - 1, lower + 1) if lower == upper { return curve[lower] } let ratio = scaled - Double(lower) return curve[lower] + ((curve[upper] - curve[lower]) * ratio) } private static func weightedMedian(values: [Double], weights: [Double]) -> Double { guard values.count == weights.count, !values.isEmpty else { return 0 } let pairs = zip(values, weights) .map { (value: $0, weight: max(0, $1)) } .sorted { lhs, rhs in lhs.value < rhs.value } let totalWeight = pairs.reduce(0.0) { $0 + $1.weight } if totalWeight <= Self.epsilon { let sortedValues = values.sorted() return sortedValues[sortedValues.count / 2] } let threshold = totalWeight / 2 var cumulative = 0.0 for pair in pairs { cumulative += pair.weight if cumulative >= threshold { return pair.value } } return pairs.last?.value ?? 0 } private static func clamp(_ value: Double, lower: Double, upper: Double) -> Double { min(upper, max(lower, value)) } private static func normalizeReset(_ value: Date) -> Date { let bucket = Self.resetBucketSeconds guard bucket > 0 else { return value } let rounded = (value.timeIntervalSinceReferenceDate / bucket).rounded() * bucket return Date(timeIntervalSinceReferenceDate: rounded) } }