import CodexBarCore import Foundation extension UsageStore { private nonisolated static let limitResetThreshold = 1.0 nonisolated static let sessionLimitResetDetectorDefaultsKey = "sessionLimitResetDetectorStates" private nonisolated static let weeklyLimitResetDetectorDefaultsKey = "weeklyLimitResetDetectorStates" private nonisolated static let claudeOAuthAccountUuidMapDefaultsKey = "ClaudeOAuthHistoryOwnerAccountUuidMapV1" private nonisolated static let claudeOAuthAccountCandidateMapDefaultsKey = "ClaudeOAuthHistoryOwnerAccountCandidateMapV1" private nonisolated static let weeklyWindowMinutes = 7 * 24 * 60 private nonisolated static let planUtilizationUnscopedPreferredKey = "__unscoped__" private nonisolated static let claudeOAuthPlanUtilizationAccountKeyPrefix = "__claude_oauth__:" enum ClaudeOAuthActiveAccountObservation: Equatable, Sendable { case stable(identity: String?) case changed } struct ClaudeOAuthAccountBindingCandidate: Codable, Equatable { let identity: String let observedAt: Date } private struct ClaudeOAuthHistoryEvidence { let owner: String let persistentRefHash: String? let keychainCredentialMismatch: Bool let keychainCredentialAbsent: Bool let keychainCredentialUnavailable: Bool let activeAccountObservation: ClaudeOAuthActiveAccountObservation let observedAt: Date } struct LimitResetDetectorState: Codable, Equatable { let wasAboveThreshold: Bool let lastObservedAt: Date let sourceRawValue: String? var resetBoundary: Date? } func supportsPlanUtilizationHistory(for provider: UsageProvider) -> Bool { switch provider { case .codex, .claude: true default: if self.planUtilizationHistory[provider]?.isEmpty == false { true } else if self.settings.historicalTrackingEnabled, let snapshot = self.snapshots[provider] { !self.planUtilizationSeriesSamples( provider: provider, snapshot: snapshot, capturedAt: snapshot.updatedAt).isEmpty } else { false } } } private nonisolated static let planUtilizationMinSampleIntervalSeconds: TimeInterval = 60 * 60 private nonisolated static let planUtilizationResetEquivalenceToleranceSeconds: TimeInterval = 2 * 60 private nonisolated static let planUtilizationMaxSamples: Int = 24 * 730 private struct PlanUtilizationSeriesKey: Hashable { let name: PlanUtilizationSeriesName let windowMinutes: Int } private struct PlanUtilizationSeriesSample { let name: PlanUtilizationSeriesName let windowMinutes: Int let entry: PlanUtilizationHistoryEntry } private struct LimitResetDetectionContext { let provider: UsageProvider let account: ProviderTokenAccount? let snapshot: UsageSnapshot let accountKey: String? let capturedAt: Date let codexLimitResetOwnerKey: CodexLimitResetOwnerKey? } private struct LimitResetObservation { let usedPercent: Double let observedAt: Date let resetBoundary: Date? let source: SessionQuotaWindowSource? } private struct LimitResetDetectionDescriptor { let seriesName: PlanUtilizationSeriesName let defaultsKey: String let resetKind: String } func planUtilizationHistory(for provider: UsageProvider) -> [PlanUtilizationSeriesHistory] { self.planUtilizationHistorySelection(for: provider).histories } func planUtilizationHistorySelection(for provider: UsageProvider) -> (accountKey: String?, histories: [PlanUtilizationSeriesHistory]) { // The persisted history has not been read yet. Return the in-memory // stub (empty) without performing account migration or enqueueing an // empty persistence snapshot — otherwise a startup refresh racing the // background load would record samples against an empty bucket and // overwrite real disk history. if !self.planUtilizationHistoryLoaded { let providerBuckets = self.planUtilizationHistory[provider] ?? PlanUtilizationHistoryBuckets() return (nil, providerBuckets.histories(for: nil)) } var providerBuckets = self.planUtilizationHistory[provider] ?? PlanUtilizationHistoryBuckets() if provider == .claude, providerBuckets.preferredAccountKey == Self.planUtilizationUnscopedPreferredKey || Self.isClaudeOAuthPlanUtilizationAccountKey(providerBuckets.preferredAccountKey) { // Persisted OAuth provenance outranks an unrelated configured token account. The unscoped // sentinel intentionally resolves to nil, including after the history store is reloaded. let accountKey = self.stickyPlanUtilizationAccountKey(providerBuckets: providerBuckets) return (accountKey, providerBuckets.histories(for: accountKey)) } let originalProviderBuckets = providerBuckets let accountKey = self.resolvePlanUtilizationAccountKey( provider: provider, snapshot: self.snapshots[provider], preferredAccount: nil, providerBuckets: &providerBuckets) self.planUtilizationHistory[provider] = providerBuckets if providerBuckets != originalProviderBuckets { let snapshotToPersist = self.planUtilizationHistory Task { await self.planUtilizationPersistenceCoordinator.enqueue(snapshotToPersist) } } return (accountKey, providerBuckets.histories(for: accountKey)) } func codexPlanUtilizationHistories(forVisibleAccount account: CodexVisibleAccount) -> [PlanUtilizationSeriesHistory] { // Same gate as `planUtilizationHistorySelection`: defer ownership // migration until the persisted history has been read. if !self.planUtilizationHistoryLoaded { let providerBuckets = self.planUtilizationHistory[.codex] ?? PlanUtilizationHistoryBuckets() return providerBuckets.histories(for: nil) } var providerBuckets = self.planUtilizationHistory[.codex] ?? PlanUtilizationHistoryBuckets() let originalProviderBuckets = providerBuckets let ownership = self.codexOwnershipContext(forVisibleAccount: account) guard let canonicalKey = ownership.canonicalKey else { return [] } if ownership.hasAdjacentEmailScopeAmbiguity { guard canonicalKey != ownership.canonicalEmailHashKey else { return [] } return providerBuckets.histories(for: canonicalKey) } let accountKey = self.materializeCodexPlanUtilizationHistoryIfNeeded( into: canonicalKey, ownership: ownership, shouldAdoptUnscopedHistory: true, providerBuckets: &providerBuckets) self.planUtilizationHistory[.codex] = providerBuckets if providerBuckets != originalProviderBuckets { let snapshotToPersist = self.planUtilizationHistory Task { await self.planUtilizationPersistenceCoordinator.enqueue(snapshotToPersist) } } return providerBuckets.histories(for: accountKey) } func shouldShowRefreshingMenuCard(for provider: UsageProvider) -> Bool { self.refreshingProviders.contains(provider) && self.snapshots[provider] == nil && self.error(for: provider) == nil } func shouldShowRefreshingMenuCardIndicator(for provider: UsageProvider) -> Bool { self.refreshingProviders.contains(provider) && self.error(for: provider) == nil } func shouldHidePlanUtilizationMenuItem(for provider: UsageProvider) -> Bool { guard self.supportsPlanUtilizationHistory(for: provider) else { return true } return self.shouldShowRefreshingMenuCard(for: provider) } func recordPlanUtilizationHistorySample( provider: UsageProvider, snapshot: UsageSnapshot, account: ProviderTokenAccount? = nil, claudeOAuthPersistentRefHash: String? = nil, claudeOAuthHistoryOwnerIdentifier: String? = nil, claudeOAuthKeychainCredentialMismatch: Bool = false, claudeOAuthKeychainCredentialAbsent: Bool = false, claudeOAuthKeychainCredentialUnavailable: Bool = false, claudeOAuthActiveAccountObservation: ClaudeOAuthActiveAccountObservation = .stable(identity: nil), isClaudeOAuthSample: Bool = false, shouldUpdatePreferredAccountKey: Bool = true, shouldAdoptUnscopedHistory: Bool = true, codexLimitResetOwnerKey: CodexLimitResetOwnerKey? = nil, now: Date = Date()) async { let samples = self.planUtilizationSeriesSamples(provider: provider, snapshot: snapshot, capturedAt: now) var effectiveOwner = claudeOAuthHistoryOwnerIdentifier if provider == .claude, isClaudeOAuthSample, let owner = claudeOAuthHistoryOwnerIdentifier { effectiveOwner = self.resolvedClaudeOAuthHistoryOwner(evidence: ClaudeOAuthHistoryEvidence( owner: owner, persistentRefHash: claudeOAuthPersistentRefHash, keychainCredentialMismatch: claudeOAuthKeychainCredentialMismatch, keychainCredentialAbsent: claudeOAuthKeychainCredentialAbsent, keychainCredentialUnavailable: claudeOAuthKeychainCredentialUnavailable, activeAccountObservation: claudeOAuthActiveAccountObservation, observedAt: now)) } let detectorAccountKey = if provider == .claude, isClaudeOAuthSample { Self.claudeOAuthPlanUtilizationAccountKey( historyOwnerIdentifier: effectiveOwner, corroboratingPersistentRefHash: claudeOAuthPersistentRefHash) } else { self.planUtilizationAccountKey( for: provider, snapshot: snapshot, preferredAccount: account) } if provider == .claude, isClaudeOAuthSample, detectorAccountKey == nil { // Persisting without a high-entropy owner would merge unrelated OAuth accounts into `unscoped`. return } let detectorContext = LimitResetDetectionContext( provider: provider, account: account, snapshot: snapshot, accountKey: detectorAccountKey, capturedAt: now, codexLimitResetOwnerKey: codexLimitResetOwnerKey) await MainActor.run { self.postLimitResetCelebrationsIfNeeded( context: detectorContext, samples: samples) } guard !samples.isEmpty else { return } guard self.shouldRecordPlanUtilizationHistory(for: provider) else { return } guard !self.shouldDeferClaudePlanUtilizationHistory(provider: provider) else { return } // Wait for the persisted history to finish loading before mutating // `self.planUtilizationHistory`. A startup refresh racing the // background decode would otherwise record samples against an empty // bucket and overwrite real disk history on the next persistence // enqueue. if !self.planUtilizationHistoryLoaded { // `_cancelPlanUtilizationHistoryLoadForTesting` cancels the task // and flips `loaded` to true; this branch only runs when the load // is still pending. Cancellation here (deinit during a startup // refresh) means the in-memory dictionary is empty — proceeding // is the safer choice than discarding the sample. _ = await self.planUtilizationHistoryLoadTask?.result } var snapshotToPersist: [UsageProvider: PlanUtilizationHistoryBuckets]? await MainActor.run { var providerBuckets = self.planUtilizationHistory[provider] ?? PlanUtilizationHistoryBuckets() let originalProviderBuckets = providerBuckets let preferredAccount = account ?? self.settings.selectedTokenAccount(for: provider) let accountKey = self.resolvePlanUtilizationAccountKey( provider: provider, snapshot: snapshot, preferredAccount: preferredAccount, claudeOAuthPersistentRefHash: claudeOAuthPersistentRefHash, claudeOAuthHistoryOwnerIdentifier: effectiveOwner, isClaudeOAuthSample: isClaudeOAuthSample, shouldUpdatePreferredAccountKey: shouldUpdatePreferredAccountKey, shouldAdoptUnscopedHistory: shouldAdoptUnscopedHistory, providerBuckets: &providerBuckets) let histories = providerBuckets.histories(for: accountKey) if let updatedHistories = Self.updatedPlanUtilizationHistories( existingHistories: histories, samples: samples) { providerBuckets.setHistories(updatedHistories, for: accountKey) } guard providerBuckets != originalProviderBuckets else { return } self.planUtilizationHistory[provider] = providerBuckets self.planUtilizationHistoryRevision &+= 1 snapshotToPersist = self.planUtilizationHistory } guard let snapshotToPersist else { return } await self.planUtilizationPersistenceCoordinator.enqueue(snapshotToPersist) } private func shouldRecordPlanUtilizationHistory(for provider: UsageProvider) -> Bool { switch provider { case .codex, .claude: true default: self.settings.historicalTrackingEnabled } } private nonisolated static func updatedPlanUtilizationHistories( existingHistories: [PlanUtilizationSeriesHistory], samples: [PlanUtilizationSeriesSample]) -> [PlanUtilizationSeriesHistory]? { guard !samples.isEmpty else { return nil } var historiesByKey: [PlanUtilizationSeriesKey: PlanUtilizationSeriesHistory] = [:] var didChange = false for history in existingHistories { let canonicalWindowMinutes = history.name.canonicalWindowMinutes(history.windowMinutes) let key = PlanUtilizationSeriesKey(name: history.name, windowMinutes: canonicalWindowMinutes) let canonicalHistory = PlanUtilizationSeriesHistory( name: history.name, windowMinutes: canonicalWindowMinutes, entries: history.entries) if let existingHistory = historiesByKey[key] { historiesByKey[key] = PlanUtilizationSeriesHistory( name: history.name, windowMinutes: canonicalWindowMinutes, entries: self.mergedPlanUtilizationEntries(existingHistory.entries + canonicalHistory.entries)) didChange = true } else { historiesByKey[key] = canonicalHistory didChange = didChange || canonicalWindowMinutes != history.windowMinutes } } for sample in samples { let canonicalWindowMinutes = sample.name.canonicalWindowMinutes(sample.windowMinutes) let key = PlanUtilizationSeriesKey(name: sample.name, windowMinutes: canonicalWindowMinutes) if let existingHistory = historiesByKey[key] { guard let updatedEntries = self.updatedPlanUtilizationEntries( existingEntries: existingHistory.entries, entry: sample.entry) else { continue } historiesByKey[key] = PlanUtilizationSeriesHistory( name: sample.name, windowMinutes: canonicalWindowMinutes, entries: updatedEntries) } else { historiesByKey[key] = PlanUtilizationSeriesHistory( name: sample.name, windowMinutes: canonicalWindowMinutes, entries: [sample.entry]) } didChange = true } guard didChange else { return nil } return historiesByKey.values.sorted { lhs, rhs in if lhs.windowMinutes != rhs.windowMinutes { return lhs.windowMinutes < rhs.windowMinutes } return lhs.name.rawValue < rhs.name.rawValue } } private nonisolated static func mergedPlanUtilizationEntries( _ entries: [PlanUtilizationHistoryEntry]) -> [PlanUtilizationHistoryEntry] { entries.reduce(into: []) { result, entry in guard !result.contains(entry) else { return } result.append(entry) } } private nonisolated static func updatedPlanUtilizationEntries( existingEntries: [PlanUtilizationHistoryEntry], entry: PlanUtilizationHistoryEntry) -> [PlanUtilizationHistoryEntry]? { var entries = existingEntries let insertionIndex = entries.firstIndex(where: { $0.capturedAt > entry.capturedAt }) ?? entries.endIndex let sampleHourBucket = self.planUtilizationHourBucket(for: entry.capturedAt) let sameHourRange = self.planUtilizationHourRange( entries: entries, insertionIndex: insertionIndex, hourBucket: sampleHourBucket) let existingHourEntries = Array(entries[sameHourRange]) let canonicalHourEntries = self.canonicalPlanUtilizationHourEntries( existingHourEntries: existingHourEntries, incomingEntry: entry) guard canonicalHourEntries != existingHourEntries else { return nil } entries.replaceSubrange(sameHourRange, with: canonicalHourEntries) if entries.count > self.planUtilizationMaxSamples { entries.removeFirst(entries.count - self.planUtilizationMaxSamples) } return entries } #if DEBUG nonisolated static func _updatedPlanUtilizationEntriesForTesting( existingEntries: [PlanUtilizationHistoryEntry], entry: PlanUtilizationHistoryEntry) -> [PlanUtilizationHistoryEntry]? { self.updatedPlanUtilizationEntries(existingEntries: existingEntries, entry: entry) } nonisolated static func _updatedPlanUtilizationHistoriesForTesting( existingHistories: [PlanUtilizationSeriesHistory], samples: [PlanUtilizationSeriesHistory]) -> [PlanUtilizationSeriesHistory]? { let normalized = samples.flatMap { history in history.entries.map { entry in PlanUtilizationSeriesSample(name: history.name, windowMinutes: history.windowMinutes, entry: entry) } } return self.updatedPlanUtilizationHistories(existingHistories: existingHistories, samples: normalized) } nonisolated static var _planUtilizationMaxSamplesForTesting: Int { self.planUtilizationMaxSamples } #endif private nonisolated static func clampedPercent(_ value: Double?) -> Double? { guard let value else { return nil } return max(0, min(100, value)) } private func postLimitResetCelebrationsIfNeeded( context: LimitResetDetectionContext, samples: [PlanUtilizationSeriesSample]) { let shouldIgnoreCommandCode = context.provider == .commandcode && context.snapshot.commandCodeSubscriptionEnrichmentUnavailable let sessionObservation: LimitResetObservation? = if shouldIgnoreCommandCode { nil } else if context.provider == .codex { samples.last(where: { $0.name == .session }).map { LimitResetObservation( usedPercent: $0.entry.usedPercent, observedAt: $0.entry.capturedAt, resetBoundary: $0.entry.resetsAt, source: nil) } } else { self.sessionQuotaWindow(provider: context.provider, snapshot: context.snapshot).flatMap { resolved in guard Self.isSemanticSessionResetWindow(resolved) else { return nil } return Self.clampedPercent(resolved.window.usedPercent).map { LimitResetObservation( usedPercent: $0, observedAt: context.capturedAt, resetBoundary: resolved.window.resetsAt, source: resolved.source) } } } self.postLimitResetCelebrationIfNeeded( states: &self.sessionLimitResetDetectorStates, context: context, descriptor: LimitResetDetectionDescriptor( seriesName: .session, defaultsKey: Self.sessionLimitResetDetectorDefaultsKey, resetKind: "session"), observation: sessionObservation) let weeklyObservation = samples.last(where: { $0.name == .weekly }).map { LimitResetObservation( usedPercent: $0.entry.usedPercent, observedAt: $0.entry.capturedAt, resetBoundary: $0.entry.resetsAt, source: nil) } self.postLimitResetCelebrationIfNeeded( states: &self.weeklyLimitResetDetectorStates, context: context, descriptor: LimitResetDetectionDescriptor( seriesName: .weekly, defaultsKey: Self.weeklyLimitResetDetectorDefaultsKey, resetKind: "weekly"), observation: weeklyObservation) } private static func isSemanticSessionResetWindow( _ resolved: (window: RateWindow, source: SessionQuotaWindowSource)) -> Bool { guard !resolved.window.isSyntheticPlaceholder else { return false } switch resolved.source { case .primary: guard let minutes = resolved.window.windowMinutes else { return false } return minutes > 0 && minutes <= 6 * 60 case .copilotSecondaryFallback, .zaiTertiary, .antigravityQuotaSummary, .antigravityLegacy: return true } } private func postLimitResetCelebrationIfNeeded( states: inout [String: LimitResetDetectorState], context: LimitResetDetectionContext, descriptor: LimitResetDetectionDescriptor, observation: LimitResetObservation?) { guard let observation else { return } guard let accountIdentifier = self.limitResetAccountIdentifier( provider: context.provider, account: context.account, snapshot: context.snapshot, accountKey: context.accountKey, codexLimitResetOwnerKey: context.codexLimitResetOwnerKey) else { return } let detectorKey = Self.limitResetDetectorStateKey( provider: context.provider, accountIdentifier: accountIdentifier) let currentUsed = observation.usedPercent let currentObservedAt = observation.observedAt let wasAboveThreshold = currentUsed > Self.limitResetThreshold if let existingState = states[detectorKey], currentObservedAt <= existingState.lastObservedAt { return } let previousState = states[detectorKey] let sourceRawValue = observation.source?.rawValue let sourceChanged = descriptor.seriesName == .session && previousState?.sourceRawValue != nil && previousState?.sourceRawValue != sourceRawValue let resetBoundaryAllowsPost = if descriptor.seriesName == .session { Self.limitResetBoundaryAdvanced( previous: previousState?.resetBoundary, current: observation.resetBoundary) } else if context.provider == .codex, descriptor.seriesName == .weekly { Self.limitResetBoundaryAdvanced( previous: previousState?.resetBoundary, current: observation.resetBoundary, requiresPreviousBoundary: true) } else { true } let crossedBelowThreshold = !sourceChanged && previousState?.wasAboveThreshold == true && !wasAboveThreshold let shouldPost = crossedBelowThreshold && resetBoundaryAllowsPost let suppressedGuardedCrossing = crossedBelowThreshold && !resetBoundaryAllowsPost // Sessions retain the last non-regressed boundary on every guarded sample. Codex weekly crossings // adopt a newly appearing boundary so a later genuine advance can still trigger once. let shouldPreserveBoundary = !sourceChanged && !resetBoundaryAllowsPost && (descriptor.seriesName == .session || previousState?.resetBoundary != nil) let shouldPreserveBaseline = suppressedGuardedCrossing states[detectorKey] = LimitResetDetectorState( // A transient zero must not erase the baseline needed to recognize the real reset that follows. wasAboveThreshold: shouldPreserveBaseline ? true : wasAboveThreshold, lastObservedAt: currentObservedAt, sourceRawValue: sourceRawValue, resetBoundary: shouldPreserveBoundary ? previousState?.resetBoundary : observation.resetBoundary) self.persistLimitResetDetectorStates( states, defaultsKey: descriptor.defaultsKey, logName: descriptor.resetKind) guard shouldPost else { return } let accountLabel = self.limitResetAccountLabel( provider: context.provider, account: context.account, snapshot: context.snapshot) CodexBarLog.logger(LogCategories.confetti).info( "\(descriptor.resetKind.capitalized) limit reset", metadata: [ "provider": context.provider.rawValue, "accountIdentifier": accountIdentifier, "accountLabel": accountLabel ?? "", "resetKind": descriptor.resetKind, "usedPercent": String(format: "%.2f", currentUsed), "observedAt": String(format: "%.0f", currentObservedAt.timeIntervalSince1970), ]) switch descriptor.seriesName { case .session: let event = SessionLimitResetEvent( provider: context.provider, accountIdentifier: accountIdentifier, accountLabel: accountLabel, usedPercent: currentUsed) NotificationCenter.default.post(name: .codexbarSessionLimitReset, object: event) case .weekly: let event = WeeklyLimitResetEvent( provider: context.provider, accountIdentifier: accountIdentifier, accountLabel: accountLabel, usedPercent: currentUsed) NotificationCenter.default.post(name: .codexbarWeeklyLimitReset, object: event) default: return } } private func planUtilizationSeriesSamples( provider: UsageProvider, snapshot: UsageSnapshot, capturedAt: Date) -> [PlanUtilizationSeriesSample] { var samplesByKey: [PlanUtilizationSeriesKey: PlanUtilizationSeriesSample] = [:] func appendWindow(_ window: RateWindow?, name: PlanUtilizationSeriesName?) { guard let name, let window, !window.isSyntheticPlaceholder, let windowMinutes = window.windowMinutes, windowMinutes > 0, let usedPercent = Self.clampedPercent(window.usedPercent) else { return } let canonicalWindowMinutes = name.canonicalWindowMinutes(windowMinutes) let key = PlanUtilizationSeriesKey(name: name, windowMinutes: canonicalWindowMinutes) samplesByKey[key] = PlanUtilizationSeriesSample( name: name, windowMinutes: canonicalWindowMinutes, entry: PlanUtilizationHistoryEntry( capturedAt: capturedAt, usedPercent: usedPercent, resetsAt: window.resetsAt)) } switch provider { case .codex: let projection = self.codexConsumerProjection( surface: .liveCard, snapshotOverride: snapshot, now: capturedAt) for lane in projection.planUtilizationLanes { appendWindow(lane.window, name: lane.role) } case .claude: appendWindow(snapshot.primary, name: .session) appendWindow(snapshot.secondary, name: .weekly) appendWindow(snapshot.tertiary, name: .opus) case .antigravity: let namedWeeklyWindows = snapshot.extraRateWindows? .filter { $0.usageKnown && $0.id.hasPrefix("antigravity-quota-summary-") && $0.window.windowMinutes == Self.weeklyWindowMinutes } .map(\.window) ?? [] if let mostUsedWeeklyWindow = namedWeeklyWindows.max(by: { $0.usedPercent < $1.usedPercent }) { appendWindow(mostUsedWeeklyWindow, name: .weekly) } else { for window in [snapshot.primary, snapshot.secondary, snapshot.tertiary] { guard let window, window.windowMinutes == Self.weeklyWindowMinutes else { continue } appendWindow(window, name: .weekly) } } default: let standardWeeklyWindow = [snapshot.primary, snapshot.secondary, snapshot.tertiary] .compactMap(\.self) .first { $0.windowMinutes == Self.weeklyWindowMinutes } let extraWeeklyWindow = snapshot.extraRateWindows? .lazy .first { $0.usageKnown && $0.window.windowMinutes == Self.weeklyWindowMinutes }? .window if let weeklyWindow = standardWeeklyWindow ?? extraWeeklyWindow { appendWindow(weeklyWindow, name: .weekly) } } return samplesByKey.values.sorted { lhs, rhs in if lhs.windowMinutes != rhs.windowMinutes { return lhs.windowMinutes < rhs.windowMinutes } return lhs.name.rawValue < rhs.name.rawValue } } private nonisolated static func planUtilizationHourBucket(for date: Date) -> Int64 { Int64(floor(date.timeIntervalSince1970 / self.planUtilizationMinSampleIntervalSeconds)) } private nonisolated static func planUtilizationHourRange( entries: [PlanUtilizationHistoryEntry], insertionIndex: Int, hourBucket: Int64) -> Range { var lowerBound = insertionIndex while lowerBound > entries.startIndex { let previousIndex = lowerBound - 1 let previousHourBucket = self.planUtilizationHourBucket(for: entries[previousIndex].capturedAt) guard previousHourBucket == hourBucket else { break } lowerBound = previousIndex } var upperBound = insertionIndex while upperBound < entries.endIndex { let currentHourBucket = self.planUtilizationHourBucket(for: entries[upperBound].capturedAt) guard currentHourBucket == hourBucket else { break } upperBound += 1 } return lowerBound.. [PlanUtilizationHistoryEntry] { let hourlyObservations = (existingHourEntries + [incomingEntry]).sorted { lhs, rhs in if lhs.capturedAt != rhs.capturedAt { return lhs.capturedAt < rhs.capturedAt } if lhs.usedPercent != rhs.usedPercent { return lhs.usedPercent < rhs.usedPercent } let lhsReset = lhs.resetsAt?.timeIntervalSince1970 ?? Date.distantPast.timeIntervalSince1970 let rhsReset = rhs.resetsAt?.timeIntervalSince1970 ?? Date.distantPast.timeIntervalSince1970 return lhsReset < rhsReset } guard var activeSegmentPeak = hourlyObservations.first else { return [] } var peakBeforeLatestReset: PlanUtilizationHistoryEntry? for observation in hourlyObservations.dropFirst() { if self.startsNewPlanUtilizationResetSegment( activeSegmentPeak: activeSegmentPeak, observation: observation) { if peakBeforeLatestReset == nil { peakBeforeLatestReset = activeSegmentPeak } activeSegmentPeak = observation continue } activeSegmentPeak = self.segmentPeakEntry( existingPeak: activeSegmentPeak, observation: observation) } if let peakBeforeLatestReset { return [peakBeforeLatestReset, activeSegmentPeak] } return [activeSegmentPeak] } private nonisolated static func startsNewPlanUtilizationResetSegment( activeSegmentPeak: PlanUtilizationHistoryEntry, observation: PlanUtilizationHistoryEntry) -> Bool { self.haveMeaningfullyDifferentResetBoundaries( activeSegmentPeak.resetsAt, observation.resetsAt) } private nonisolated static func segmentPeakEntry( existingPeak: PlanUtilizationHistoryEntry, observation: PlanUtilizationHistoryEntry) -> PlanUtilizationHistoryEntry { if existingPeak.resetsAt == nil, observation.resetsAt != nil { return observation } let hasHigherUsage = observation.usedPercent > existingPeak.usedPercent let tiesUsageAndIsMoreRecent = observation.usedPercent == existingPeak.usedPercent && observation.capturedAt >= existingPeak.capturedAt let observationShouldReplacePeak = hasHigherUsage || tiesUsageAndIsMoreRecent let peakSource = observationShouldReplacePeak ? observation : existingPeak let preferObservationMetadata = observation.capturedAt >= existingPeak.capturedAt return PlanUtilizationHistoryEntry( capturedAt: peakSource.capturedAt, usedPercent: peakSource.usedPercent, resetsAt: self.preferredResetBoundary( existing: existingPeak.resetsAt, incoming: observation.resetsAt, preferIncoming: preferObservationMetadata)) } private nonisolated static func haveMeaningfullyDifferentResetBoundaries(_ lhs: Date?, _ rhs: Date?) -> Bool { switch (lhs, rhs) { case let (lhs?, rhs?): abs(lhs.timeIntervalSince(rhs)) >= self.planUtilizationResetEquivalenceToleranceSeconds case (.none, .none): false default: false } } private nonisolated static func preferredResetBoundary( existing: Date?, incoming: Date?, preferIncoming: Bool) -> Date? { if preferIncoming { return incoming ?? existing } return existing ?? incoming } private func planUtilizationAccountKey( for provider: UsageProvider, snapshot: UsageSnapshot? = nil, preferredAccount: ProviderTokenAccount? = nil) -> String? { let account = preferredAccount ?? self.settings.selectedTokenAccount(for: provider) let accountKey = Self.planUtilizationAccountKey(provider: provider, account: account) if let accountKey { return accountKey } let resolvedSnapshot = snapshot ?? self.snapshots[provider] return resolvedSnapshot.flatMap { Self.planUtilizationIdentityAccountKey(provider: provider, snapshot: $0) } } private nonisolated static func planUtilizationAccountKey( provider: UsageProvider, account: ProviderTokenAccount?) -> String? { guard let account else { return nil } return self.sha256Hex("\(provider.rawValue):token-account:\(account.id.uuidString.lowercased())") } /// The Keychain row reference is corroborating provenance, not principal identity. Excluding it from the /// canonical key keeps one credential stable when its row is recreated, while requiring the credential /// discriminator ensures an in-place login replacement cannot inherit the prior principal's history. private nonisolated static func claudeOAuthPlanUtilizationAccountKey( historyOwnerIdentifier: String?, corroboratingPersistentRefHash _: String? = nil) -> String? { guard let normalizedIdentifier = historyOwnerIdentifier? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased(), normalizedIdentifier.count == 64, normalizedIdentifier.allSatisfy(\.isHexDigit) else { return nil } let digest = self.sha256Hex("claude:oauth-history-owner:v2:\(normalizedIdentifier)") return "\(self.claudeOAuthPlanUtilizationAccountKeyPrefix)\(digest)" } private nonisolated static func isClaudeOAuthPlanUtilizationAccountKey(_ accountKey: String?) -> Bool { accountKey?.hasPrefix(self.claudeOAuthPlanUtilizationAccountKeyPrefix) == true } private nonisolated static func planUtilizationIdentityAccountKey( provider: UsageProvider, snapshot: UsageSnapshot) -> String? { guard let identity = snapshot.identity(for: provider) else { return nil } let normalizedEmail = identity.accountEmail? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() if let normalizedEmail, !normalizedEmail.isEmpty { if provider == .codex { return CodexHistoryOwnership.canonicalEmailHashKey(for: normalizedEmail) } if provider == .claude { let normalizedOrganization = identity.accountOrganization? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() let normalizedLoginMethod = identity.loginMethod? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() let normalizedPlan = ClaudePlan.fromCompatibilityLoginMethod(identity.loginMethod)?.rawValue let organizationDiscriminator: String? = if let normalizedOrganization, !normalizedOrganization.isEmpty { "org:\(normalizedOrganization)" } else { nil } let planDiscriminator = normalizedPlan.map { "plan:\($0)" } let loginMethodDiscriminator: String? = if let normalizedLoginMethod, !normalizedLoginMethod.isEmpty { "plan:\(normalizedLoginMethod)" } else { nil } let discriminator = organizationDiscriminator ?? planDiscriminator ?? loginMethodDiscriminator guard let discriminator else { return self.sha256Hex("claude:email:\(normalizedEmail)") } return self.sha256Hex("\(provider.rawValue):email:\(normalizedEmail):\(discriminator)") } return self.sha256Hex("\(provider.rawValue):email:\(normalizedEmail)") } if provider == .claude { return nil } let normalizedOrganization = identity.accountOrganization? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() if let normalizedOrganization, !normalizedOrganization.isEmpty { return self.sha256Hex("\(provider.rawValue):organization:\(normalizedOrganization)") } return nil } private nonisolated static func legacyClaudePlanUtilizationEmailAccountKey(snapshot: UsageSnapshot) -> String? { guard let identity = snapshot.identity(for: .claude) else { return nil } let normalizedEmail = identity.accountEmail? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() guard let normalizedEmail, !normalizedEmail.isEmpty else { return nil } return self.sha256Hex("claude:email:\(normalizedEmail)") } private func shouldDeferClaudePlanUtilizationHistory(provider: UsageProvider) -> Bool { provider == .claude && self.shouldHidePlanUtilizationMenuItem(for: .claude) } private nonisolated static func limitResetDetectorStateKey( provider: UsageProvider, accountIdentifier: String) -> String { "\(provider.rawValue):\(accountIdentifier)" } nonisolated static func loadWeeklyLimitResetDetectorStates(from userDefaults: UserDefaults) -> [String: LimitResetDetectorState] { self.loadLimitResetDetectorStates( from: userDefaults, defaultsKey: self.weeklyLimitResetDetectorDefaultsKey, logName: "weekly") } nonisolated static func loadLimitResetDetectorStates( from userDefaults: UserDefaults, defaultsKey: String, logName: String) -> [String: LimitResetDetectorState] { guard let data = userDefaults.data(forKey: defaultsKey) else { return [:] } do { return try JSONDecoder().decode([String: LimitResetDetectorState].self, from: data) } catch { CodexBarLog.logger(LogCategories.confetti).error( "Failed to decode \(logName) limit reset detector state", metadata: ["error": String(describing: error)]) return [:] } } private func persistLimitResetDetectorStates( _ states: [String: LimitResetDetectorState], defaultsKey: String, logName: String) { do { let data = try JSONEncoder().encode(states) self.settings.userDefaults.set(data, forKey: defaultsKey) } catch { CodexBarLog.logger(LogCategories.confetti).error( "Failed to encode \(logName) limit reset detector state", metadata: ["error": String(describing: error)]) } } // MARK: - Active Claude account corroboration (~/.claude.json) /// The currently-active Claude account UUID, read prompt-free from `~/.claude.json`. This is the only /// always-fresh, never-gated signal of the active account on a background poll: Claude Code's `/login` /// updates the Keychain item in place and leaves `~/.claude/.credentials.json` stale, but immediately /// rewrites `oauthAccount.accountUuid` in this sibling plain file. Returns nil on absence/corruption. nonisolated static func activeClaudeAccountUuid() -> String? { ClaudeActiveAccountProbe.activeClaudeAccountUuid() } /// Persisted `historyOwnerIdentifier -> hashed active account identity` bindings. nonisolated static func loadClaudeOAuthAccountUuidMap(from userDefaults: UserDefaults) -> [String: String] { guard let data = userDefaults.data(forKey: claudeOAuthAccountUuidMapDefaultsKey) else { return [:] } do { return try JSONDecoder().decode([String: String].self, from: data) } catch { CodexBarLog.logger(LogCategories.confetti).error( "Failed to decode Claude OAuth history owner account UUID map", metadata: ["error": String(describing: error)]) return [:] } } /// Persist the `historyOwnerIdentifier -> active accountUuid` map. Mirrors `persistLimitResetDetectorStates`. func persistClaudeOAuthAccountUuidMap(_ map: [String: String]) { do { let data = try JSONEncoder().encode(map) self.settings.userDefaults.set(data, forKey: Self.claudeOAuthAccountUuidMapDefaultsKey) } catch { CodexBarLog.logger(LogCategories.confetti).error( "Failed to encode Claude OAuth history owner account UUID map", metadata: ["error": String(describing: error)]) } } nonisolated static func loadClaudeOAuthAccountBindingCandidateMap( from userDefaults: UserDefaults) -> [String: ClaudeOAuthAccountBindingCandidate] { guard let data = userDefaults.data(forKey: claudeOAuthAccountCandidateMapDefaultsKey) else { return [:] } do { return try JSONDecoder().decode([String: ClaudeOAuthAccountBindingCandidate].self, from: data) } catch { CodexBarLog.logger(LogCategories.confetti).error( "Failed to decode Claude OAuth account binding candidates", metadata: ["error": String(describing: error)]) return [:] } } private func confirmClaudeOAuthAccountBindingCandidate( owner: String, identity: String, observedAt: Date) -> Bool { var candidates = Self.loadClaudeOAuthAccountBindingCandidateMap(from: self.settings.userDefaults) if let candidate = candidates[owner], candidate.identity == identity, candidate.observedAt < observedAt { candidates.removeValue(forKey: owner) self.persistClaudeOAuthAccountBindingCandidateMap(candidates) return true } candidates[owner] = ClaudeOAuthAccountBindingCandidate(identity: identity, observedAt: observedAt) self.persistClaudeOAuthAccountBindingCandidateMap(candidates) return false } private func resolvedClaudeOAuthHistoryOwner(evidence: ClaudeOAuthHistoryEvidence) -> String? { let requiresClaudeCodeCorroboration = evidence.persistentRefHash != nil || evidence.keychainCredentialMismatch || evidence.keychainCredentialAbsent || evidence.keychainCredentialUnavailable guard requiresClaudeCodeCorroboration else { // Explicit/environment credentials do not belong to Claude Code's active-account lifecycle. return evidence.owner } guard case let .stable(currentAccountIdentity) = evidence.activeAccountObservation else { // An account/credential change while capturing the UUID cannot safely identify this sample. return nil } var map = Self.loadClaudeOAuthAccountUuidMap(from: self.settings.userDefaults) if let mapped = map[evidence.owner] { guard let currentAccountIdentity else { return evidence.keychainCredentialMismatch || evidence.keychainCredentialUnavailable ? nil : evidence.owner } guard mapped != currentAccountIdentity else { self.clearClaudeOAuthAccountBindingCandidate(owner: evidence.owner) return evidence.owner } guard evidence.persistentRefHash != nil, self.confirmClaudeOAuthAccountBindingCandidate( owner: evidence.owner, identity: currentAccountIdentity, observedAt: evidence.observedAt) else { return nil } // Two stable exact-Keychain observations repair a binding poisoned by a non-atomic login. map[evidence.owner] = currentAccountIdentity self.persistClaudeOAuthAccountUuidMap(map) return evidence.owner } if evidence.keychainCredentialUnavailable, !evidence.keychainCredentialMismatch { // With no authoritative binding, the secret-derived file owner is the only safe bootstrap scope. // Existing bindings are checked above, so normal background gating cannot bypass a detected switch. return evidence.owner } if evidence.keychainCredentialAbsent { // A proven-empty Keychain leaves the file credential as the only owner. Existing bindings were // checked above, so an unbound owner is safe without inventing account continuity. return evidence.owner } guard let currentAccountIdentity else { return evidence.keychainCredentialMismatch || evidence.keychainCredentialUnavailable ? nil : evidence.owner } guard evidence.persistentRefHash != nil else { return nil } // Two stable exact-Keychain observations are required before a first binding becomes authoritative. if self.confirmClaudeOAuthAccountBindingCandidate( owner: evidence.owner, identity: currentAccountIdentity, observedAt: evidence.observedAt) { map[evidence.owner] = currentAccountIdentity self.persistClaudeOAuthAccountUuidMap(map) } return evidence.owner } private func clearClaudeOAuthAccountBindingCandidate(owner: String) { var candidates = Self.loadClaudeOAuthAccountBindingCandidateMap(from: self.settings.userDefaults) guard candidates.removeValue(forKey: owner) != nil else { return } self.persistClaudeOAuthAccountBindingCandidateMap(candidates) } private func persistClaudeOAuthAccountBindingCandidateMap( _ candidates: [String: ClaudeOAuthAccountBindingCandidate]) { do { let data = try JSONEncoder().encode(candidates) self.settings.userDefaults.set(data, forKey: Self.claudeOAuthAccountCandidateMapDefaultsKey) } catch { CodexBarLog.logger(LogCategories.confetti).error( "Failed to encode Claude OAuth account binding candidates", metadata: ["error": String(describing: error)]) } } nonisolated static func activeClaudeAccountIdentity() -> String? { self.activeClaudeAccountUuid().map(self.claudeAccountIdentity) } private nonisolated static func claudeAccountIdentity(_ uuid: String) -> String { self.sha256Hex( "claude:active-account:v1:\(uuid.trimmingCharacters(in: .whitespacesAndNewlines).lowercased())") } #if DEBUG static func withActiveClaudeAccountUuidForTesting( _ uuid: String?, _ body: () async throws -> T) async rethrows -> T { try await ClaudeActiveAccountProbe.$activeClaudeAccountUuidOverrideForTesting.withValue( .value(uuid), operation: body) } nonisolated static func _activeClaudeAccountIdentityForTesting(_ uuid: String) -> String { self.claudeAccountIdentity(uuid) } #endif private func resolvePlanUtilizationAccountKey( provider: UsageProvider, snapshot: UsageSnapshot?, preferredAccount: ProviderTokenAccount?, claudeOAuthPersistentRefHash: String? = nil, claudeOAuthHistoryOwnerIdentifier: String? = nil, isClaudeOAuthSample: Bool = false, shouldUpdatePreferredAccountKey: Bool = true, shouldAdoptUnscopedHistory: Bool = true, providerBuckets: inout PlanUtilizationHistoryBuckets) -> String? { if provider == .codex { return self.resolveCodexPlanUtilizationAccountKey( snapshot: snapshot, shouldUpdatePreferredAccountKey: shouldUpdatePreferredAccountKey, shouldAdoptUnscopedHistory: shouldAdoptUnscopedHistory, providerBuckets: &providerBuckets) } // Claude's unscoped history is only safe to adopt during the first unambiguous migration. // The sentinel marks identityless OAuth, while any scoped bucket proves multiple owners may exist. let canAdoptUnscopedHistory = shouldAdoptUnscopedHistory && !(provider == .claude && (providerBuckets.preferredAccountKey == Self.planUtilizationUnscopedPreferredKey || !providerBuckets.accounts.isEmpty)) if provider == .claude, isClaudeOAuthSample { if let oauthAccountKey = Self.claudeOAuthPlanUtilizationAccountKey( historyOwnerIdentifier: claudeOAuthHistoryOwnerIdentifier, corroboratingPersistentRefHash: claudeOAuthPersistentRefHash) { if shouldUpdatePreferredAccountKey { providerBuckets.preferredAccountKey = oauthAccountKey } // Existing unscoped or identity-keyed history can belong to another OAuth account. // Preserve it in place rather than silently adopting it into this opaque account. return oauthAccountKey } // Never append identityless OAuth samples to the shared unscoped bucket. A future fetch with // trustworthy ownership evidence can start a scoped history without inheriting this sample. return nil } let resolvedAccount = preferredAccount ?? self.settings.selectedTokenAccount(for: provider) if let tokenAccountKey = Self.planUtilizationAccountKey(provider: provider, account: resolvedAccount) { if shouldUpdatePreferredAccountKey { providerBuckets.preferredAccountKey = tokenAccountKey } if canAdoptUnscopedHistory { self.adoptPlanUtilizationUnscopedHistoryIfNeeded( into: tokenAccountKey, provider: provider, providerBuckets: &providerBuckets) } return tokenAccountKey } if let snapshot, let identityAccountKey = Self.planUtilizationIdentityAccountKey(provider: provider, snapshot: snapshot) { let resolvedIdentityAccountKey = self.materializeLegacyClaudePlanUtilizationHistoryIfNeeded( into: identityAccountKey, provider: provider, snapshot: snapshot, providerBuckets: &providerBuckets) if shouldUpdatePreferredAccountKey { providerBuckets.preferredAccountKey = resolvedIdentityAccountKey } if canAdoptUnscopedHistory { self.adoptPlanUtilizationUnscopedHistoryIfNeeded( into: resolvedIdentityAccountKey, provider: provider, providerBuckets: &providerBuckets) } return resolvedIdentityAccountKey } if let stickyAccountKey = self.stickyPlanUtilizationAccountKey(providerBuckets: providerBuckets) { return stickyAccountKey } return nil } private func resolveCodexPlanUtilizationAccountKey( snapshot: UsageSnapshot?, shouldUpdatePreferredAccountKey: Bool, shouldAdoptUnscopedHistory: Bool, providerBuckets: inout PlanUtilizationHistoryBuckets) -> String? { let ownership = self.codexOwnershipContext(snapshot: snapshot, includeDashboardFallback: true) if let canonicalKey = ownership.canonicalKey { let resolvedAccountKey = self.materializeCodexPlanUtilizationHistoryIfNeeded( into: canonicalKey, ownership: ownership, shouldAdoptUnscopedHistory: shouldAdoptUnscopedHistory, providerBuckets: &providerBuckets) if shouldUpdatePreferredAccountKey { providerBuckets.preferredAccountKey = resolvedAccountKey } return resolvedAccountKey } if let stickyAccountKey = self.stickyPlanUtilizationAccountKey(providerBuckets: providerBuckets) { return stickyAccountKey } return nil } private func materializeCodexPlanUtilizationHistoryIfNeeded( into canonicalKey: String, ownership: CodexOwnershipContext, shouldAdoptUnscopedHistory: Bool, providerBuckets: inout PlanUtilizationHistoryBuckets) -> String { var historiesToMerge: [[PlanUtilizationSeriesHistory]] = [] let scopedRawKeys = Array(providerBuckets.accounts.keys) var legacyRawKeysToRemove: [String] = [] for rawKey in scopedRawKeys { let owner = CodexHistoryOwnership.classifyPersistedKey( rawKey, legacyEmailHash: ownership.planUtilizationLegacyEmailHash) let matchesTargetContinuity = CodexHistoryOwnership.belongsToTargetContinuity( owner, targetCanonicalKey: canonicalKey, canonicalEmailHashKey: ownership.canonicalEmailHashKey) if matchesTargetContinuity, !Self.codexPlanHistoryOwnerIsAmbiguousEmailScope(owner, ownership: ownership), let accountHistories = providerBuckets.accounts[rawKey], !accountHistories.isEmpty { historiesToMerge.append(accountHistories) if rawKey != canonicalKey { legacyRawKeysToRemove.append(rawKey) } } } if let recoverableOpaqueRawKey = self.recoverableCodexOpaquePlanHistoryRawKey( targetCanonicalKey: canonicalKey, ownership: ownership, providerBuckets: providerBuckets), let opaqueHistories = providerBuckets.accounts[recoverableOpaqueRawKey], !opaqueHistories.isEmpty { historiesToMerge.append(opaqueHistories) legacyRawKeysToRemove.append(recoverableOpaqueRawKey) } if shouldAdoptUnscopedHistory, !providerBuckets.unscoped.isEmpty, CodexHistoryOwnership.hasStrictSingleAccountContinuity( scopedRawKeys: Self.scopedRawKeysRelevantToCodexUnscopedPlanHistory(providerBuckets), targetCanonicalKey: canonicalKey, canonicalEmailHashKey: ownership.canonicalEmailHashKey, legacyEmailHash: ownership.planUtilizationLegacyEmailHash, hasAdjacentMultiAccountVeto: ownership.hasAdjacentMultiAccountVeto) { historiesToMerge.append(providerBuckets.unscoped) providerBuckets.unscoped = [] } guard !historiesToMerge.isEmpty else { return canonicalKey } for rawKey in legacyRawKeysToRemove { providerBuckets.accounts.removeValue(forKey: rawKey) } let mergedHistory = Self.mergedPlanUtilizationHistories(provider: .codex, histories: historiesToMerge) providerBuckets.setHistories(mergedHistory, for: canonicalKey) return canonicalKey } private static func codexPlanHistoryOwnerIsAmbiguousEmailScope( _ owner: CodexHistoryPersistedOwner, ownership: CodexOwnershipContext) -> Bool { guard ownership.hasAdjacentEmailScopeAmbiguity else { return false } return switch owner { case let .canonical(key): key == ownership.canonicalEmailHashKey case .legacyEmailHash: true case .legacyOpaqueScoped, .legacyUnscoped: false } } private func materializeLegacyClaudePlanUtilizationHistoryIfNeeded( into accountKey: String, provider: UsageProvider, snapshot: UsageSnapshot, providerBuckets: inout PlanUtilizationHistoryBuckets) -> String { guard provider == .claude, let legacyAccountKey = Self.legacyClaudePlanUtilizationEmailAccountKey(snapshot: snapshot), legacyAccountKey != accountKey, let legacyHistories = providerBuckets.accounts[legacyAccountKey], !legacyHistories.isEmpty else { return accountKey } let existingHistories = providerBuckets.accounts[accountKey] ?? [] let mergedHistory = Self.mergedPlanUtilizationHistories(provider: provider, histories: [ existingHistories, legacyHistories, ]) providerBuckets.accounts.removeValue(forKey: legacyAccountKey) providerBuckets.setHistories(mergedHistory, for: accountKey) if providerBuckets.preferredAccountKey == legacyAccountKey { providerBuckets.preferredAccountKey = accountKey } return accountKey } private func adoptPlanUtilizationUnscopedHistoryIfNeeded( into accountKey: String, provider: UsageProvider, providerBuckets: inout PlanUtilizationHistoryBuckets) { guard !providerBuckets.unscoped.isEmpty else { return } let existingHistory = providerBuckets.accounts[accountKey] ?? [] let mergedHistory = Self.mergedPlanUtilizationHistories(provider: provider, histories: [ existingHistory, providerBuckets.unscoped, ]) providerBuckets.setHistories(mergedHistory, for: accountKey) providerBuckets.setHistories([], for: nil) } private func stickyPlanUtilizationAccountKey( providerBuckets: PlanUtilizationHistoryBuckets) -> String? { if providerBuckets.preferredAccountKey == Self.planUtilizationUnscopedPreferredKey { return nil } let knownAccountKeys = self.knownPlanUtilizationAccountKeys(providerBuckets: providerBuckets) guard !knownAccountKeys.isEmpty else { return nil } if let preferredAccountKey = providerBuckets.preferredAccountKey, knownAccountKeys.contains(preferredAccountKey) { return preferredAccountKey } if knownAccountKeys.count == 1 { return knownAccountKeys[0] } return knownAccountKeys.max { lhs, rhs in let lhsDate = providerBuckets.accounts[lhs]?.compactMap(\.latestCapturedAt).max() ?? .distantPast let rhsDate = providerBuckets.accounts[rhs]?.compactMap(\.latestCapturedAt).max() ?? .distantPast if lhsDate == rhsDate { return lhs > rhs } return lhsDate < rhsDate } } private func knownPlanUtilizationAccountKeys(providerBuckets: PlanUtilizationHistoryBuckets) -> [String] { providerBuckets.accounts.keys .sorted() } private func recoverableCodexOpaquePlanHistoryRawKey( targetCanonicalKey: String, ownership: CodexOwnershipContext, providerBuckets: PlanUtilizationHistoryBuckets) -> String? { guard !ownership.hasAdjacentMultiAccountVeto, let targetWeeklyResetAt = ownership.currentWeeklyResetAt else { return nil } let candidates = providerBuckets.accounts.compactMap { rawKey, histories -> String? in let owner = CodexHistoryOwnership.classifyPersistedKey( rawKey, legacyEmailHash: ownership.planUtilizationLegacyEmailHash) guard case .legacyOpaqueScoped = owner else { return nil } guard Self.isRecoverableCodexOpaquePlanHistory( histories, targetWeeklyResetAt: targetWeeklyResetAt) else { return nil } return rawKey } guard candidates.count == 1, let recoverableRawKey = candidates.first, let targetWeeklyResetAt = ownership.currentWeeklyResetAt else { return nil } guard !Self.hasConflictingScopedCodexPlanHistory( recoverableRawKey: recoverableRawKey, targetWeeklyResetAt: targetWeeklyResetAt, targetCanonicalKey: targetCanonicalKey, ownership: ownership, providerBuckets: providerBuckets) else { return nil } return recoverableRawKey } private nonisolated static func isRecoverableCodexOpaquePlanHistory( _ histories: [PlanUtilizationSeriesHistory], targetWeeklyResetAt: Date) -> Bool { guard let weekly = histories.first(where: { $0.name == .weekly && $0.windowMinutes == 10080 }), let session = histories.first(where: { $0.name == .session && $0.windowMinutes == 300 }), !session.entries.isEmpty else { return false } let distinctWeeklyResets = Set(weekly.entries.compactMap(\.resetsAt)) guard distinctWeeklyResets.count >= 2 else { return false } guard weekly.entries.contains(where: { entry in Self.areEquivalentPlanUtilizationResetBoundaries(entry.resetsAt, targetWeeklyResetAt) }) else { return false } guard weekly.entries.contains(where: { entry in guard let reset = entry.resetsAt else { return false } return !Self.areEquivalentPlanUtilizationResetBoundaries(reset, targetWeeklyResetAt) }) else { return false } return true } nonisolated static func areEquivalentPlanUtilizationResetBoundaries(_ lhs: Date?, _ rhs: Date?) -> Bool { guard let lhs, let rhs else { return false } return abs(lhs.timeIntervalSince(rhs)) < self.planUtilizationResetEquivalenceToleranceSeconds } private nonisolated static func scopedRawKeysRelevantToCodexUnscopedPlanHistory( _ providerBuckets: PlanUtilizationHistoryBuckets) -> [String] { guard let continuityWindow = self.planUtilizationContinuityWindow(for: providerBuckets) else { return [] } return providerBuckets.accounts.compactMap { rawKey, histories in guard self.planUtilizationHistories(histories, overlap: continuityWindow) else { return nil } return rawKey } } private nonisolated static func planUtilizationContinuityWindow( for providerBuckets: PlanUtilizationHistoryBuckets) -> ClosedRange? { let capturedDates = providerBuckets.unscoped.flatMap(\.entries).map(\.capturedAt) guard let lowerBound = capturedDates.min(), let upperBound = capturedDates.max() else { return nil } let allHistories = providerBuckets.unscoped + providerBuckets.accounts.values.flatMap(\.self) let expansionMinutes = allHistories.map(\.windowMinutes).max() ?? 0 let expansion = TimeInterval(expansionMinutes) * 60 return lowerBound.addingTimeInterval(-expansion)...upperBound.addingTimeInterval(expansion) } private nonisolated static func planUtilizationHistories( _ histories: [PlanUtilizationSeriesHistory], overlap continuityWindow: ClosedRange) -> Bool { histories.contains { history in history.entries.contains { continuityWindow.contains($0.capturedAt) } } } private nonisolated static func hasConflictingScopedCodexPlanHistory( recoverableRawKey: String, targetWeeklyResetAt: Date, targetCanonicalKey: String, ownership: CodexOwnershipContext, providerBuckets: PlanUtilizationHistoryBuckets) -> Bool { providerBuckets.accounts.contains { rawKey, histories in guard rawKey != recoverableRawKey else { return false } guard self.historiesContainEquivalentWeeklyResetBoundary( histories, targetWeeklyResetAt: targetWeeklyResetAt) else { return false } let owner = CodexHistoryOwnership.classifyPersistedKey( rawKey, legacyEmailHash: ownership.planUtilizationLegacyEmailHash) switch owner { case .legacyOpaqueScoped: return false case .canonical, .legacyEmailHash: return !CodexHistoryOwnership.belongsToTargetContinuity( owner, targetCanonicalKey: targetCanonicalKey, canonicalEmailHashKey: ownership.canonicalEmailHashKey) case .legacyUnscoped: return false } } } private nonisolated static func historiesContainEquivalentWeeklyResetBoundary( _ histories: [PlanUtilizationSeriesHistory], targetWeeklyResetAt: Date) -> Bool { histories.contains { history in history.entries.contains { entry in self.areEquivalentPlanUtilizationResetBoundaries(entry.resetsAt, targetWeeklyResetAt) } } } private nonisolated static func mergedPlanUtilizationHistories( provider _: UsageProvider, histories: [[PlanUtilizationSeriesHistory]]) -> [PlanUtilizationSeriesHistory] { var mergedByKey: [PlanUtilizationSeriesKey: PlanUtilizationSeriesHistory] = [:] for historyGroup in histories { for history in historyGroup { let key = PlanUtilizationSeriesKey(name: history.name, windowMinutes: history.windowMinutes) let existingEntries = mergedByKey[key]?.entries ?? [] var mergedEntries = existingEntries for entry in history.entries.sorted(by: { $0.capturedAt < $1.capturedAt }) { if let updatedEntries = self.updatedPlanUtilizationEntries( existingEntries: mergedEntries, entry: entry) { mergedEntries = updatedEntries } } mergedByKey[key] = PlanUtilizationSeriesHistory( name: history.name, windowMinutes: history.windowMinutes, entries: mergedEntries) } } return mergedByKey.values.sorted { lhs, rhs in if lhs.windowMinutes != rhs.windowMinutes { return lhs.windowMinutes < rhs.windowMinutes } return lhs.name.rawValue < rhs.name.rawValue } } #if DEBUG nonisolated static func _planUtilizationAccountKeyForTesting( provider: UsageProvider, snapshot: UsageSnapshot) -> String? { self.planUtilizationIdentityAccountKey(provider: provider, snapshot: snapshot) } nonisolated static func _planUtilizationTokenAccountKeyForTesting( provider: UsageProvider, account: ProviderTokenAccount) -> String? { self.planUtilizationAccountKey(provider: provider, account: account) } nonisolated static func _claudeOAuthPlanUtilizationAccountKeyForTesting( historyOwnerIdentifier: String?, persistentRefHash: String? = nil) -> String? { self.claudeOAuthPlanUtilizationAccountKey( historyOwnerIdentifier: historyOwnerIdentifier, corroboratingPersistentRefHash: persistentRefHash) } nonisolated static func _legacyClaudePlanUtilizationEmailAccountKeyForTesting(snapshot: UsageSnapshot) -> String? { self.legacyClaudePlanUtilizationEmailAccountKey(snapshot: snapshot) } nonisolated static func _codexLegacyPlanUtilizationEmailHashKeyForTesting( normalizedEmail: String) -> String { self.codexLegacyPlanUtilizationEmailHashKey(for: normalizedEmail) } #endif } actor PlanUtilizationHistoryPersistenceCoordinator { private let store: PlanUtilizationHistoryStore private var pendingSnapshot: [UsageProvider: PlanUtilizationHistoryBuckets]? private var isPersisting: Bool = false init(store: PlanUtilizationHistoryStore) { self.store = store } func enqueue(_ snapshot: [UsageProvider: PlanUtilizationHistoryBuckets]) { self.pendingSnapshot = snapshot guard !self.isPersisting else { return } self.isPersisting = true Task(priority: .utility) { await self.persistLoop() } } private func persistLoop() async { while let nextSnapshot = self.pendingSnapshot { self.pendingSnapshot = nil await self.saveAsync(nextSnapshot) } self.isPersisting = false } private func saveAsync(_ snapshot: [UsageProvider: PlanUtilizationHistoryBuckets]) async { let store = self.store await Task.detached(priority: .utility) { store.save(snapshot) }.value } } /// Prompt-free reader for the active Claude account UUID recorded in `~/.claude.json`. The `@TaskLocal` test /// seam lives here (not on `UsageStore`) because Swift forbids stored properties in extensions and task-local /// storage must be nonisolated, whereas `UsageStore` is `@MainActor`. private enum ClaudeActiveAccountProbe { #if DEBUG enum Override: Sendable { case value(String?) } @TaskLocal static var activeClaudeAccountUuidOverrideForTesting: Override? #endif private struct ClaudeConfigAccount: Decodable { struct OAuthAccount: Decodable { let accountUuid: String? } let oauthAccount: OAuthAccount? } static func activeClaudeAccountUuid() -> String? { #if DEBUG if case let .value(uuid) = self.activeClaudeAccountUuidOverrideForTesting { return uuid } #endif // `~/.claude.json` is a SIBLING of `.claude/`, not inside it. Home resolution mirrors // `ClaudeOAuthCredentials.defaultCredentialsURL()`. This intentionally does NOT honor // CLAUDE_CONFIG_DIR: the credential store that yields `historyOwnerIdentifier` is purely // home-relative, so the accountUuid corroboration must resolve against the same home or the // two signals would point at different accounts. let url = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".claude.json") guard let data = try? Data(contentsOf: url), let decoded = try? JSONDecoder().decode(ClaudeConfigAccount.self, from: data), let uuid = decoded.oauthAccount?.accountUuid?.trimmingCharacters(in: .whitespacesAndNewlines), !uuid.isEmpty else { return nil } return uuid } }