Files
steipete--codexbar/Sources/CodexBar/UsageStore+PlanUtilization.swift
T
2026-07-13 12:22:33 +08:00

1713 lines
73 KiB
Swift

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<Int>
{
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..<upperBound
}
private nonisolated static func canonicalPlanUtilizationHourEntries(
existingHourEntries: [PlanUtilizationHistoryEntry],
incomingEntry: PlanUtilizationHistoryEntry) -> [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<T>(
_ 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<Date>?
{
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<Date>) -> 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
}
}