diff --git a/Resources/Info.plist b/Resources/Info.plist
index bc2b3c6..4adf947 100644
--- a/Resources/Info.plist
+++ b/Resources/Info.plist
@@ -13,9 +13,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.2.5
+ 0.2.6
CFBundleVersion
- 6
+ 7
LSApplicationCategoryType
public.app-category.developer-tools
LSMinimumSystemVersion
diff --git a/Sources/CodexLimits/AnalyticsWorkspace.swift b/Sources/CodexLimits/AnalyticsWorkspace.swift
index c4e6400..db4302e 100644
--- a/Sources/CodexLimits/AnalyticsWorkspace.swift
+++ b/Sources/CodexLimits/AnalyticsWorkspace.swift
@@ -50,7 +50,7 @@ enum AnalyticsTimeRange: String, CaseIterable, Codable, Identifiable, Sendable {
func interval(
within bounds: DateInterval,
- endingAt proposedEnd: Date
+ now: Date
) -> DateInterval {
let duration: TimeInterval
switch self {
@@ -65,63 +65,138 @@ enum AnalyticsTimeRange: String, CaseIterable, Codable, Identifiable, Sendable {
case .twelveWeeks:
duration = 84 * 86_400
}
- let end = min(max(proposedEnd, bounds.start), bounds.end)
+ let end = max(now, bounds.start)
return DateInterval(
- start: max(bounds.start, end.addingTimeInterval(-duration)),
+ start: end.addingTimeInterval(-duration),
end: end
)
}
}
-struct AccountTokenActivityRange: Equatable, Sendable {
- let days: [TokenDay]
- let completeDayCount: Int
- let completeTokens: Int64?
-
- init(days: [TokenDay], interval: DateInterval) {
- let day: TimeInterval = 86_400
- self.days = days
- .filter {
- $0.date >= interval.start
- && $0.date.addingTimeInterval(day) <= interval.end
- }
- .sorted { $0.date < $1.date }
-
- var calendar = Calendar(identifier: .gregorian)
- calendar.timeZone = TimeZone(secondsFromGMT: 0)
- ?? calendar.timeZone
- let startOfDay = calendar.startOfDay(for: interval.start)
- var expected = startOfDay < interval.start
- ? startOfDay.addingTimeInterval(day)
- : startOfDay
- var expectedDates: [Date] = []
- while expected.addingTimeInterval(day) <= interval.end {
- expectedDates.append(expected)
- expected = expected.addingTimeInterval(day)
- }
- completeDayCount = expectedDates.count
+func accountTokenInterval(
+ at date: Date,
+ in intervals: [AccountTokenActivityInterval],
+ within range: DateInterval
+) -> AccountTokenActivityInterval? {
+ intervals.first {
+ $0.start >= range.start
+ && $0.end <= range.end
+ && $0.start <= date
+ && date <= $0.end
+ }
+}
+
+func steppedAccountTokenInterval(
+ in intervals: [AccountTokenActivityInterval],
+ from selected: AccountTokenActivityInterval?,
+ by offset: Int
+) -> AccountTokenActivityInterval? {
+ let ordered = intervals.sorted {
+ $0.start == $1.start ? $0.end < $1.end : $0.start < $1.start
+ }
+ guard !ordered.isEmpty else { return nil }
+ guard let selected,
+ let index = ordered.firstIndex(of: selected) else {
+ return offset < 0 ? ordered.last : ordered.first
+ }
+ return ordered[min(max(index + offset, 0), ordered.count - 1)]
+}
+
+func retainedAccountTokenInterval(
+ _ selected: AccountTokenActivityInterval?,
+ in intervals: [AccountTokenActivityInterval],
+ range: DateInterval
+) -> AccountTokenActivityInterval? {
+ guard let selected,
+ selected.start >= range.start,
+ selected.end <= range.end else { return nil }
+ return intervals.first { $0 == selected }
+}
+
+struct AccountTokenActivityDisplayInterval: Equatable, Hashable, Identifiable,
+ Sendable {
+ let sourceIntervals: [AccountTokenActivityInterval]
+
+ init(sourceIntervals: [AccountTokenActivityInterval]) {
+ precondition(!sourceIntervals.isEmpty)
+ self.sourceIntervals = sourceIntervals
+ }
- guard !expectedDates.isEmpty else {
- completeTokens = nil
- return
+ var id: Self { self }
+ var start: Date { sourceIntervals[0].start }
+ var end: Date { sourceIntervals[sourceIntervals.count - 1].end }
+ var tokenDelta: Int64 {
+ sourceIntervals.reduce(0) { $0 + $1.tokenDelta }
+ }
+ var method: AccountTokenActivityMethod { sourceIntervals[0].method }
+ var isAggregated: Bool { sourceIntervals.count > 1 }
+}
+
+func displayedAccountTokenIntervals(
+ _ intervals: [AccountTokenActivityInterval],
+ breaks: [AccountTokenActivityBreak],
+ maximumMarks: Int
+) -> [AccountTokenActivityDisplayInterval] {
+ let ordered = intervals.sorted {
+ $0.start == $1.start ? $0.end < $1.end : $0.start < $1.start
+ }
+ guard !ordered.isEmpty else { return [] }
+ let groupLimit = max(
+ Int(ceil(Double(ordered.count) / Double(max(maximumMarks, 1)))),
+ 1
+ )
+ var result: [AccountTokenActivityDisplayInterval] = []
+ var group = [ordered[0]]
+ for interval in ordered.dropFirst() {
+ let previous = group[group.count - 1]
+ let hasBreak = breaks.contains {
+ $0.timestamp == previous.end
+ && $0.timestamp == interval.start
}
- var total: Int64 = 0
- for date in expectedDates {
- guard let bucket = self.days.first(where: { $0.date == date }),
- bucket.completeness == .complete,
- bucket.tokens >= 0 else {
- completeTokens = nil
- return
- }
- let result = total.addingReportingOverflow(bucket.tokens)
- guard !result.overflow else {
- completeTokens = nil
- return
- }
- total = result.partialValue
+ let compatible = previous.end == interval.start
+ && previous.method == interval.method
+ && previous.accountPartitionID == interval.accountPartitionID
+ && previous.limitID == interval.limitID
+ && previous.allowanceReset == interval.allowanceReset
+ && (previous.tokenDelta == 0) == (interval.tokenDelta == 0)
+ && !hasBreak
+ if compatible, group.count < groupLimit {
+ group.append(interval)
+ } else {
+ result.append(AccountTokenActivityDisplayInterval(
+ sourceIntervals: group
+ ))
+ group = [interval]
}
- completeTokens = total
}
+ result.append(AccountTokenActivityDisplayInterval(sourceIntervals: group))
+ return result
+}
+
+func accountTokenDisplayInterval(
+ at date: Date,
+ in intervals: [AccountTokenActivityDisplayInterval],
+ within range: DateInterval
+) -> AccountTokenActivityDisplayInterval? {
+ intervals.first {
+ $0.start >= range.start
+ && $0.end <= range.end
+ && $0.start <= date
+ && date <= $0.end
+ }
+}
+
+func steppedAccountTokenDisplayInterval(
+ in intervals: [AccountTokenActivityDisplayInterval],
+ from selected: AccountTokenActivityDisplayInterval?,
+ by offset: Int
+) -> AccountTokenActivityDisplayInterval? {
+ guard !intervals.isEmpty else { return nil }
+ guard let selected,
+ let index = intervals.firstIndex(of: selected) else {
+ return offset < 0 ? intervals.last : intervals.first
+ }
+ return intervals[min(max(index + offset, 0), intervals.count - 1)]
}
struct WorkspaceFilters: Codable, Equatable, Sendable {
@@ -312,7 +387,7 @@ final class AnalyticsWorkspaceStore: ObservableObject {
func effectiveRange(
within bounds: DateInterval,
- endingAt latestObserved: Date
+ now: Date
) -> DateInterval {
if state.timeRange == .selected,
let visibleRange = state.visibleRange,
@@ -321,7 +396,7 @@ final class AnalyticsWorkspaceStore: ObservableObject {
}
return state.timeRange.interval(
within: bounds,
- endingAt: latestObserved
+ now: now
)
}
diff --git a/Sources/CodexLimits/CodexAssistedInsights.swift b/Sources/CodexLimits/CodexAssistedInsights.swift
index 8d09a2a..0b7c4a6 100644
--- a/Sources/CodexLimits/CodexAssistedInsights.swift
+++ b/Sources/CodexLimits/CodexAssistedInsights.swift
@@ -193,7 +193,8 @@ struct CodexMetadataAnalysisPayload: Codable, Equatable, Sendable {
) -> CodexMetadataAnalysisPayload {
let deterministicInput = DeterministicInsightInput(
reader: reader,
- exploration: exploration
+ exploration: exploration,
+ now: now
)
let selectedUsage = deterministicInput.usagePerToken
let range = deterministicInput.selectedRange ?? reader.interval.map {
diff --git a/Sources/CodexLimits/DeterministicInsights.swift b/Sources/CodexLimits/DeterministicInsights.swift
index 92ad282..20f593b 100644
--- a/Sources/CodexLimits/DeterministicInsights.swift
+++ b/Sources/CodexLimits/DeterministicInsights.swift
@@ -110,7 +110,8 @@ struct DeterministicInsightInput: Equatable, Sendable {
init(
reader: UsageReaderSnapshot,
- exploration: AnalyticsExplorationState
+ exploration: AnalyticsExplorationState,
+ now: Date = Date()
) {
sourceState = reader.sourceState
freshness = reader.freshness
@@ -133,7 +134,7 @@ struct DeterministicInsightInput: Equatable, Sendable {
selectedRange = Self.effectiveRange(
usagePerToken: selectedUsage,
observedInterval: reader.interval,
- fetchedAt: reader.fetchedAt,
+ now: now,
exploration: exploration
)
filters = exploration.filters
@@ -166,7 +167,7 @@ struct DeterministicInsightInput: Equatable, Sendable {
static func effectiveRange(
usagePerToken: UsagePerTokenSnapshot,
observedInterval: UsageObservedInterval?,
- fetchedAt: Date?,
+ now: Date,
exploration: AnalyticsExplorationState
) -> DateInterval? {
let evidence = usagePerToken.history
@@ -183,7 +184,7 @@ struct DeterministicInsightInput: Equatable, Sendable {
? exploration.visibleRange
: nil
}
- let bounds = DateInterval(start: start, end: end)
+ let bounds = DateInterval(start: start, end: max(end, now))
if exploration.timeRange == .currentWindow {
return usagePerToken.current?.interval ?? observedRange
}
@@ -195,7 +196,7 @@ struct DeterministicInsightInput: Equatable, Sendable {
}
return exploration.timeRange.interval(
within: bounds,
- endingAt: fetchedAt ?? evidenceEnd ?? end
+ now: now
)
}
}
@@ -207,12 +208,14 @@ enum DeterministicInsightEngine {
static func evaluate(
reader: UsageReaderSnapshot,
exploration: AnalyticsExplorationState,
+ now: Date = Date(),
dispositions: [String: InsightDisposition]
) -> DeterministicInsightsSnapshot {
evaluate(
DeterministicInsightInput(
reader: reader,
- exploration: exploration
+ exploration: exploration,
+ now: now
),
dispositions: dispositions
)
diff --git a/Sources/CodexLimits/ForecastEngine.swift b/Sources/CodexLimits/ForecastEngine.swift
index 7bbc64c..c907f4e 100644
--- a/Sources/CodexLimits/ForecastEngine.swift
+++ b/Sources/CodexLimits/ForecastEngine.swift
@@ -1,5 +1,10 @@
import Foundation
+struct RecentAccountMovement: Equatable, Sendable {
+ let latest: UsageSample
+ let percentPerDay: Double
+}
+
enum ForecastEngine {
static func evaluate(
window: UsageWindow,
@@ -7,9 +12,16 @@ enum ForecastEngine {
tokenHistory: [TokenDay],
safetyBuffer: Double,
now: Date,
- previousStatus: PaceStatus?
+ previousStatus: PaceStatus?,
+ recentMovement: RecentAccountMovement? = nil
) -> Forecast {
- let daysLeft = max(window.resetsAt.timeIntervalSince(now) / 86_400, 0)
+ let latestDate = recentMovement?.latest.observedAt ?? now
+ let latestRemaining = recentMovement?.latest.remainingPercent
+ ?? window.remainingPercent
+ let daysLeft = max(
+ window.resetsAt.timeIntervalSince(latestDate) / 86_400,
+ 0
+ )
let currentSamples = samples
.filter { $0.resetsAt == window.resetsAt && $0.observedAt <= now }
.sorted { $0.observedAt < $1.observedAt }
@@ -27,9 +39,10 @@ enum ForecastEngine {
recentRate = windowRate
}
- let currentRate = currentIntervalSamples.count > 1
- ? 0.7 * recentRate + 0.3 * windowRate
- : windowRate
+ let currentRate = recentMovement?.percentPerDay
+ ?? (currentIntervalSamples.count > 1
+ ? 0.7 * recentRate + 0.3 * windowRate
+ : windowRate)
let historicalRates = comparableHistoricalRates(
samples: samples,
excluding: window.resetsAt
@@ -49,11 +62,17 @@ enum ForecastEngine {
historicalRate = median(Array(historicalRates.prefix(4)))
historicalReferenceSource = .accountHistory
}
- let expectedRate = 0.75 * currentRate + 0.25 * historicalRate
- let safetyRate = max(currentRate, historicalRate) * 1.2
- let expected = max(window.remainingPercent - expectedRate * daysLeft, 0)
- let safety = max(window.remainingPercent - safetyRate * daysLeft, 0)
- let historical = max(window.remainingPercent - historicalRate * daysLeft, 0)
+ let expectedRate = recentMovement == nil
+ ? 0.75 * currentRate + 0.25 * historicalRate
+ : currentRate
+ let safetyRate = (
+ recentMovement == nil
+ ? max(currentRate, historicalRate)
+ : currentRate
+ ) * 1.2
+ let expected = max(latestRemaining - expectedRate * daysLeft, 0)
+ let safety = max(latestRemaining - safetyRate * daysLeft, 0)
+ let historical = max(latestRemaining - historicalRate * daysLeft, 0)
let historicalReference = historicalReferenceSource.map {
UsageForecastReference(
source: $0,
@@ -62,7 +81,7 @@ enum ForecastEngine {
)
}
let allowanceRate = daysLeft > 0
- ? max(window.remainingPercent - safetyBuffer, 0) / daysLeft
+ ? max(latestRemaining - safetyBuffer, 0) / daysLeft
: 0
let recommended = historicalReferenceSource == .accountHistory
? min(allowanceRate, historicalRate * 1.2)
diff --git a/Sources/CodexLimits/MenuContentView.swift b/Sources/CodexLimits/MenuContentView.swift
index c364c28..3448c13 100644
--- a/Sources/CodexLimits/MenuContentView.swift
+++ b/Sources/CodexLimits/MenuContentView.swift
@@ -100,28 +100,31 @@ struct MenuContentView: View {
Task { await monitor.refresh() }
}
) {
- AnalyticsWorkspaceBody(
- reader: monitor.readerSnapshot,
- store: workspace,
- assistedInsights: assistedInsights,
- analyticsPreferencesChanged: {
- monitor.analyticsPreferencesDidChange(
- exploration: workspace.state,
- dispositions: workspace.insightDispositions
- )
- },
- resetReminderState: monitor.resetReminderState,
- setResetReminderEnabled: { isEnabled in
- Task {
- await monitor.setResetReminderEnabled(isEnabled)
- }
- },
- setResetReminderLeadTime: { leadTime in
- Task {
- await monitor.setResetReminderLeadTime(leadTime)
+ TimelineView(.periodic(from: .now, by: 60)) { context in
+ AnalyticsWorkspaceBody(
+ reader: monitor.readerSnapshot,
+ store: workspace,
+ assistedInsights: assistedInsights,
+ now: context.date,
+ analyticsPreferencesChanged: {
+ monitor.analyticsPreferencesDidChange(
+ exploration: workspace.state,
+ dispositions: workspace.insightDispositions
+ )
+ },
+ resetReminderState: monitor.resetReminderState,
+ setResetReminderEnabled: { isEnabled in
+ Task {
+ await monitor.setResetReminderEnabled(isEnabled)
+ }
+ },
+ setResetReminderLeadTime: { leadTime in
+ Task {
+ await monitor.setResetReminderLeadTime(leadTime)
+ }
}
- }
- )
+ )
+ }
}
}
@@ -215,6 +218,7 @@ struct AnalyticsWorkspaceBody: View {
let reader: UsageReaderSnapshot
@ObservedObject var store: AnalyticsWorkspaceStore
@ObservedObject var assistedInsights: CodexAssistedInsightStore
+ let now: Date
let analyticsPreferencesChanged: () -> Void
let resetReminderState: ResetReminderState
let setResetReminderEnabled: (Bool) -> Void
@@ -224,6 +228,7 @@ struct AnalyticsWorkspaceBody: View {
reader: UsageReaderSnapshot,
store: AnalyticsWorkspaceStore,
assistedInsights: CodexAssistedInsightStore,
+ now: Date = Date(),
analyticsPreferencesChanged: @escaping () -> Void = {},
resetReminderState: ResetReminderState = ResetReminderState(
isEnabled: false,
@@ -237,6 +242,7 @@ struct AnalyticsWorkspaceBody: View {
self.reader = reader
self.store = store
self.assistedInsights = assistedInsights
+ self.now = now
self.analyticsPreferencesChanged = analyticsPreferencesChanged
self.resetReminderState = resetReminderState
self.setResetReminderEnabled = setResetReminderEnabled
@@ -248,7 +254,7 @@ struct AnalyticsWorkspaceBody: View {
Group {
switch store.state.section {
case .graphs:
- GraphsWorkspace(reader: reader, store: store)
+ GraphsWorkspace(reader: reader, store: store, now: now)
case .facts:
FactsWorkspace(
reader: reader,
@@ -271,6 +277,16 @@ struct AnalyticsWorkspaceBody: View {
.onChange(of: store.insightDispositions) { _, _ in
analyticsPreferencesChanged()
}
+ .onChange(of: now) { _, _ in
+ guard store.state.section == .graphs,
+ store.state.graph == .tokenActivity else { return }
+ switch store.state.timeRange {
+ case .oneDay, .threeDays, .fourWeeks, .twelveWeeks:
+ analyticsPreferencesChanged()
+ case .currentWindow, .selected:
+ break
+ }
+ }
}
}
@@ -296,7 +312,7 @@ private struct WorkspaceHeader: View {
Text("% remaining")
.foregroundStyle(.secondary)
} else {
- Text("Weekly usage unavailable")
+ Text(reader.evidence.reason ?? "Weekly usage unavailable")
.font(.headline)
}
@@ -344,9 +360,6 @@ private struct WorkspaceHeader: View {
.font(.callout)
.foregroundStyle(.secondary)
.lineLimit(isCompact ? 2 : 1)
- Text(reader.evidenceText)
- .font(.caption)
- .foregroundStyle(.tertiary)
}
}
}
@@ -448,6 +461,7 @@ private struct HeaderFact: View {
private struct GraphsWorkspace: View {
let reader: UsageReaderSnapshot
@ObservedObject var store: AnalyticsWorkspaceStore
+ let now: Date
var body: some View {
VStack(alignment: .leading, spacing: 16) {
@@ -457,7 +471,16 @@ private struct GraphsWorkspace: View {
case .usageRemaining:
usageRemaining
case .tokenActivity:
- TokenActivityWorkspace(reader: reader, store: store)
+ if store.state.timeRange != .currentWindow
+ || reader.weeklyUsageRemaining != nil {
+ TokenActivityWorkspace(
+ reader: reader,
+ store: store,
+ now: now
+ )
+ } else {
+ unavailableCurrentWindow
+ }
case .usagePerToken:
UsagePerTokenWorkspace(
sourceSnapshot: reader.usagePerToken,
@@ -536,13 +559,15 @@ private struct GraphsWorkspace: View {
@ViewBuilder
private var usageRemaining: some View {
- if let weekly = reader.weeklyUsageRemaining {
+ if let weekly = reader.weeklyUsageRemaining
+ ?? historicalWeeklyUsageRemaining {
VStack(alignment: .leading, spacing: 14) {
UsageRemainingChart(
window: weekly.window,
chart: reader.chart,
evidence: reader.evidence,
- store: store
+ store: store,
+ now: now
)
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
@@ -564,7 +589,7 @@ private struct GraphsWorkspace: View {
GridRow {
Text("Runway")
.foregroundStyle(.secondary)
- Text(reader.guidance?.runway.text ?? "Not enough data")
+ Text(reader.runwayText)
}
if let gap = reader.guidance?.runway.gapText {
GridRow {
@@ -584,12 +609,20 @@ private struct GraphsWorkspace: View {
.font(.callout)
}
} else {
- UnavailableGraph(
- title: "Weekly usage unavailable",
- message: "Try refreshing to check again."
- )
+ unavailableCurrentWindow
}
}
+
+ private var historicalWeeklyUsageRemaining: LimitReading? {
+ store.state.timeRange == .currentWindow ? nil : reader.account?.mainLimit
+ }
+
+ private var unavailableCurrentWindow: some View {
+ UnavailableGraph(
+ title: reader.evidence.reason ?? "Weekly usage unavailable",
+ message: "Try refreshing to check again."
+ )
+ }
}
struct UsagePerTokenWorkspace: View {
@@ -634,7 +667,7 @@ struct UsagePerTokenWorkspace: View {
}
return store.effectiveRange(
within: bounds,
- endingAt: snapshot.current?.interval.end ?? bounds.end
+ now: snapshot.current?.interval.end ?? bounds.end
)
}
@@ -1165,7 +1198,7 @@ private struct ConcurrencyWorkspace: View {
private var visibleRange: DateInterval {
store.effectiveRange(
within: bounds,
- endingAt: min(
+ now: min(
reader.localTokenActivity.observedAt ?? bounds.end,
bounds.end
)
@@ -1514,15 +1547,64 @@ private struct ConcurrencyWorkspace: View {
}
}
+func accountTokenIntervalText(
+ _ interval: DateInterval,
+ timeZone: TimeZone = .autoupdatingCurrent,
+ locale: Locale = .autoupdatingCurrent
+) -> String {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .medium
+ formatter.timeStyle = .short
+ formatter.timeZone = timeZone
+ formatter.locale = locale
+ return "\(formatter.string(from: interval.start))–\(formatter.string(from: interval.end))"
+}
+
+func accountTokenIntervalAccessibilityValue(
+ _ interval: AccountTokenActivityInterval,
+ timeZone: TimeZone = .autoupdatingCurrent,
+ locale: Locale = .autoupdatingCurrent
+) -> String {
+ let dates = accountTokenIntervalText(
+ DateInterval(start: interval.start, end: interval.end),
+ timeZone: timeZone,
+ locale: locale
+ )
+ return "\(interval.tokenDelta) account tokens. Account. \(dates). \(interval.method.displayName)."
+}
+
+func accountTokenDisplayIntervalAccessibilityValue(
+ _ interval: AccountTokenActivityDisplayInterval,
+ timeZone: TimeZone = .autoupdatingCurrent,
+ locale: Locale = .autoupdatingCurrent
+) -> String {
+ let dates = accountTokenIntervalText(
+ DateInterval(start: interval.start, end: interval.end),
+ timeZone: timeZone,
+ locale: locale
+ )
+ let aggregation = interval.isAggregated
+ ? " Combined from \(interval.sourceIntervals.count) observed periods."
+ : ""
+ let zero = interval.tokenDelta == 0
+ ? " Observed zero-token interval."
+ : ""
+ return "\(interval.tokenDelta) account tokens. Account. \(dates). \(interval.method.displayName).\(aggregation)\(zero)"
+}
+
private struct TokenActivityWorkspace: View {
let reader: UsageReaderSnapshot
@ObservedObject var store: AnalyticsWorkspaceStore
+ let now: Date
- @State private var selectedDay: TokenDay?
- private let day: TimeInterval = 86_400
+ @State private var selectedInterval: AccountTokenActivityDisplayInterval?
- private var accountDays: [TokenDay] {
- (reader.account?.tokenHistory ?? []).sorted { $0.date < $1.date }
+ private var displayIntervals: [AccountTokenActivityDisplayInterval] {
+ displayedAccountTokenIntervals(
+ reader.accountTokenActivity.intervals,
+ breaks: reader.accountTokenActivity.breaks,
+ maximumMarks: 120
+ )
}
private var currentWindowBounds: DateInterval? {
@@ -1534,56 +1616,49 @@ private struct TokenActivityWorkspace: View {
}
}
- private var bounds: DateInterval {
- let fallback = reader.fetchedAt ?? Date()
- let first = accountDays.first?.date
- ?? currentWindowBounds?.start
- ?? fallback.addingTimeInterval(-day)
- let last = accountDays.last?.date.addingTimeInterval(day)
- ?? currentWindowBounds?.end
- ?? fallback
+ private var zoomBounds: DateInterval {
+ let tokenDates = (reader.account?.tokenHistory ?? []).flatMap {
+ [$0.date, $0.date.addingTimeInterval(86_400)]
+ }
+ let dates = reader.chart.allObserved.map(\.date)
+ + tokenDates
+ + [visibleRange.start, visibleRange.end, now]
+ + [currentWindowBounds?.start, currentWindowBounds?.end].compactMap { $0 }
return DateInterval(
- start: min(first, currentWindowBounds?.start ?? first),
- end: max(last, currentWindowBounds?.end ?? last)
+ start: dates.min() ?? visibleRange.start,
+ end: dates.max() ?? visibleRange.end
)
}
private var visibleRange: DateInterval {
+ if let evaluatedRange = reader.accountTokenActivity.range {
+ return evaluatedRange
+ }
if store.state.timeRange == .currentWindow,
let currentWindowBounds {
return currentWindowBounds
}
- return store.effectiveRange(
- within: bounds,
- endingAt: min(
- reader.fetchedAt ?? bounds.end,
- bounds.end
- )
- )
- }
-
- private var accountRange: AccountTokenActivityRange {
- AccountTokenActivityRange(
- days: accountDays,
- interval: visibleRange
+ if store.state.timeRange == .selected,
+ let selected = store.state.visibleRange {
+ return selected
+ }
+ return store.state.timeRange.interval(
+ within: DateInterval(start: now, end: now),
+ now: now
)
}
var body: some View {
- content(accountRange)
- }
-
- private func content(_ range: AccountTokenActivityRange) -> some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
Text("Token activity")
.font(.title3.weight(.semibold))
- Text("Daily token totals from your Codex account.")
+ Text("Observed token activity from your Codex account.")
.font(.callout)
.foregroundStyle(.secondary)
}
- accountCard(range)
+ accountCard
VStack(alignment: .leading, spacing: 10) {
ViewThatFits(in: .horizontal) {
@@ -1598,151 +1673,144 @@ private struct TokenActivityWorkspace: View {
}
}
- if range.days.isEmpty {
+ zoomControls
+
+ if reader.accountTokenActivity.intervals.isEmpty {
WorkspaceMessage(
icon: "chart.xyaxis.line",
- title: "No complete daily totals",
- message: store.state.timeRange == .currentWindow
- ? "Choose 4 weeks to see account history."
- : "Codex did not return a complete day for this range."
+ title: reader.accountTokenActivity.reason
+ ?? "No account readings in this range",
+ message: "Missing time remains empty."
) {
EmptyView()
}
.frame(minHeight: 170)
} else {
- accountChart(range)
+ accountIntervalChart(
+ displayIntervals
+ )
}
- selectedPointDetail(range)
+ intervalSelectionDetail
+ evidenceDetails
}
}
+ .onChange(of: reader.accountTokenActivity.intervals) { _, _ in
+ selectedInterval = displayIntervals.first { $0 == selectedInterval }
+ }
.onChange(of: visibleRange) { _, range in
- if let selectedDay,
- selectedDay.date >= range.end
- || selectedDay.date.addingTimeInterval(day) <= range.start {
- self.selectedDay = nil
+ guard let selectedInterval,
+ selectedInterval.start >= range.start,
+ selectedInterval.end <= range.end else {
+ self.selectedInterval = nil
+ return
+ }
+ self.selectedInterval = displayIntervals.first {
+ $0 == selectedInterval
}
}
}
private var chartSourceLabel: some View {
ChartLegendItem(
- label: "Daily totals · Account",
+ label: "Observed intervals · Account",
color: .blue
)
}
private var chartIntervalLabel: some View {
- Text(intervalText(visibleRange))
+ Text(accountTokenIntervalText(visibleRange))
.font(.caption)
.foregroundStyle(.tertiary)
}
- private func accountCard(
- _ range: AccountTokenActivityRange
- ) -> some View {
+ private var zoomControls: some View {
+ HStack(spacing: 10) {
+ Button {
+ zoom(by: 1.5)
+ } label: {
+ Label("Zoom in", systemImage: "plus.magnifyingglass")
+ }
+ .help("Show a shorter range")
+ Button {
+ zoom(by: 1 / 1.5)
+ } label: {
+ Label("Zoom out", systemImage: "minus.magnifyingglass")
+ }
+ .help("Show a longer range")
+ if store.state.timeRange == .selected {
+ Button("Reset range") {
+ store.resetVisibleRange()
+ }
+ }
+ }
+ .buttonStyle(.borderless)
+ .font(.caption)
+ }
+
+ private var accountCard: some View {
TokenSourceCard(
title: "Account",
- source: store.state.timeRange == .currentWindow
- ? accountSource
- : "Codex daily token totals",
- value: summaryTokens(in: range).map(compactTokenCount)
+ source: reader.accountTokenActivity.sourceDescription,
+ value: reader.accountTokenActivity.tokens.map(compactTokenCount)
?? "Not available",
- detail: summaryDetail(in: range),
- coverage: summaryCoverage(in: range),
+ detail: summaryDetail,
freshness: reader.fetchedAt,
freshnessLabel: "Updated",
color: .blue
)
}
- private var accountSource: String {
- switch reader.accountTokenActivity.method {
- case .lifetimeDelta:
- "Codex account summary"
- case .dailyBuckets:
- "Codex daily token totals"
- case nil:
- "Codex account"
- }
- }
-
- private func summaryTokens(
- in range: AccountTokenActivityRange
- ) -> Int64? {
- if store.state.timeRange == .currentWindow {
- return reader.accountTokenActivity.tokens
- }
- return range.completeTokens
- }
-
- private func summaryDetail(
- in range: AccountTokenActivityRange
- ) -> String {
- guard store.state.timeRange == .currentWindow else {
- if range.completeDayCount == 0 {
- return "No full days in this range"
- }
- return range.completeTokens == nil
- ? "Codex returned only part of this range"
- : "Sum of \(range.completeDayCount) complete days"
- }
- switch reader.accountTokenActivity.method {
- case .lifetimeDelta:
- return "Change between two account readings"
- case .dailyBuckets:
- return reader.accountTokenActivity.state == .partial
- ? "Sum of complete days"
- : "Complete daily totals"
- case nil:
+ private var summaryDetail: String {
+ guard let observed = reader.accountTokenActivity.interval else {
return reader.accountTokenActivity.reason
?? "Account token activity is unavailable"
}
+ let qualifier = store.state.timeRange == .currentWindow
+ ? "Activity so far · "
+ : ""
+ return "\(qualifier)Observed from \(accountTokenIntervalText(observed))"
}
- private func summaryCoverage(
- in range: AccountTokenActivityRange
- ) -> String {
- guard store.state.timeRange == .currentWindow else {
- return range.completeTokens == nil
- ? "Unavailable"
- : "Complete days"
- }
- switch reader.accountTokenActivity.state {
- case .exact: return "Complete"
- case .partial: return "Partial"
- case .unavailable: return "Unavailable"
- }
- }
-
- private func renderedDays(
- _ range: AccountTokenActivityRange
- ) -> [TokenDay] {
- downsampledForDisplay(range.days)
- }
-
- private func accountChart(
- _ range: AccountTokenActivityRange
+ private func accountIntervalChart(
+ _ intervals: [AccountTokenActivityDisplayInterval]
) -> some View {
Chart {
- ForEach(renderedDays(range), id: \.date) { tokenDay in
- BarMark(
- x: .value("Day", tokenDay.date, unit: .day),
- y: .value("Account tokens", tokenDay.tokens)
- )
- .foregroundStyle(Color.blue)
+ ForEach(intervals) { interval in
+ if interval.tokenDelta == 0 {
+ PointMark(
+ x: .value("Observed interval", midpoint(of: interval)),
+ y: .value("Account tokens", 0)
+ )
+ .foregroundStyle(Color.blue)
+ .symbol(.diamond)
+ .symbolSize(55)
+ .accessibilityLabel("Account token interval")
+ .accessibilityValue(
+ accountTokenDisplayIntervalAccessibilityValue(interval)
+ )
+ } else {
+ RectangleMark(
+ xStart: .value("Start", interval.start),
+ xEnd: .value("End", interval.end),
+ yStart: .value("Baseline", 0),
+ yEnd: .value("Account tokens", interval.tokenDelta)
+ )
+ .foregroundStyle(Color.blue)
+ .accessibilityLabel("Account token interval")
+ .accessibilityValue(
+ accountTokenDisplayIntervalAccessibilityValue(interval)
+ )
+ }
}
- if let selectedDay {
- RuleMark(x: .value("Selected time", selectedDay.date))
- .foregroundStyle(Color.primary.opacity(0.45))
+ if let selectedInterval {
+ RuleMark(x: .value("Selected start", selectedInterval.start))
+ .foregroundStyle(Color.primary.opacity(0.5))
+ .lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3]))
+ RuleMark(x: .value("Selected end", selectedInterval.end))
+ .foregroundStyle(Color.primary.opacity(0.5))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3]))
- PointMark(
- x: .value("Selected day", selectedDay.date),
- y: .value("Account tokens", selectedDay.tokens)
- )
- .foregroundStyle(Color.blue)
- .symbolSize(52)
}
}
.chartXScale(domain: visibleRange.start ... visibleRange.end)
@@ -1763,89 +1831,78 @@ private struct TokenActivityWorkspace: View {
Rectangle()
.fill(.clear)
.contentShape(Rectangle())
- .onContinuousHover { phase in
- switch phase {
- case let .active(location):
- selectNearestPoint(
- at: location,
- proxy: proxy,
- geometry: geometry,
- days: range.days
- )
- case .ended:
- selectedDay = nil
- }
- }
+ .gesture(
+ DragGesture(minimumDistance: 0)
+ .onEnded { value in
+ selectInterval(
+ at: value.location,
+ proxy: proxy,
+ geometry: geometry
+ )
+ }
+ )
}
}
.frame(height: 180)
- .accessibilityElement(children: .ignore)
+ .accessibilityElement(children: .contain)
.accessibilityLabel("Account token activity")
.accessibilityValue(
- selectedDay.map {
- "\(compactTokenCount($0.tokens)) account tokens, \($0.date.formatted(date: .abbreviated, time: .omitted))"
- } ?? "Daily account token totals are shown."
+ store.state.timeRange == .currentWindow
+ ? reader.accountTokenActivity.currentWindowAccessibilityValue
+ : reader.accountTokenActivity.accessibilityValue
)
- .accessibilityHint(
- "Use Previous point and Next point for exact values."
+ }
+
+ private func midpoint(
+ of interval: AccountTokenActivityDisplayInterval
+ ) -> Date {
+ interval.start.addingTimeInterval(
+ interval.end.timeIntervalSince(interval.start) / 2
)
}
- @ViewBuilder
- private func selectedPointDetail(
- _ range: AccountTokenActivityRange
- ) -> some View {
- VStack(alignment: .leading, spacing: 7) {
- HStack(spacing: 10) {
- if let selectedDay {
- ViewThatFits(in: .horizontal) {
- HStack(spacing: 10) {
- Text("Daily account total")
- .fontWeight(.semibold)
- Text(compactTokenCount(selectedDay.tokens))
- .monospacedDigit()
- Text(
- selectedDay.date.formatted(
- date: .abbreviated,
- time: .omitted
- )
- )
- .foregroundStyle(.secondary)
- }
- VStack(alignment: .leading, spacing: 3) {
- HStack(spacing: 8) {
- Text("Daily account total")
- .fontWeight(.semibold)
- Text(compactTokenCount(selectedDay.tokens))
- .monospacedDigit()
- }
- Text(
- selectedDay.date.formatted(
- date: .abbreviated,
- time: .omitted
- )
- )
- .foregroundStyle(.secondary)
- }
- }
- } else {
- Text("Choose a point for exact details.")
+ private var intervalSelectionDetail: some View {
+ HStack(spacing: 10) {
+ if let selectedInterval {
+ VStack(alignment: .leading, spacing: 3) {
+ Text("Selected interval")
+ .fontWeight(.semibold)
+ Text("\(compactTokenCount(selectedInterval.tokenDelta)) tokens · Account")
+ .monospacedDigit()
+ Text(
+ "\(accountTokenIntervalText(DateInterval(start: selectedInterval.start, end: selectedInterval.end))) · \(selectedInterval.method.displayName)"
+ )
+ .foregroundStyle(.secondary)
+ if selectedInterval.isAggregated {
+ Text(
+ "Combined from \(selectedInterval.sourceIntervals.count) observed periods"
+ )
.foregroundStyle(.secondary)
+ }
}
- Spacer()
- Button {
- moveSelection(in: range.days, by: -1)
- } label: {
- Image(systemName: "chevron.left")
- }
- .accessibilityLabel("Previous point")
- Button {
- moveSelection(in: range.days, by: 1)
- } label: {
- Image(systemName: "chevron.right")
- }
- .accessibilityLabel("Next point")
+ .accessibilityElement(children: .ignore)
+ .accessibilityLabel(
+ accountTokenDisplayIntervalAccessibilityValue(
+ selectedInterval
+ )
+ )
+ } else {
+ Text("Choose an observed interval for exact details.")
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ Button {
+ moveIntervalSelection(by: -1)
+ } label: {
+ Image(systemName: "chevron.left")
+ }
+ .accessibilityLabel("Previous interval")
+ Button {
+ moveIntervalSelection(by: 1)
+ } label: {
+ Image(systemName: "chevron.right")
}
+ .accessibilityLabel("Next interval")
}
.font(.caption)
.padding(10)
@@ -1856,45 +1913,89 @@ private struct TokenActivityWorkspace: View {
)
}
- private func moveSelection(
- in days: [TokenDay],
- by offset: Int
- ) {
- selectedDay = steppedPoint(
- in: days,
- from: selectedDay,
+ private var evidenceDetails: some View {
+ DisclosureGroup("Evidence Details") {
+ VStack(alignment: .leading, spacing: 5) {
+ Text("Source · \(reader.accountTokenActivity.sourceDescription)")
+ if let observed = reader.accountTokenActivity.interval {
+ Text("Observed Interval · \(accountTokenIntervalText(observed))")
+ }
+ if let method = selectedInterval?.method
+ ?? reader.accountTokenActivity.method {
+ Text("Method · \(method.displayName)")
+ }
+ ForEach(evidenceCaveats, id: \.self) { caveat in
+ Text(caveat)
+ }
+ }
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .padding(.top, 5)
+ }
+ .font(.caption)
+ }
+
+ private var evidenceCaveats: [String] {
+ var caveats: [String] = []
+ if reader.accountTokenActivity.state == .partial {
+ caveats.append(
+ "Only complete observed intervals inside the selected range are included."
+ )
+ }
+ if reader.accountTokenActivity.method == .dailyBuckets {
+ caveats.append(
+ "Lifetime intervals were unavailable; complete UTC daily buckets were used."
+ )
+ }
+ if !reader.accountTokenActivity.breaks.isEmpty {
+ caveats.append("Counter breaks remain empty on the chart.")
+ }
+ return caveats
+ }
+
+ private func moveIntervalSelection(by offset: Int) {
+ selectedInterval = steppedAccountTokenDisplayInterval(
+ in: displayIntervals,
+ from: selectedInterval,
by: offset
)
}
- private func selectNearestPoint(
+ private func zoom(by factor: CGFloat) {
+ let anchor = selectedInterval.map {
+ Date(
+ timeIntervalSince1970:
+ ($0.start.timeIntervalSince1970
+ + $0.end.timeIntervalSince1970) / 2
+ )
+ } ?? Date(
+ timeIntervalSince1970:
+ (visibleRange.start.timeIntervalSince1970
+ + visibleRange.end.timeIntervalSince1970) / 2
+ )
+ store.zoom(
+ factor: Double(factor),
+ anchor: anchor,
+ currentRange: visibleRange,
+ within: zoomBounds
+ )
+ }
+
+ private func selectInterval(
at location: CGPoint,
proxy: ChartProxy,
- geometry: GeometryProxy,
- days: [TokenDay]
+ geometry: GeometryProxy
) {
guard let date = chartDate(
at: location,
proxy: proxy,
geometry: geometry
) else { return }
- selectedDay = nearestPoint(
- in: days,
- to: date,
- date: \.date
- )
- }
-
- private func intervalText(_ interval: DateInterval) -> String {
- let start = interval.start.formatted(
- date: .abbreviated,
- time: .shortened
- )
- let end = interval.end.formatted(
- date: .abbreviated,
- time: .shortened
+ selectedInterval = accountTokenDisplayInterval(
+ at: date,
+ in: displayIntervals,
+ within: visibleRange
)
- return "\(start)–\(end)"
}
}
@@ -1903,7 +2004,6 @@ private struct TokenSourceCard: View {
let source: String
let value: String
let detail: String
- let coverage: String
let freshness: Date?
let freshnessLabel: String
let color: Color
@@ -1925,17 +2025,14 @@ private struct TokenSourceCard: View {
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(3)
- HStack(spacing: 12) {
- Text("Coverage \(coverage)")
- if let freshness {
- Text(freshnessLabel + " " + freshness.formatted(
- date: .abbreviated,
- time: .shortened
- ))
- }
+ if let freshness {
+ Text(freshnessLabel + " " + freshness.formatted(
+ date: .abbreviated,
+ time: .shortened
+ ))
+ .font(.caption2)
+ .foregroundStyle(.tertiary)
}
- .font(.caption2)
- .foregroundStyle(.tertiary)
}
.padding(14)
.frame(maxWidth: .infinity, minHeight: 155, alignment: .topLeading)
@@ -1948,7 +2045,7 @@ private struct TokenSourceCard: View {
.stroke(color.opacity(0.16))
}
.help(
- "\(title), \(source). \(value) tokens. Coverage \(coverage). \(detail)"
+ "\(title), \(source). \(value) tokens. \(detail)"
)
}
}
@@ -1961,7 +2058,7 @@ private struct WorkspaceFilterMenu: View {
let bounds = reader.usageReceipts.interval
return store.effectiveRange(
within: bounds,
- endingAt: min(
+ now: min(
reader.localTokenActivity.observedAt ?? bounds.end,
bounds.end
)
@@ -2046,6 +2143,7 @@ private struct UsageRemainingChart: View {
let chart: UsageChartSnapshot
let evidence: UsageEvidence
@ObservedObject var store: AnalyticsWorkspaceStore
+ let now: Date
@State private var selection: UsageChartSelection?
@State private var pendingRange: DateInterval?
@@ -2057,7 +2155,11 @@ private struct UsageRemainingChart: View {
}
private var bounds: DateInterval {
- chart.availableRange(including: currentWindowBounds)
+ let available = chart.availableRange(including: currentWindowBounds)
+ return DateInterval(
+ start: available.start,
+ end: max(available.end, now)
+ )
}
private var visibleRange: DateInterval {
@@ -2066,7 +2168,7 @@ private struct UsageRemainingChart: View {
}
return store.effectiveRange(
within: bounds,
- endingAt: chart.preferredZoomAnchor ?? min(Date(), bounds.end)
+ now: now
)
}
@@ -2933,7 +3035,7 @@ private struct FactsWorkspace: View {
let bounds = reader.usageReceipts.interval
return store.effectiveRange(
within: bounds,
- endingAt: min(
+ now: min(
reader.localTokenActivity.observedAt ?? bounds.end,
bounds.end
)
diff --git a/Sources/CodexLimits/UsageHistory.swift b/Sources/CodexLimits/UsageHistory.swift
index d0b31b4..da35ec5 100644
--- a/Sources/CodexLimits/UsageHistory.swift
+++ b/Sources/CodexLimits/UsageHistory.swift
@@ -983,34 +983,39 @@ actor UsageHistory {
}
private func normalized(_ samples: [UsageSample]) -> [UsageSample] {
- let valid = samples.filter(\.isValid)
- var byIdentity: [UsageSample: UsageSample] = [:]
- for sample in valid {
- guard let existing = byIdentity[sample] else {
- byIdentity[sample] = sample
- continue
- }
- let lifetimeTokens = if let newTokens = sample.lifetimeTokens,
- newTokens > (existing.lifetimeTokens ?? .min) {
- newTokens
- } else {
- existing.lifetimeTokens
+ let valid = samples.filter(\.hasValidObservationMetadata)
+ let normalized = Dictionary(grouping: valid, by: { $0 }).values
+ .flatMap { matchingSamples -> [UsageSample] in
+ let sample = matchingSamples[0]
+ let comparisonBreak = matchingSamples.contains {
+ $0.comparisonBreak
+ }
+ let counters = Set(matchingSamples.compactMap(\.lifetimeTokens))
+ if counters.isEmpty {
+ return [UsageSample(
+ observedAt: sample.observedAt,
+ remainingPercent: sample.remainingPercent,
+ resetsAt: sample.resetsAt,
+ comparisonBreak: comparisonBreak
+ )]
+ }
+ return counters.map {
+ UsageSample(
+ observedAt: sample.observedAt,
+ remainingPercent: sample.remainingPercent,
+ resetsAt: sample.resetsAt,
+ lifetimeTokens: $0,
+ comparisonBreak: comparisonBreak
+ )
+ }
}
- byIdentity[sample] = UsageSample(
- observedAt: existing.observedAt,
- remainingPercent: existing.remainingPercent,
- resetsAt: existing.resetsAt,
- lifetimeTokens: lifetimeTokens,
- comparisonBreak:
- existing.comparisonBreak || sample.comparisonBreak
- )
- }
- return byIdentity.values.sorted {
+ return normalized.sorted {
if $0.observedAt != $1.observedAt { return $0.observedAt < $1.observedAt }
if $0.remainingPercent != $1.remainingPercent {
return $0.remainingPercent > $1.remainingPercent
}
- return $0.resetsAt < $1.resetsAt
+ if $0.resetsAt != $1.resetsAt { return $0.resetsAt < $1.resetsAt }
+ return ($0.lifetimeTokens ?? .min) < ($1.lifetimeTokens ?? .min)
}
}
diff --git a/Sources/CodexLimits/UsageIntelligenceEngine.swift b/Sources/CodexLimits/UsageIntelligenceEngine.swift
index 3535d5b..92c7099 100644
--- a/Sources/CodexLimits/UsageIntelligenceEngine.swift
+++ b/Sources/CodexLimits/UsageIntelligenceEngine.swift
@@ -97,7 +97,7 @@ enum UsageRunway: Equatable, Sendable {
.locale(Locale(identifier: "en_US"))
)
case .throughReset:
- "Through reset"
+ "Limit should last through reset"
}
}
@@ -358,9 +358,45 @@ enum AccountTokenActivityState: String, Equatable, Sendable {
case unavailable
}
-enum AccountTokenActivityMethod: String, Equatable, Sendable {
+enum AccountTokenActivityMethod: String, Equatable, Hashable, Sendable {
case lifetimeDelta
case dailyBuckets
+
+ var displayName: String {
+ switch self {
+ case .lifetimeDelta: "Lifetime counter interval"
+ case .dailyBuckets: "UTC daily bucket"
+ }
+ }
+}
+
+struct AccountTokenActivityInterval: Equatable, Hashable, Identifiable, Sendable {
+ let start: Date
+ let end: Date
+ let tokenDelta: Int64
+ let method: AccountTokenActivityMethod
+ let accountPartitionID: String?
+ let limitID: String?
+ let allowanceReset: Date?
+
+ var id: Self { self }
+ var duration: TimeInterval { end.timeIntervalSince(start) }
+}
+
+enum AccountTokenActivityBreakReason: String, Equatable, Hashable, Sendable {
+ case counterDecrease
+ case accountChange
+ case allowanceWindowChange
+ case conflictingObservation
+ case correction
+ case invalidCounter
+}
+
+struct AccountTokenActivityBreak: Equatable, Hashable, Identifiable, Sendable {
+ let timestamp: Date
+ let reason: AccountTokenActivityBreakReason
+
+ var id: Self { self }
}
struct AccountTokenActivitySnapshot: Equatable, Sendable {
@@ -369,19 +405,74 @@ struct AccountTokenActivitySnapshot: Equatable, Sendable {
let method: AccountTokenActivityMethod?
let interval: DateInterval?
let reason: String?
+ let range: DateInterval?
+ let intervals: [AccountTokenActivityInterval]
+ let breaks: [AccountTokenActivityBreak]
+
+ init(
+ state: AccountTokenActivityState,
+ tokens: Int64?,
+ method: AccountTokenActivityMethod?,
+ interval: DateInterval?,
+ reason: String?,
+ range: DateInterval? = nil,
+ intervals: [AccountTokenActivityInterval] = [],
+ breaks: [AccountTokenActivityBreak] = []
+ ) {
+ self.state = state
+ self.tokens = tokens
+ self.method = method
+ self.interval = interval
+ self.reason = reason
+ self.range = range
+ self.intervals = intervals
+ self.breaks = breaks
+ }
static func unavailable(
_ reason: String,
- interval: DateInterval? = nil
+ interval: DateInterval? = nil,
+ range: DateInterval? = nil,
+ breaks: [AccountTokenActivityBreak] = []
) -> AccountTokenActivitySnapshot {
AccountTokenActivitySnapshot(
state: .unavailable,
tokens: nil,
method: nil,
interval: interval,
- reason: reason
+ reason: reason,
+ range: range,
+ breaks: breaks
)
}
+
+ var accessibilityValue: String {
+ guard let tokens else {
+ return reason ?? "No account readings in this range"
+ }
+ let zero = intervals.contains { $0.tokenDelta == 0 }
+ ? " Includes an observed zero-token interval."
+ : ""
+ return "\(tokens) account tokens observed.\(zero) Time without account observations is missing, not zero."
+ }
+
+ var currentWindowAccessibilityValue: String {
+ guard let tokens else {
+ return reason ?? "No account readings in this range"
+ }
+ let zero = intervals.contains { $0.tokenDelta == 0 }
+ ? " Includes an observed zero-token interval."
+ : ""
+ return "\(tokens) account tokens so far.\(zero) Gaps between readings and time since the latest reading are missing, not zero. Time from now until reset is future and has no observation."
+ }
+
+ var sourceDescription: String {
+ switch method {
+ case .lifetimeDelta: "Codex account summary"
+ case .dailyBuckets: "Codex UTC daily token totals"
+ case nil: "Codex account"
+ }
+ }
}
struct UsageIntelligenceInput: Equatable, Sendable {
@@ -555,6 +646,9 @@ struct UsageReaderSnapshot: Equatable, Sendable {
let freshness: UsageFreshness
let evidence: UsageEvidence
let guidance: UsageGuidance?
+ let guidanceUnavailableReason: String?
+ let suggestedPacePercentPerDay: Double?
+ let suggestedPaceUnavailableReason: String?
let chart: UsageChartSnapshot
let bankedResets: BankedResetSummary?
let accountTokenActivity: AccountTokenActivitySnapshot
@@ -570,22 +664,38 @@ struct UsageReaderSnapshot: Equatable, Sendable {
var fetchedAt: Date? { account?.fetchedAt }
- var weeklyUsageRemaining: LimitReading? { account?.mainLimit }
+ var weeklyUsageRemaining: LimitReading? {
+ interval == nil ? nil : account?.mainLimit
+ }
var accountFacts: AccountFacts? { account?.accountFacts }
var otherLimits: [LimitReading] { account?.otherLimits ?? [] }
var guidanceTitle: String {
- guidance?.title ?? "Not enough data"
+ guidance?.title
+ ?? guidanceUnavailableReason
+ ?? (evidence.coverage == .notApplicable ? evidence.reason : nil)
+ ?? "Guidance unavailable"
}
var guidanceMessage: String {
- guidance?.message ?? evidence.reason ?? "More account history is needed."
+ guidance?.message
+ ?? guidanceUnavailableReason
+ ?? evidence.reason
+ ?? "Guidance unavailable"
+ }
+
+ var runwayText: String {
+ guidance?.runway.text
+ ?? guidanceUnavailableReason
+ ?? "Guidance unavailable"
}
var suggestedPaceText: String {
- guidance?.suggestedPace ?? "Not enough data"
+ suggestedPacePercentPerDay.map(formattedSuggestedPace)
+ ?? suggestedPaceUnavailableReason
+ ?? "Weekly usage unavailable"
}
var evidenceText: String {
@@ -628,6 +738,11 @@ private enum CurrentUsagePolicy {
static let partialShare = 0.5
}
+private enum RecentMovementBreak {
+ case resetOrCorrection
+ case accountChange
+}
+
enum UsageIntelligenceEngine {
static func evaluate(_ input: UsageIntelligenceInput) -> UsageReaderSnapshot {
let currentSamples = input.account.flatMap { account in
@@ -675,28 +790,37 @@ enum UsageIntelligenceEngine {
now: input.now,
workloadMixChanged: workloadMixChanged
)
- let forecast: Forecast? = input.account.flatMap { account in
- guard let weeklyLimit = account.mainLimit else { return nil }
- guard case .available = input.sourceState,
- input.now >= weeklyLimit.window.startsAt,
- input.now < weeklyLimit.window.resetsAt else {
- return nil
- }
+ let recentMovement = recentAccountMovement(
+ account: input.account,
+ samples: input.samples,
+ sourceState: input.sourceState,
+ now: input.now,
+ accountEpochStartedAt: input.accountEpochStartedAt
+ )
+ let forecast: Forecast? = recentMovement.movement.flatMap { movement in
+ guard let account = input.account,
+ let weeklyLimit = account.mainLimit else { return nil }
return ForecastEngine.evaluate(
window: weeklyLimit.window,
samples: input.samples,
tokenHistory: account.tokenHistory,
safetyBuffer: input.safetyBuffer,
now: input.now,
- previousStatus: input.previousStatus
+ previousStatus: input.previousStatus,
+ recentMovement: movement
)
}
- let guidance: UsageGuidance? = forecast.flatMap { forecast in
- guard evidence.confidence == .high || evidence.confidence == .medium else {
- return nil
- }
- guard let weeklyLimit = input.account?.mainLimit else { return nil }
- return UsageGuidance(
+ let suggestedPace = suggestedPace(
+ account: input.account,
+ sourceState: input.sourceState,
+ safetyBuffer: input.safetyBuffer,
+ now: input.now
+ )
+ let guidance: UsageGuidance?
+ if let forecast,
+ let movement = recentMovement.movement,
+ let weeklyLimit = input.account?.mainLimit {
+ guidance = UsageGuidance(
source: .derivedEstimate,
status: forecast.status,
title: title(for: forecast.status),
@@ -704,17 +828,15 @@ enum UsageIntelligenceEngine {
window: weeklyLimit.window,
forecast: forecast,
safetyBuffer: input.safetyBuffer,
- now: input.now
- ),
- suggestedPace: suggestedPace(
- forecast: forecast,
- reset: weeklyLimit.window.resetsAt,
- now: input.now
+ movement: movement
),
+ suggestedPace: suggestedPace.percentPerDay
+ .map(formattedSuggestedPace)
+ ?? suggestedPace.reason
+ ?? "Weekly usage unavailable",
runway: runway(
window: weeklyLimit.window,
- forecast: forecast,
- now: input.now
+ movement: movement
),
remainingAtResetRange: evidence.confidence == .medium
? estimateRange(forecast)
@@ -722,11 +844,15 @@ enum UsageIntelligenceEngine {
caveat: evidence.confidence == .medium ? evidence.reason : nil,
forecast: forecast
)
+ } else {
+ guidance = nil
}
let chart = chart(
account: input.account,
samples: input.samples,
forecast: forecast,
+ recentMovement: recentMovement.movement,
+ sourceState: input.sourceState,
safetyBuffer: input.safetyBuffer
)
let currentFreshness = freshness(
@@ -734,14 +860,51 @@ enum UsageIntelligenceEngine {
sourceState: input.sourceState,
now: input.now
)
- let accountTokenActivity = accountTokenActivity(
+ let weeklyAccountTokenActivity = accountTokenActivity(
account: input.account,
samples: currentSamples
)
+ let selectedTokenRange: DateInterval?
+ switch input.analyticsExploration.timeRange {
+ case .currentWindow:
+ if let window = input.account?.mainLimit?.window,
+ window.startsAt <= input.now,
+ input.now < window.resetsAt {
+ selectedTokenRange = DateInterval(
+ start: window.startsAt,
+ end: window.resetsAt
+ )
+ } else {
+ selectedTokenRange = nil
+ }
+ case .selected:
+ selectedTokenRange = input.analyticsExploration.visibleRange
+ default:
+ selectedTokenRange = input.analyticsExploration.timeRange.interval(
+ within: DateInterval(start: input.now, end: input.now),
+ now: input.now
+ )
+ }
+ let accountTokenActivity = if let selectedTokenRange {
+ selectedRangeAccountTokenActivity(
+ account: input.account,
+ samples: input.samples,
+ range: selectedTokenRange,
+ now: input.now,
+ accountPartitionID: input.accountPartitionID,
+ accountEpochStartedAt: input.accountEpochStartedAt
+ )
+ } else {
+ AccountTokenActivitySnapshot.unavailable(
+ input.analyticsExploration.timeRange == .currentWindow
+ ? "Current allowance window unavailable"
+ : "No account readings in this range"
+ )
+ }
let localTokenActivity: LocalTokenActivitySnapshot
if let interval = tokenActivityInterval(
account: input.account,
- accountActivity: accountTokenActivity,
+ accountActivity: weeklyAccountTokenActivity,
accountEpochStartedAt: input.accountEpochStartedAt
) {
if let cached = reusableLocalAggregates?.localTokenActivity,
@@ -930,13 +1093,18 @@ enum UsageIntelligenceEngine {
localHistory: localHistoryCache
)
}
- let observedInterval = input.account.flatMap { account in
- account.mainLimit.map {
- UsageObservedInterval(
- limitID: $0.limitId,
- durationMinutes: $0.window.durationMinutes,
- startsAt: $0.window.startsAt,
- resetsAt: $0.window.resetsAt
+ let observedInterval = input.account.flatMap {
+ account -> UsageObservedInterval? in
+ return account.mainLimit.flatMap { limit in
+ guard input.now >= limit.window.startsAt,
+ input.now < limit.window.resetsAt else {
+ return nil
+ }
+ return UsageObservedInterval(
+ limitID: limit.limitId,
+ durationMinutes: limit.window.durationMinutes,
+ startsAt: limit.window.startsAt,
+ resetsAt: limit.window.resetsAt
)
}
}
@@ -952,7 +1120,7 @@ enum UsageIntelligenceEngine {
selectedRange: DeterministicInsightInput.effectiveRange(
usagePerToken: usagePerToken,
observedInterval: observedInterval,
- fetchedAt: input.account?.fetchedAt,
+ now: input.now,
exploration: input.analyticsExploration
),
filters: input.analyticsExploration.filters
@@ -972,6 +1140,9 @@ enum UsageIntelligenceEngine {
freshness: currentFreshness,
evidence: evidence,
guidance: guidance,
+ guidanceUnavailableReason: recentMovement.reason,
+ suggestedPacePercentPerDay: suggestedPace.percentPerDay,
+ suggestedPaceUnavailableReason: suggestedPace.reason,
chart: chart,
bankedResets: bankedResetSummary(
account: input.account,
@@ -1193,6 +1364,263 @@ enum UsageIntelligenceEngine {
)
}
+ private static func selectedRangeAccountTokenActivity(
+ account: UsageSnapshot?,
+ samples: [UsageSample],
+ range: DateInterval,
+ now: Date,
+ accountPartitionID: String?,
+ accountEpochStartedAt: Date?
+ ) -> AccountTokenActivitySnapshot {
+ var timeline = samples
+ if let account,
+ let window = account.mainLimit?.window,
+ let facts = account.accountFacts,
+ let lifetimeTokens = facts.lifetimeTokens {
+ let observedAt = facts.lifetimeTokensObservedAt ?? account.fetchedAt
+ guard window.startsAt <= observedAt,
+ observedAt <= window.resetsAt else {
+ return evaluateAccountTokenTimeline(
+ timeline,
+ account: account,
+ range: range,
+ now: now,
+ accountPartitionID: accountPartitionID,
+ accountEpochStartedAt: accountEpochStartedAt
+ )
+ }
+ timeline.append(UsageSample(
+ observedAt: observedAt,
+ remainingPercent: window.remainingPercent,
+ resetsAt: window.resetsAt,
+ lifetimeTokens: lifetimeTokens
+ ))
+ }
+ return evaluateAccountTokenTimeline(
+ timeline,
+ account: account,
+ range: range,
+ now: now,
+ accountPartitionID: accountPartitionID,
+ accountEpochStartedAt: accountEpochStartedAt
+ )
+ }
+
+ private static func evaluateAccountTokenTimeline(
+ _ timeline: [UsageSample],
+ account: UsageSnapshot?,
+ range: DateInterval,
+ now: Date,
+ accountPartitionID: String?,
+ accountEpochStartedAt: Date?
+ ) -> AccountTokenActivitySnapshot {
+ let readings = timeline.filter { $0.observedAt <= now }.sorted {
+ if $0.observedAt != $1.observedAt {
+ return $0.observedAt < $1.observedAt
+ }
+ if $0.resetsAt != $1.resetsAt {
+ return $0.resetsAt < $1.resetsAt
+ }
+ if $0.lifetimeTokens != $1.lifetimeTokens {
+ return ($0.lifetimeTokens ?? -1) < ($1.lifetimeTokens ?? -1)
+ }
+ if $0.remainingPercent != $1.remainingPercent {
+ return $0.remainingPercent < $1.remainingPercent
+ }
+ return !$0.comparisonBreak && $1.comparisonBreak
+ }
+ var unique: [UsageSample] = []
+ for reading in readings {
+ if let last = unique.last,
+ last.observedAt == reading.observedAt,
+ last.resetsAt == reading.resetsAt,
+ last.lifetimeTokens == reading.lifetimeTokens,
+ last.remainingPercent == reading.remainingPercent {
+ if reading.comparisonBreak {
+ unique[unique.count - 1] = reading
+ }
+ continue
+ }
+ unique.append(reading)
+ }
+ let conflictingTimestamps = Set(
+ Dictionary(grouping: unique, by: \.observedAt)
+ .compactMap { $0.value.count > 1 ? $0.key : nil }
+ )
+ var intervals: [AccountTokenActivityInterval] = []
+ var breaks: [AccountTokenActivityBreak] = []
+ var previous: UsageSample?
+ var sawEarlierEpoch = false
+
+ func recordBreak(
+ at timestamp: Date,
+ reason: AccountTokenActivityBreakReason
+ ) {
+ guard range.contains(timestamp) else { return }
+ let value = AccountTokenActivityBreak(
+ timestamp: timestamp,
+ reason: reason
+ )
+ if breaks.last != value { breaks.append(value) }
+ }
+
+ for reading in unique {
+ if let accountEpochStartedAt,
+ reading.observedAt < accountEpochStartedAt {
+ sawEarlierEpoch = true
+ previous = nil
+ continue
+ }
+ let crossedAccountEpoch = sawEarlierEpoch
+ if crossedAccountEpoch, let accountEpochStartedAt {
+ recordBreak(at: accountEpochStartedAt, reason: .accountChange)
+ sawEarlierEpoch = false
+ }
+ if conflictingTimestamps.contains(reading.observedAt) {
+ recordBreak(
+ at: reading.observedAt,
+ reason: .conflictingObservation
+ )
+ previous = nil
+ continue
+ }
+ guard reading.isValid,
+ let readingTokens = reading.lifetimeTokens,
+ readingTokens >= 0 else {
+ recordBreak(at: reading.observedAt, reason: .invalidCounter)
+ previous = nil
+ continue
+ }
+ if reading.comparisonBreak {
+ if !crossedAccountEpoch {
+ recordBreak(at: reading.observedAt, reason: .correction)
+ }
+ previous = reading
+ continue
+ }
+ guard let start = previous,
+ let startTokens = start.lifetimeTokens,
+ let endTokens = reading.lifetimeTokens else {
+ previous = reading
+ continue
+ }
+ defer { previous = reading }
+ guard reading.resetsAt == start.resetsAt else {
+ recordBreak(
+ at: reading.observedAt,
+ reason: .allowanceWindowChange
+ )
+ continue
+ }
+ guard endTokens >= startTokens else {
+ recordBreak(
+ at: reading.observedAt,
+ reason: .counterDecrease
+ )
+ continue
+ }
+ guard start.observedAt >= range.start,
+ reading.observedAt <= range.end else { continue }
+ intervals.append(AccountTokenActivityInterval(
+ start: start.observedAt,
+ end: reading.observedAt,
+ tokenDelta: endTokens - startTokens,
+ method: .lifetimeDelta,
+ accountPartitionID: accountPartitionID,
+ limitID: account?.mainLimit?.limitId,
+ allowanceReset: reading.resetsAt
+ ))
+ }
+ guard let first = intervals.first,
+ let last = intervals.last else {
+ if let account,
+ let fallback = dailyTokenActivity(
+ account: account,
+ range: range,
+ now: now,
+ accountPartitionID: accountPartitionID,
+ breaks: breaks
+ ) {
+ return fallback
+ }
+ return .unavailable(
+ "No account readings in this range",
+ range: range,
+ breaks: breaks
+ )
+ }
+ var total: Int64 = 0
+ for interval in intervals {
+ let sum = total.addingReportingOverflow(interval.tokenDelta)
+ guard !sum.overflow else {
+ return .unavailable(
+ "Lifetime token reading is invalid",
+ range: range
+ )
+ }
+ total = sum.partialValue
+ }
+ return AccountTokenActivitySnapshot(
+ state: .partial,
+ tokens: total,
+ method: .lifetimeDelta,
+ interval: DateInterval(start: first.start, end: last.end),
+ reason: nil,
+ range: range,
+ intervals: intervals,
+ breaks: breaks
+ )
+ }
+
+ private static func dailyTokenActivity(
+ account: UsageSnapshot,
+ range: DateInterval,
+ now: Date,
+ accountPartitionID: String?,
+ breaks: [AccountTokenActivityBreak] = []
+ ) -> AccountTokenActivitySnapshot? {
+ let intervals = account.tokenHistory.compactMap {
+ day -> AccountTokenActivityInterval? in
+ let end = day.date.addingTimeInterval(86_400)
+ guard day.completeness == .complete,
+ day.tokens >= 0,
+ day.date >= range.start,
+ end <= range.end,
+ end <= now else { return nil }
+ return AccountTokenActivityInterval(
+ start: day.date,
+ end: end,
+ tokenDelta: day.tokens,
+ method: .dailyBuckets,
+ accountPartitionID: accountPartitionID,
+ limitID: account.mainLimit?.limitId,
+ allowanceReset: nil
+ )
+ }.sorted { $0.start < $1.start }
+ guard let first = intervals.first,
+ let last = intervals.last else { return nil }
+ var total: Int64 = 0
+ for interval in intervals {
+ let sum = total.addingReportingOverflow(interval.tokenDelta)
+ guard !sum.overflow else { return nil }
+ total = sum.partialValue
+ }
+ let isContinuous = zip(intervals, intervals.dropFirst())
+ .allSatisfy { $0.0.end == $0.1.start }
+ return AccountTokenActivitySnapshot(
+ state: first.start == range.start
+ && last.end == range.end
+ && isContinuous ? .exact : .partial,
+ tokens: total,
+ method: .dailyBuckets,
+ interval: DateInterval(start: first.start, end: last.end),
+ reason: nil,
+ range: range,
+ intervals: intervals,
+ breaks: breaks
+ )
+ }
+
private static func dailyTokenActivity(
account: UsageSnapshot,
window: UsageWindow
@@ -1248,6 +1676,8 @@ enum UsageIntelligenceEngine {
account: UsageSnapshot?,
samples: [UsageSample],
forecast: Forecast?,
+ recentMovement: RecentAccountMovement?,
+ sourceState: UsageSourceState,
safetyBuffer: Double
) -> UsageChartSnapshot {
guard let account, let weeklyLimit = account.mainLimit else {
@@ -1270,11 +1700,41 @@ enum UsageIntelligenceEngine {
account: account,
samples: samples
)
- guard let forecast else {
+ let historicalReference: UsageForecastReference? = if case .available = sourceState {
+ chartHistoricalReference(
+ samples: samples,
+ excluding: window.resetsAt,
+ remaining: window.remainingPercent,
+ daysLeft: max(
+ window.resetsAt.timeIntervalSince(account.fetchedAt)
+ / 86_400,
+ 0
+ )
+ )
+ } else {
+ nil
+ }
+ guard let forecast, let recentMovement else {
+ let reference = historicalReference.map {
+ UsageChartReferenceSeries(
+ source: $0.source,
+ points: projection(
+ reading: UsageSample(
+ observedAt: account.fetchedAt,
+ remainingPercent: window.remainingPercent,
+ resetsAt: window.resetsAt
+ ),
+ window: window,
+ rate: $0.percentPerDay,
+ remainingAtReset: $0.remainingAtReset
+ )
+ )
+ }
return UsageChartSnapshot(
observedSource: .account,
target: target,
currentProjection: [],
+ reference: reference,
currentAllowanceReset: window.resetsAt,
allowanceWindows: allowanceWindows,
currentRunsFaster: false,
@@ -1286,22 +1746,13 @@ enum UsageIntelligenceEngine {
? forecast.historicalReference
: nil
let chartReference = strictAccountReference
- ?? chartHistoricalReference(
- samples: samples,
- excluding: window.resetsAt,
- remaining: window.remainingPercent,
- daysLeft: max(
- window.resetsAt.timeIntervalSince(account.fetchedAt)
- / 86_400,
- 0
- )
- )
+ ?? historicalReference
?? forecast.historicalReference
let reference = chartReference.map {
UsageChartReferenceSeries(
source: $0.source,
points: projection(
- account: account,
+ reading: recentMovement.latest,
window: window,
rate: $0.percentPerDay,
remainingAtReset: $0.remainingAtReset
@@ -1315,7 +1766,7 @@ enum UsageIntelligenceEngine {
observedSource: .account,
target: target,
currentProjection: projection(
- account: account,
+ reading: recentMovement.latest,
window: window,
rate: forecast.currentPercentPerDay,
remainingAtReset: forecast.expectedRemainingAtReset
@@ -1426,26 +1877,26 @@ enum UsageIntelligenceEngine {
}
private static func projection(
- account: UsageSnapshot,
+ reading: UsageSample,
window: UsageWindow,
rate: Double,
remainingAtReset: Double
) -> [UsageChartPoint] {
let current = UsageChartPoint(
- date: account.fetchedAt,
- remaining: window.remainingPercent
+ date: reading.observedAt,
+ remaining: reading.remainingPercent
)
guard rate > 0 else {
return [
current,
UsageChartPoint(
date: window.resetsAt,
- remaining: window.remainingPercent
+ remaining: reading.remainingPercent
)
]
}
- let exhaustion = account.fetchedAt.addingTimeInterval(
- window.remainingPercent / rate * 86_400
+ let exhaustion = reading.observedAt.addingTimeInterval(
+ reading.remainingPercent / rate * 86_400
)
let endpoint = exhaustion < window.resetsAt
? UsageChartPoint(date: exhaustion, remaining: 0)
@@ -1487,6 +1938,130 @@ enum UsageIntelligenceEngine {
: .fresh
}
+ private static func recentAccountMovement(
+ account: UsageSnapshot?,
+ samples: [UsageSample],
+ sourceState: UsageSourceState,
+ now: Date,
+ accountEpochStartedAt: Date?
+ ) -> (movement: RecentAccountMovement?, reason: String?) {
+ if case let .failed(message) = sourceState {
+ return (nil, message)
+ }
+ guard let account,
+ let window = account.mainLimit?.window,
+ window.isValid else {
+ return (nil, "No current weekly allowance reading")
+ }
+ guard now >= window.startsAt else {
+ return (nil, "Current allowance window has not started")
+ }
+ guard now < window.resetsAt else {
+ return (nil, "Current allowance window unavailable")
+ }
+
+ let current = UsageSample(
+ observedAt: account.fetchedAt,
+ remainingPercent: window.remainingPercent,
+ resetsAt: window.resetsAt,
+ comparisonBreak: accountEpochStartedAt == account.fetchedAt
+ )
+ let rollingStart = max(
+ now.addingTimeInterval(-86_400),
+ window.startsAt,
+ accountEpochStartedAt ?? .distantPast
+ )
+ let grouped = Dictionary(
+ grouping: (samples + [current]).filter {
+ $0.hasValidObservationMetadata
+ && $0.observedAt >= rollingStart
+ && $0.observedAt <= account.fetchedAt
+ && $0.observedAt <= now
+ },
+ by: \.observedAt
+ )
+ var conflicts = Set()
+ let readings = grouped.compactMap { date, values -> UsageSample? in
+ let preferred = values.first {
+ date == current.observedAt
+ && $0.remainingPercent == current.remainingPercent
+ && $0.resetsAt == current.resetsAt
+ } ?? values.min {
+ if $0.resetsAt != $1.resetsAt {
+ return $0.resetsAt < $1.resetsAt
+ }
+ return $0.remainingPercent < $1.remainingPercent
+ }
+ guard let preferred else { return nil }
+ if values.contains(where: {
+ $0.remainingPercent != preferred.remainingPercent
+ || $0.resetsAt != preferred.resetsAt
+ }) {
+ conflicts.insert(date)
+ }
+ return UsageSample(
+ observedAt: preferred.observedAt,
+ remainingPercent: preferred.remainingPercent,
+ resetsAt: preferred.resetsAt,
+ lifetimeTokens: preferred.lifetimeTokens,
+ comparisonBreak: values.contains { $0.comparisonBreak }
+ )
+ }.sorted { $0.observedAt < $1.observedAt }
+
+ var segment: [UsageSample] = []
+ var latestBreak: RecentMovementBreak?
+ for reading in readings {
+ let accountChanged = reading.comparisonBreak
+ && accountEpochStartedAt == reading.observedAt
+ if conflicts.contains(reading.observedAt)
+ || reading.comparisonBreak
+ || segment.last.map({
+ $0.resetsAt != reading.resetsAt
+ || reading.remainingPercent
+ > $0.remainingPercent
+ + UsageHistoryPolicy.correctionTolerance
+ }) == true {
+ segment = [reading]
+ latestBreak = accountChanged ? .accountChange : .resetOrCorrection
+ } else {
+ segment.append(reading)
+ }
+ }
+
+ guard let first = segment.first,
+ let latest = segment.last,
+ segment.count >= 2,
+ latest.observedAt > first.observedAt else {
+ switch latestBreak {
+ case .accountChange:
+ return (nil, "Account changed — waiting for a second reading")
+ case .resetOrCorrection:
+ return (
+ nil,
+ "Waiting for a second account reading after reset or correction"
+ )
+ case nil:
+ return (nil, "At least two compatible account readings are needed")
+ }
+ }
+ let elapsedDays = latest.observedAt.timeIntervalSince(first.observedAt)
+ / 86_400
+ let percentPerDay = max(
+ (first.remainingPercent - latest.remainingPercent) / elapsedDays,
+ 0
+ )
+ guard percentPerDay.isFinite else {
+ return (nil, "At least two compatible account readings are needed")
+ }
+ return (
+ RecentAccountMovement(
+ latest: latest,
+ percentPerDay: percentPerDay
+ ),
+ nil
+ )
+ }
+
private static func evidence(
account: UsageSnapshot?,
samples: [UsageSample],
@@ -1498,7 +2073,7 @@ enum UsageIntelligenceEngine {
return UsageEvidence(
coverage: .unavailable,
confidence: .unavailable,
- reason: "Weekly usage unavailable",
+ reason: "No current weekly allowance reading",
policyVersion: CurrentUsagePolicy.version
)
}
@@ -1507,7 +2082,9 @@ enum UsageIntelligenceEngine {
return UsageEvidence(
coverage: .notApplicable,
confidence: .unavailable,
- reason: "Weekly window ended",
+ reason: now >= window.resetsAt
+ ? "Current allowance window unavailable"
+ : "Current allowance window has not started",
policyVersion: CurrentUsagePolicy.version
)
}
@@ -1635,12 +2212,11 @@ enum UsageIntelligenceEngine {
private static func runway(
window: UsageWindow,
- forecast: Forecast,
- now: Date
+ movement: RecentAccountMovement
) -> UsageRunway {
- guard forecast.currentPercentPerDay > 0 else { return .throughReset }
- let exhaustsAt = now.addingTimeInterval(
- window.remainingPercent / forecast.currentPercentPerDay * 86_400
+ guard movement.percentPerDay > 0 else { return .throughReset }
+ let exhaustsAt = movement.latest.observedAt.addingTimeInterval(
+ movement.latest.remainingPercent / movement.percentPerDay * 86_400
)
return exhaustsAt < window.resetsAt
? .exhausts(
@@ -1674,12 +2250,14 @@ enum UsageIntelligenceEngine {
window: UsageWindow,
forecast: Forecast,
safetyBuffer: Double,
- now: Date
+ movement: RecentAccountMovement
) -> String {
switch forecast.status {
case .slowDown:
- let timeLeft = window.resetsAt.timeIntervalSince(now)
- let timeToEmpty = window.remainingPercent
+ let timeLeft = window.resetsAt.timeIntervalSince(
+ movement.latest.observedAt
+ )
+ let timeToEmpty = movement.latest.remainingPercent
/ max(forecast.safetyPercentPerDay, 0.01) * 86_400
let early = max(timeLeft - timeToEmpty, 0)
return early > 0
@@ -1694,23 +2272,32 @@ enum UsageIntelligenceEngine {
}
private static func suggestedPace(
- forecast: Forecast,
- reset: Date,
+ account: UsageSnapshot?,
+ sourceState: UsageSourceState,
+ safetyBuffer: Double,
now: Date
- ) -> String {
- let value = reset.timeIntervalSince(now) <= 86_400
- ? forecast.recommendedPercentPerDay / 24
- : forecast.recommendedPercentPerDay
- let unit = reset.timeIntervalSince(now) <= 86_400 ? "an hour" : "a day"
- return "Up to \(oneDecimal(value))% \(unit)"
- }
-
- private static func oneDecimal(_ value: Double) -> String {
- value.formatted(
- .number
- .precision(.fractionLength(1))
- .locale(Locale(identifier: "en_US"))
+ ) -> (percentPerDay: Double?, reason: String?) {
+ if case let .failed(message) = sourceState {
+ return (nil, message)
+ }
+ guard let window = account?.mainLimit?.window, window.isValid else {
+ return (nil, "Weekly usage unavailable")
+ }
+ guard now >= window.startsAt else {
+ return (nil, "Current allowance window has not started")
+ }
+ let secondsToReset = window.resetsAt.timeIntervalSince(now)
+ guard secondsToReset > 0, secondsToReset.isFinite else {
+ return (nil, "Current allowance window unavailable")
+ }
+ let value = max(
+ (window.remainingPercent - safetyBuffer)
+ / (secondsToReset / 86_400),
+ 0
)
+ return value.isFinite
+ ? (value, nil)
+ : (nil, "Current allowance window unavailable")
}
private static func durationText(_ seconds: TimeInterval) -> String {
@@ -1722,3 +2309,12 @@ enum UsageIntelligenceEngine {
return "\(hours) \(hours == 1 ? "hour" : "hours")"
}
}
+
+private func formattedSuggestedPace(_ percentPerDay: Double) -> String {
+ let value = percentPerDay.formatted(
+ .number
+ .precision(.fractionLength(1))
+ .locale(Locale(identifier: "en_US"))
+ )
+ return "Up to \(value) percentage points a day"
+}
diff --git a/Sources/CodexLimits/UsageModels.swift b/Sources/CodexLimits/UsageModels.swift
index f9c5e7d..79e7789 100644
--- a/Sources/CodexLimits/UsageModels.swift
+++ b/Sources/CodexLimits/UsageModels.swift
@@ -69,12 +69,16 @@ struct UsageSample: Codable, Equatable, Hashable, Sendable {
let lifetimeTokens: Int64?
let comparisonBreak: Bool
- var isValid: Bool {
+ var hasValidObservationMetadata: Bool {
observedAt.isSupportedUsageDate
&& resetsAt.isSupportedUsageDate
&& observedAt <= resetsAt
&& remainingPercent.isFinite
&& (0 ... 100).contains(remainingPercent)
+ }
+
+ var isValid: Bool {
+ hasValidObservationMetadata
&& (lifetimeTokens.map { $0 >= 0 } ?? true)
}
diff --git a/Sources/CodexLimits/UsageMonitor.swift b/Sources/CodexLimits/UsageMonitor.swift
index c9b5d6a..c52c021 100644
--- a/Sources/CodexLimits/UsageMonitor.swift
+++ b/Sources/CodexLimits/UsageMonitor.swift
@@ -144,7 +144,9 @@ final class UsageMonitor: ObservableObject {
}
if let data = defaults.data(forKey: Self.stateKey),
let state = try? JSONDecoder().decode(StoredState.self, from: data) {
- let restoredSamples = state.samples.filter(\.isValid)
+ let restoredSamples = state.samples.filter(
+ \.hasValidObservationMetadata
+ )
accountSnapshot = state.snapshot.flatMap {
$0.isValid ? $0 : nil
}
@@ -918,13 +920,13 @@ final class UsageMonitor: ObservableObject {
) {
let input = DeterministicInsightInput(
reader: readerSnapshot,
- exploration: exploration
+ exploration: exploration,
+ now: Date()
)
readerSnapshot.insights = DeterministicInsightEngine.evaluate(
input,
dispositions: dispositions
)
- guard evaluationTask != nil else { return }
let pending = beginRecalculation(
analyticsExploration: exploration,
insightDispositions: dispositions
diff --git a/Tests/CodexLimitsTests/AnalyticsWorkspaceTests.swift b/Tests/CodexLimitsTests/AnalyticsWorkspaceTests.swift
index 06819c1..a7736ba 100644
--- a/Tests/CodexLimitsTests/AnalyticsWorkspaceTests.swift
+++ b/Tests/CodexLimitsTests/AnalyticsWorkspaceTests.swift
@@ -123,45 +123,6 @@ final class AnalyticsWorkspaceTests: XCTestCase {
)
}
- func testAccountTokenRangeSumsOnlyCompleteFullDays() throws {
- let formatter = ISO8601DateFormatter()
- let interval = DateInterval(
- start: try XCTUnwrap(
- formatter.date(from: "2026-07-01T12:00:00Z")
- ),
- end: try XCTUnwrap(
- formatter.date(from: "2026-07-04T12:00:00Z")
- )
- )
- let days = try [
- ("2026-07-01T00:00:00Z", 100, TokenDayCompleteness.complete),
- ("2026-07-02T00:00:00Z", 200, .complete),
- ("2026-07-03T00:00:00Z", 300, .complete),
- ("2026-07-04T00:00:00Z", 400, .partial)
- ].map {
- TokenDay(
- date: try XCTUnwrap(formatter.date(from: $0.0)),
- tokens: Int64($0.1),
- completeness: $0.2
- )
- }
-
- let range = AccountTokenActivityRange(
- days: days,
- interval: interval
- )
-
- XCTAssertEqual(range.days.map(\.tokens), [200, 300])
- XCTAssertEqual(range.completeDayCount, 2)
- XCTAssertEqual(range.completeTokens, 500)
-
- let missingDay = AccountTokenActivityRange(
- days: days.filter { $0.date != days[2].date },
- interval: interval
- )
- XCTAssertNil(missingDay.completeTokens)
- }
-
func testRestoredLocalGraphFallsBackToUsageRemaining() throws {
let defaults = try XCTUnwrap(
UserDefaults(
@@ -277,33 +238,589 @@ final class AnalyticsWorkspaceTests: XCTestCase {
XCTAssertNil(reader.localTokenActivity.tokens)
}
- func testPresetRangeEndsAtLatestObservedTimeAndIsClampedToWindow() {
- let end = Date(timeIntervalSince1970: 10 * 86_400)
- let bounds = DateInterval(
- start: end.addingTimeInterval(-7 * 86_400),
- end: end
+ func testRollingTokenActivityViewDistinguishesObservedZeroAndMissing() {
+ let now = Date(timeIntervalSince1970: 10 * 86_400)
+ let account = usageSnapshot(fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ var exploration = AnalyticsExplorationState.initial
+ exploration.graph = .tokenActivity
+ exploration.timeRange = .oneDay
+ let evaluate: ([UsageSample]) -> UsageReaderSnapshot = { samples in
+ UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: samples,
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ analyticsExploration: exploration
+ )
+ )
+ }
+ let first = UsageSample(
+ observedAt: now.addingTimeInterval(-6 * 3_600),
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ )
+ let positive = evaluate([
+ first,
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 75,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ])
+ let zero = evaluate([
+ first,
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 75,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ )
+ ])
+ let missing = evaluate([first])
+ let store = AnalyticsWorkspaceStore(
+ defaults: UserDefaults(
+ suiteName: "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ )!
)
- let latestObserved = end.addingTimeInterval(-5 * 86_400)
+ store.selectGraph(.tokenActivity)
+ store.selectTimeRange(.oneDay)
+ XCTAssertEqual(positive.accountTokenActivity.tokens, 300)
+ XCTAssertTrue(
+ positive.accountTokenActivity.accessibilityValue.contains(
+ "Time without account observations is missing, not zero"
+ )
+ )
+ XCTAssertFalse(
+ positive.accountTokenActivity.accessibilityValue.contains(
+ "Coverage"
+ )
+ )
+ XCTAssertEqual(zero.accountTokenActivity.tokens, 0)
+ XCTAssertTrue(
+ zero.accountTokenActivity.accessibilityValue.contains(
+ "observed zero-token interval"
+ )
+ )
XCTAssertEqual(
- AnalyticsTimeRange.oneDay.interval(
- within: bounds,
- endingAt: latestObserved
+ missing.accountTokenActivity.accessibilityValue,
+ "No account readings in this range"
+ )
+ XCTAssertTrue(
+ renders(
+ AnalyticsWorkspaceBody(
+ reader: positive,
+ store: store,
+ assistedInsights: CodexAssistedInsightStore(),
+ now: now
+ ),
+ size: CGSize(width: 640, height: 780)
+ )
+ )
+ }
+
+ func testUTCBucketIntervalUsesTheRequestedMacTimeZoneForDisplay() throws {
+ let formatter = ISO8601DateFormatter()
+ let locale = Locale(identifier: "en_US_POSIX")
+ let utc = try XCTUnwrap(TimeZone(identifier: "UTC"))
+ let berlin = try XCTUnwrap(TimeZone(identifier: "Europe/Berlin"))
+ let winter = DateInterval(
+ start: try XCTUnwrap(formatter.date(from: "2026-01-01T00:00:00Z")),
+ end: try XCTUnwrap(formatter.date(from: "2026-01-02T00:00:00Z"))
+ )
+ let summer = DateInterval(
+ start: try XCTUnwrap(formatter.date(from: "2026-07-01T00:00:00Z")),
+ end: try XCTUnwrap(formatter.date(from: "2026-07-02T00:00:00Z"))
+ )
+
+ let utcText = accountTokenIntervalText(
+ winter,
+ timeZone: utc,
+ locale: locale
+ )
+ let cetText = accountTokenIntervalText(
+ winter,
+ timeZone: berlin,
+ locale: locale
+ )
+ let summerUTCText = accountTokenIntervalText(
+ summer,
+ timeZone: utc,
+ locale: locale
+ )
+ let cestText = accountTokenIntervalText(
+ summer,
+ timeZone: berlin,
+ locale: locale
+ )
+
+ XCTAssertNotEqual(utcText, cetText)
+ XCTAssertNotEqual(summerUTCText, cestText)
+ XCTAssertEqual(berlin.secondsFromGMT(for: winter.start), 3_600)
+ XCTAssertEqual(berlin.secondsFromGMT(for: summer.start), 7_200)
+ XCTAssertEqual(winter.duration, 86_400)
+ XCTAssertEqual(summer.duration, 86_400)
+ }
+
+ func testCurrentWindowTokenActivityExposesSoFarCopyAndRenders() throws {
+ let start = try date("2026-08-01T12:13:00Z")
+ let now = try date("2026-08-03T12:13:00Z")
+ let reset = try date("2026-08-08T12:13:00Z")
+ let account = UsageSnapshot(
+ mainLimit: LimitReading(
+ limitId: "weekly",
+ name: "Weekly",
+ window: UsageWindow(
+ remainingPercent: 70,
+ resetsAt: reset,
+ durationMinutes: 10_080
+ )
),
- DateInterval(
- start: latestObserved.addingTimeInterval(-86_400),
- end: latestObserved
+ otherLimits: [],
+ tokenHistory: [],
+ emergencyResetCount: 0,
+ fetchedAt: now
+ )
+ let reader = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: start,
+ remainingPercent: 100,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 70,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil
)
)
+ let store = AnalyticsWorkspaceStore(
+ defaults: UserDefaults(
+ suiteName: "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ )!
+ )
+ store.selectGraph(.tokenActivity)
+ store.selectTimeRange(.currentWindow)
+
+ XCTAssertEqual(reader.accountTokenActivity.tokens, 300)
+ XCTAssertTrue(
+ reader.accountTokenActivity.currentWindowAccessibilityValue
+ .contains("so far")
+ )
+ XCTAssertTrue(
+ reader.accountTokenActivity.currentWindowAccessibilityValue
+ .contains("Time from now until reset is future and has no observation")
+ )
+ XCTAssertTrue(
+ renders(
+ AnalyticsWorkspaceBody(
+ reader: reader,
+ store: store,
+ assistedInsights: CodexAssistedInsightStore(),
+ now: now
+ ),
+ size: CGSize(width: 640, height: 780)
+ )
+ )
+ }
+
+ func testAccountTokenIntervalSelectionPreservesFactualIdentity() {
+ let range = DateInterval(
+ start: Date(timeIntervalSince1970: 1_000),
+ end: Date(timeIntervalSince1970: 10_000)
+ )
+ let positive = AccountTokenActivityInterval(
+ start: Date(timeIntervalSince1970: 2_000),
+ end: Date(timeIntervalSince1970: 3_000),
+ tokenDelta: 300,
+ method: .lifetimeDelta,
+ accountPartitionID: "account-a",
+ limitID: "weekly",
+ allowanceReset: range.end
+ )
+ let zero = AccountTokenActivityInterval(
+ start: Date(timeIntervalSince1970: 5_000),
+ end: Date(timeIntervalSince1970: 6_000),
+ tokenDelta: 0,
+ method: .lifetimeDelta,
+ accountPartitionID: "account-a",
+ limitID: "weekly",
+ allowanceReset: range.end
+ )
+ let daily = AccountTokenActivityInterval(
+ start: Date(timeIntervalSince1970: 7_000),
+ end: Date(timeIntervalSince1970: 8_000),
+ tokenDelta: 900,
+ method: .dailyBuckets,
+ accountPartitionID: "account-a",
+ limitID: "weekly",
+ allowanceReset: nil
+ )
+ let intervals = [daily, zero, positive]
+
+ XCTAssertEqual(
+ accountTokenInterval(
+ at: Date(timeIntervalSince1970: 2_500),
+ in: intervals,
+ within: range
+ ),
+ positive
+ )
+ XCTAssertEqual(
+ accountTokenInterval(
+ at: Date(timeIntervalSince1970: 5_500),
+ in: intervals,
+ within: range
+ ),
+ zero
+ )
+ XCTAssertNil(accountTokenInterval(
+ at: Date(timeIntervalSince1970: 4_000),
+ in: intervals,
+ within: range
+ ))
+ XCTAssertNil(accountTokenInterval(
+ at: Date(timeIntervalSince1970: 9_000),
+ in: intervals,
+ within: range
+ ))
+ XCTAssertEqual(
+ steppedAccountTokenInterval(
+ in: intervals,
+ from: nil,
+ by: 1
+ ),
+ positive
+ )
+ XCTAssertEqual(
+ steppedAccountTokenInterval(
+ in: intervals,
+ from: positive,
+ by: 1
+ ),
+ zero
+ )
+ XCTAssertEqual(
+ steppedAccountTokenInterval(
+ in: intervals,
+ from: zero,
+ by: 1
+ ),
+ daily
+ )
+ XCTAssertEqual(
+ retainedAccountTokenInterval(
+ daily,
+ in: intervals,
+ range: range
+ ),
+ daily
+ )
+ XCTAssertNil(retainedAccountTokenInterval(
+ daily,
+ in: [positive, zero],
+ range: range
+ ))
+ XCTAssertNil(retainedAccountTokenInterval(
+ daily,
+ in: intervals,
+ range: DateInterval(start: range.start, end: daily.start)
+ ))
+ XCTAssertEqual(zero.method.displayName, "Lifetime counter interval")
+ XCTAssertEqual(daily.method.displayName, "UTC daily bucket")
+ XCTAssertTrue(
+ accountTokenIntervalAccessibilityValue(zero)
+ .contains("0 account tokens. Account.")
+ )
+ XCTAssertTrue(
+ accountTokenIntervalAccessibilityValue(daily)
+ .contains("UTC daily bucket")
+ )
+ }
+
+ func testSelectedTokenIntervalFormattingDoesNotChangeIdentity() throws {
+ let formatter = ISO8601DateFormatter()
+ let interval = AccountTokenActivityInterval(
+ start: try XCTUnwrap(formatter.date(from: "2026-07-01T00:00:00Z")),
+ end: try XCTUnwrap(formatter.date(from: "2026-07-02T00:00:00Z")),
+ tokenDelta: 900,
+ method: .dailyBuckets,
+ accountPartitionID: "account-a",
+ limitID: "weekly",
+ allowanceReset: nil
+ )
+ let utc = try XCTUnwrap(TimeZone(identifier: "UTC"))
+ let berlin = try XCTUnwrap(TimeZone(identifier: "Europe/Berlin"))
+ let locale = Locale(identifier: "en_US_POSIX")
+ let dateInterval = DateInterval(start: interval.start, end: interval.end)
+
+ XCTAssertNotEqual(
+ accountTokenIntervalText(
+ dateInterval,
+ timeZone: utc,
+ locale: locale
+ ),
+ accountTokenIntervalText(
+ dateInterval,
+ timeZone: berlin,
+ locale: locale
+ )
+ )
+ XCTAssertEqual(interval.tokenDelta, 900)
+ XCTAssertEqual(interval.id, interval)
+ }
+
+ func testTokenDisplayAggregationPreservesTotalsGapsAndBreaks() {
+ let base = Date(timeIntervalSince1970: 1_000)
+ let reset = base.addingTimeInterval(20_000)
+ func interval(
+ _ start: TimeInterval,
+ _ end: TimeInterval,
+ _ tokens: Int64,
+ method: AccountTokenActivityMethod = .lifetimeDelta
+ ) -> AccountTokenActivityInterval {
+ AccountTokenActivityInterval(
+ start: base.addingTimeInterval(start),
+ end: base.addingTimeInterval(end),
+ tokenDelta: tokens,
+ method: method,
+ accountPartitionID: "account-a",
+ limitID: "weekly",
+ allowanceReset: reset
+ )
+ }
+ let source = [
+ interval(0, 10, 10),
+ interval(10, 20, 20),
+ interval(30, 40, 30),
+ interval(40, 50, 40),
+ interval(50, 60, 50, method: .dailyBuckets),
+ interval(60, 70, 0, method: .dailyBuckets),
+ interval(70, 80, 0, method: .dailyBuckets)
+ ]
+ let breakAt40 = AccountTokenActivityBreak(
+ timestamp: base.addingTimeInterval(40),
+ reason: .counterDecrease
+ )
+ let coarse = displayedAccountTokenIntervals(
+ source.reversed(),
+ breaks: [breakAt40],
+ maximumMarks: 2
+ )
+ let fine = displayedAccountTokenIntervals(
+ source,
+ breaks: [breakAt40],
+ maximumMarks: 100
+ )
+ let sourceTotal = source.reduce(Int64(0)) { $0 + $1.tokenDelta }
+
+ XCTAssertEqual(
+ coarse.reduce(Int64(0)) { $0 + $1.tokenDelta },
+ sourceTotal
+ )
+ XCTAssertEqual(
+ fine.reduce(Int64(0)) { $0 + $1.tokenDelta },
+ sourceTotal
+ )
+ XCTAssertEqual(
+ coarse,
+ displayedAccountTokenIntervals(
+ source,
+ breaks: [breakAt40],
+ maximumMarks: 2
+ )
+ )
+ XCTAssertGreaterThanOrEqual(fine.count, coarse.count)
+ XCTAssertEqual(fine.count, source.count)
+ XCTAssertEqual(coarse.first?.sourceIntervals.count, 2)
+ XCTAssertFalse(coarse.contains {
+ $0.start < base.addingTimeInterval(30)
+ && $0.end > base.addingTimeInterval(20)
+ })
+ XCTAssertFalse(coarse.contains {
+ $0.start < breakAt40.timestamp
+ && $0.end > breakAt40.timestamp
+ })
+ XCTAssertFalse(coarse.contains {
+ Set($0.sourceIntervals.map(\.method)).count > 1
+ })
+ XCTAssertFalse(coarse.contains {
+ $0.sourceIntervals.contains { $0.tokenDelta == 0 }
+ && $0.sourceIntervals.contains { $0.tokenDelta > 0 }
+ })
+ XCTAssertEqual(source.count, 7)
+ }
+
+ func testAggregatedTokenSelectionAndAccessibilityRemainTruthful() {
+ let range = DateInterval(
+ start: Date(timeIntervalSince1970: 1_000),
+ end: Date(timeIntervalSince1970: 2_000)
+ )
+ func sourceInterval(_ index: Int) -> AccountTokenActivityInterval {
+ AccountTokenActivityInterval(
+ start: range.start.addingTimeInterval(Double(index) * 100),
+ end: range.start.addingTimeInterval(Double(index + 1) * 100),
+ tokenDelta: Int64((index + 1) * 100),
+ method: .lifetimeDelta,
+ accountPartitionID: "account-a",
+ limitID: "weekly",
+ allowanceReset: range.end
+ )
+ }
+ let source = [0, 1, 2].map(sourceInterval)
+ let display = displayedAccountTokenIntervals(
+ source,
+ breaks: [],
+ maximumMarks: 1
+ )
+ let zoomedRange = DateInterval(
+ start: source[0].start,
+ end: source[1].end
+ )
+ let zoomed = displayedAccountTokenIntervals(
+ source.filter {
+ $0.start >= zoomedRange.start && $0.end <= zoomedRange.end
+ },
+ breaks: [],
+ maximumMarks: 3
+ )
+ let selected = accountTokenDisplayInterval(
+ at: range.start.addingTimeInterval(150),
+ in: display,
+ within: range
+ )
+
+ XCTAssertEqual(display.count, 1)
+ XCTAssertEqual(selected?.tokenDelta, 600)
+ XCTAssertEqual(selected?.sourceIntervals.count, 3)
+ XCTAssertEqual(zoomed.count, 2)
+ XCTAssertTrue(zoomed.allSatisfy { !$0.isAggregated })
+ XCTAssertEqual(source.count, 3)
+ XCTAssertEqual(
+ steppedAccountTokenDisplayInterval(
+ in: display,
+ from: nil,
+ by: 1
+ ),
+ selected
+ )
+ XCTAssertTrue(
+ accountTokenDisplayIntervalAccessibilityValue(selected!)
+ .contains("Combined from 3 observed periods")
+ )
+ XCTAssertNil(accountTokenDisplayInterval(
+ at: range.start.addingTimeInterval(500),
+ in: display,
+ within: range
+ ))
+
+ let zero = AccountTokenActivityDisplayInterval(
+ sourceIntervals: [AccountTokenActivityInterval(
+ start: range.start,
+ end: range.start.addingTimeInterval(100),
+ tokenDelta: 0,
+ method: .lifetimeDelta,
+ accountPartitionID: "account-a",
+ limitID: "weekly",
+ allowanceReset: range.end
+ )]
+ )
+ XCTAssertTrue(
+ accountTokenDisplayIntervalAccessibilityValue(zero)
+ .contains("Observed zero-token interval")
+ )
+ }
+
+ func testRollingPresetsEndAtInjectedNowDespiteStaleObservation() throws {
+ let now = try date("2026-08-03T09:08:00Z")
+ let latestObserved = now.addingTimeInterval(-6 * 3_600)
+ let bounds = DateInterval(
+ start: now.addingTimeInterval(-12 * 3_600),
+ end: latestObserved
+ )
+
+ for (range, duration) in [
+ (AnalyticsTimeRange.oneDay, 86_400.0),
+ (.threeDays, 259_200.0),
+ (.fourWeeks, 2_419_200.0),
+ (.twelveWeeks, 7_257_600.0)
+ ] {
+ let resolved = range.interval(within: bounds, now: now)
+ XCTAssertEqual(resolved.start, now.addingTimeInterval(-duration))
+ XCTAssertEqual(resolved.end, now)
+ XCTAssertEqual(resolved.duration, duration)
+ }
+ XCTAssertNotEqual(
+ AnalyticsTimeRange.oneDay.interval(within: bounds, now: now).end,
+ latestObserved
+ )
XCTAssertEqual(
AnalyticsTimeRange.currentWindow.interval(
within: bounds,
- endingAt: latestObserved
+ now: now
),
bounds
)
}
+ func testRollingPresetDatesKeepElapsedDurationAcrossTimezonesAndDST() throws {
+ let berlin = try XCTUnwrap(TimeZone(identifier: "Europe/Berlin"))
+ let losAngeles = try XCTUnwrap(
+ TimeZone(identifier: "America/Los_Angeles")
+ )
+
+ for now in [
+ try date("2026-03-29T10:08:00Z"),
+ try date("2026-10-25T11:08:00Z")
+ ] {
+ let interval = AnalyticsTimeRange.oneDay.interval(
+ within: DateInterval(
+ start: now.addingTimeInterval(-2 * 86_400),
+ end: now.addingTimeInterval(3_600)
+ ),
+ now: now
+ )
+ XCTAssertEqual(interval.start, now.addingTimeInterval(-86_400))
+ XCTAssertEqual(interval.end, now)
+ XCTAssertEqual(interval.duration, 86_400)
+ XCTAssertNotEqual(
+ boundaryLabel(interval.start, timeZone: berlin),
+ boundaryLabel(interval.end, timeZone: berlin)
+ )
+ }
+
+ let now = try date("2026-08-03T09:08:00Z")
+ let interval = AnalyticsTimeRange.oneDay.interval(
+ within: DateInterval(
+ start: now.addingTimeInterval(-2 * 86_400),
+ end: now.addingTimeInterval(3_600)
+ ),
+ now: now
+ )
+ XCTAssertEqual(interval.end, now)
+ XCTAssertEqual(interval.duration, 86_400)
+ XCTAssertNotEqual(
+ boundaryLabel(interval.end, timeZone: berlin),
+ boundaryLabel(interval.end, timeZone: losAngeles)
+ )
+ }
+
func testSelectedRangeIsClampedAndRejectsTinySelections() {
let bounds = DateInterval(
start: Date(timeIntervalSince1970: 1_000),
@@ -334,6 +851,189 @@ final class AnalyticsWorkspaceTests: XCTestCase {
)
}
+ func testSelectedRangeStaysFixedAfterRefresh() throws {
+ let store = AnalyticsWorkspaceStore(
+ defaults: UserDefaults(
+ suiteName: "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ )!
+ )
+ let chosen = DateInterval(
+ start: try date("2026-08-02T08:15:00Z"),
+ end: try date("2026-08-02T14:45:00Z")
+ )
+ store.selectVisibleRange(
+ chosen,
+ within: DateInterval(
+ start: try date("2026-08-01T00:00:00Z"),
+ end: try date("2026-08-03T00:00:00Z")
+ )
+ )
+
+ XCTAssertEqual(
+ store.effectiveRange(
+ within: DateInterval(
+ start: try date("2026-08-01T00:00:00Z"),
+ end: try date("2026-08-04T00:00:00Z")
+ ),
+ now: try date("2026-08-03T12:00:00Z")
+ ),
+ chosen
+ )
+ }
+
+ func testSelectedRangeIgnoresAdvancingNowAndPresetsClearIt() {
+ let store = AnalyticsWorkspaceStore(
+ defaults: UserDefaults(
+ suiteName: "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ )!
+ )
+ let bounds = DateInterval(
+ start: Date(timeIntervalSince1970: 0),
+ end: Date(timeIntervalSince1970: 20_000)
+ )
+ let chosen = DateInterval(
+ start: Date(timeIntervalSince1970: 2_000),
+ end: Date(timeIntervalSince1970: 8_000)
+ )
+ store.selectVisibleRange(chosen, within: bounds)
+
+ XCTAssertEqual(
+ store.effectiveRange(
+ within: bounds,
+ now: Date(timeIntervalSince1970: 15_000)
+ ),
+ chosen
+ )
+
+ store.selectTimeRange(.oneDay)
+ XCTAssertNil(store.state.visibleRange)
+ store.selectVisibleRange(chosen, within: bounds)
+ store.resetVisibleRange()
+ XCTAssertEqual(store.state.timeRange, .currentWindow)
+ XCTAssertNil(store.state.visibleRange)
+ }
+
+ func testSelectedRangeRestoresExactBoundaries() {
+ let suite = "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ let defaults = UserDefaults(suiteName: suite)!
+ defer { defaults.removePersistentDomain(forName: suite) }
+ let chosen = DateInterval(
+ start: Date(timeIntervalSince1970: 2_000),
+ end: Date(timeIntervalSince1970: 8_000)
+ )
+ AnalyticsWorkspaceStore(defaults: defaults).selectVisibleRange(
+ chosen,
+ within: DateInterval(
+ start: Date(timeIntervalSince1970: 0),
+ end: Date(timeIntervalSince1970: 10_000)
+ )
+ )
+
+ let restored = AnalyticsWorkspaceStore(defaults: defaults)
+ XCTAssertEqual(restored.state.timeRange, .selected)
+ XCTAssertEqual(restored.state.visibleRange, chosen)
+ }
+
+ func testSelectedRangeClampsOneSideWithoutMovingTheOther() {
+ let store = AnalyticsWorkspaceStore(
+ defaults: UserDefaults(
+ suiteName: "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ )!
+ )
+ let chosen = DateInterval(
+ start: Date(timeIntervalSince1970: 2_000),
+ end: Date(timeIntervalSince1970: 8_000)
+ )
+ store.selectVisibleRange(
+ chosen,
+ within: DateInterval(
+ start: Date(timeIntervalSince1970: 0),
+ end: Date(timeIntervalSince1970: 10_000)
+ )
+ )
+
+ XCTAssertEqual(
+ store.effectiveRange(
+ within: DateInterval(
+ start: Date(timeIntervalSince1970: 4_000),
+ end: Date(timeIntervalSince1970: 10_000)
+ ),
+ now: .now
+ ),
+ DateInterval(
+ start: Date(timeIntervalSince1970: 4_000),
+ end: Date(timeIntervalSince1970: 8_000)
+ )
+ )
+ }
+
+ func testSelectedRangeUsesBoundsWhenItNoLongerIntersects() {
+ let store = AnalyticsWorkspaceStore(
+ defaults: UserDefaults(
+ suiteName: "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ )!
+ )
+ let chosen = DateInterval(
+ start: Date(timeIntervalSince1970: 2_000),
+ end: Date(timeIntervalSince1970: 8_000)
+ )
+ store.selectVisibleRange(
+ chosen,
+ within: DateInterval(
+ start: Date(timeIntervalSince1970: 0),
+ end: Date(timeIntervalSince1970: 10_000)
+ )
+ )
+ let refreshedBounds = DateInterval(
+ start: Date(timeIntervalSince1970: 10_000),
+ end: Date(timeIntervalSince1970: 20_000)
+ )
+
+ XCTAssertEqual(
+ store.effectiveRange(within: refreshedBounds, now: .now),
+ refreshedBounds
+ )
+ XCTAssertEqual(store.state.visibleRange, chosen)
+ }
+
+ func testZoomInAndOutContinueFromSelectedRange() {
+ let store = AnalyticsWorkspaceStore(
+ defaults: UserDefaults(
+ suiteName: "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ )!
+ )
+ let bounds = DateInterval(
+ start: Date(timeIntervalSince1970: 0),
+ end: Date(timeIntervalSince1970: 10_000)
+ )
+ let chosen = DateInterval(
+ start: Date(timeIntervalSince1970: 2_000),
+ end: Date(timeIntervalSince1970: 8_000)
+ )
+ let anchor = Date(timeIntervalSince1970: 5_000)
+ store.selectVisibleRange(chosen, within: bounds)
+ store.zoom(
+ factor: 2,
+ anchor: anchor,
+ currentRange: store.effectiveRange(within: bounds, now: .now),
+ within: bounds
+ )
+
+ let zoomed = DateInterval(
+ start: Date(timeIntervalSince1970: 3_500),
+ end: Date(timeIntervalSince1970: 6_500)
+ )
+ XCTAssertEqual(store.state.visibleRange, zoomed)
+
+ store.zoom(
+ factor: 0.5,
+ anchor: anchor,
+ currentRange: store.effectiveRange(within: bounds, now: .now),
+ within: bounds
+ )
+ XCTAssertEqual(store.state.visibleRange, chosen)
+ }
+
func testZoomKeepsRangeInsideWindowAndAroundAnchor() {
let bounds = DateInterval(
start: Date(timeIntervalSince1970: 0),
@@ -369,11 +1069,11 @@ final class AnalyticsWorkspaceTests: XCTestCase {
start: Date(timeIntervalSince1970: 0),
end: Date(timeIntervalSince1970: 200_000)
)
- let latestObserved = Date(timeIntervalSince1970: 150_000)
+ let now = Date(timeIntervalSince1970: 150_000)
store.selectTimeRange(.oneDay)
let visible = store.effectiveRange(
within: bounds,
- endingAt: latestObserved
+ now: now
)
store.zoom(
@@ -604,7 +1304,7 @@ final class AnalyticsWorkspaceTests: XCTestCase {
store.selectTimeRange(.fourWeeks)
XCTAssertEqual(
- store.effectiveRange(within: bounds, endingAt: observedAt),
+ store.effectiveRange(within: bounds, now: observedAt),
DateInterval(
start: observedAt.addingTimeInterval(-28 * 86_400),
end: observedAt
@@ -613,7 +1313,7 @@ final class AnalyticsWorkspaceTests: XCTestCase {
store.selectTimeRange(.twelveWeeks)
XCTAssertEqual(
- store.effectiveRange(within: bounds, endingAt: observedAt),
+ store.effectiveRange(within: bounds, now: observedAt),
DateInterval(start: oldest, end: observedAt)
)
}
@@ -936,6 +1636,71 @@ final class AnalyticsWorkspaceTests: XCTestCase {
}
}
+ func testExpiredCurrentWindowRendersUnavailableAndHistoricalUsage() throws {
+ let start = try date("2026-08-01T12:13:00Z")
+ let reset = try date("2026-08-08T12:13:00Z")
+ let observedAt = try date("2026-08-03T12:13:00Z")
+ let reader = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: UsageSnapshot(
+ mainLimit: LimitReading(
+ limitId: "weekly",
+ name: "Weekly",
+ window: UsageWindow(
+ remainingPercent: 70,
+ resetsAt: reset,
+ durationMinutes: 10_080
+ )
+ ),
+ otherLimits: [],
+ tokenHistory: [],
+ emergencyResetCount: 0,
+ fetchedAt: observedAt
+ ),
+ samples: [
+ UsageSample(
+ observedAt: start,
+ remainingPercent: 100,
+ resetsAt: reset
+ )
+ ],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: try date("2026-08-09T12:13:00Z"),
+ previousStatus: nil
+ )
+ )
+ let store = AnalyticsWorkspaceStore(
+ defaults: UserDefaults(
+ suiteName: "AnalyticsWorkspaceTests-\(UUID().uuidString)"
+ )!
+ )
+
+ XCTAssertNil(reader.weeklyUsageRemaining)
+ XCTAssertTrue(
+ renders(
+ AnalyticsWorkspaceBody(
+ reader: reader,
+ store: store,
+ assistedInsights: CodexAssistedInsightStore()
+ ),
+ size: CGSize(width: 640, height: 780)
+ )
+ )
+
+ store.selectTimeRange(.fourWeeks)
+ XCTAssertTrue(
+ renders(
+ AnalyticsWorkspaceBody(
+ reader: reader,
+ store: store,
+ assistedInsights: CodexAssistedInsightStore()
+ ),
+ size: CGSize(width: 640, height: 780)
+ )
+ )
+ }
+
func testUsagePerTokenComparisonRendersAtSmallAndLargeSizes() {
let suiteName =
"AnalyticsWorkspaceTests-usage-per-token-\(UUID().uuidString)"
@@ -978,6 +1743,22 @@ final class AnalyticsWorkspaceTests: XCTestCase {
}
}
+ private func date(_ value: String) throws -> Date {
+ try XCTUnwrap(ISO8601DateFormatter().date(from: value))
+ }
+
+ private func boundaryLabel(
+ _ date: Date,
+ timeZone: TimeZone
+ ) -> String {
+ let formatter = DateFormatter()
+ formatter.locale = Locale(identifier: "en_US")
+ formatter.timeZone = timeZone
+ formatter.dateStyle = .medium
+ formatter.timeStyle = .short
+ return formatter.string(from: date)
+ }
+
private func renders(
_ view: Content,
size: CGSize
diff --git a/Tests/CodexLimitsTests/DeterministicInsightTests.swift b/Tests/CodexLimitsTests/DeterministicInsightTests.swift
index 4032c3a..2d99cc9 100644
--- a/Tests/CodexLimitsTests/DeterministicInsightTests.swift
+++ b/Tests/CodexLimitsTests/DeterministicInsightTests.swift
@@ -113,7 +113,7 @@ final class DeterministicInsightTests: XCTestCase {
(AnalyticsTimeRange.oneDay, 86_400.0),
(.threeDays, 3 * 86_400.0),
(.fourWeeks, 28 * 86_400.0),
- (.twelveWeeks, 35 * 86_400.0)
+ (.twelveWeeks, 84 * 86_400.0)
] {
var exploration = AnalyticsExplorationState.initial
exploration.timeRange = range
@@ -121,18 +121,58 @@ final class DeterministicInsightTests: XCTestCase {
DeterministicInsightInput.effectiveRange(
usagePerToken: usage,
observedInterval: nil,
- fetchedAt: current.interval.end,
+ now: current.interval.end,
exploration: exploration
)
)
XCTAssertEqual(
resolved.duration,
- min(duration, 35 * 86_400),
+ duration,
accuracy: 0.1
)
}
}
+ func testPresetInsightRangeEndsAtNowWhenEvidenceIsStale() throws {
+ let usage = insightInput(multiplier: 1.4).usagePerToken
+ let latestObserved = try XCTUnwrap(usage.current?.interval.end)
+ let now = latestObserved.addingTimeInterval(6 * 3_600)
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .oneDay
+
+ let resolved = try XCTUnwrap(
+ DeterministicInsightInput.effectiveRange(
+ usagePerToken: usage,
+ observedInterval: nil,
+ now: now,
+ exploration: exploration
+ )
+ )
+
+ XCTAssertEqual(resolved.start, now.addingTimeInterval(-86_400))
+ XCTAssertEqual(resolved.end, now)
+ }
+
+ func testSelectedInsightRangeKeepsEmptyTimeAfterStaleEvidence() throws {
+ let usage = insightInput(multiplier: 1.4).usagePerToken
+ let latestObserved = try XCTUnwrap(usage.current?.interval.end)
+ let now = latestObserved.addingTimeInterval(6 * 3_600)
+ let selected = DateInterval(start: latestObserved, end: now)
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .selected
+ exploration.visibleRange = selected
+
+ XCTAssertEqual(
+ DeterministicInsightInput.effectiveRange(
+ usagePerToken: usage,
+ observedInterval: nil,
+ now: now,
+ exploration: exploration
+ ),
+ selected
+ )
+ }
+
func testStaleLowCoverageAndUnavailableSourceWithholdConclusion() {
let stale = insightSnapshot(
multiplier: 1.4,
diff --git a/Tests/CodexLimitsTests/UsageHistoryTests.swift b/Tests/CodexLimitsTests/UsageHistoryTests.swift
index 43bb09d..444dc16 100644
--- a/Tests/CodexLimitsTests/UsageHistoryTests.swift
+++ b/Tests/CodexLimitsTests/UsageHistoryTests.swift
@@ -108,7 +108,33 @@ final class UsageHistoryTests: XCTestCase {
XCTAssertEqual(state.samples.first?.comparisonBreak, true)
}
- func testNegativeLifetimeTokenReadingIsRejectedDuringNormalization() async throws {
+ func testConflictingLifetimeCountersKeepBothReadings() async throws {
+ let root = temporaryDirectory()
+ let observedAt = Date(timeIntervalSince1970: 1_900_000)
+ let resetsAt = Date(timeIntervalSince1970: 2_000_000)
+ let history = UsageHistory(
+ localDirectory: root,
+ installationID: "writer-a"
+ )
+ _ = await history.load()
+ _ = await history.record(UsageSample(
+ observedAt: observedAt,
+ remainingPercent: 80,
+ resetsAt: resetsAt,
+ lifetimeTokens: 1_200
+ ))
+
+ let state = await history.record(UsageSample(
+ observedAt: observedAt,
+ remainingPercent: 80,
+ resetsAt: resetsAt,
+ lifetimeTokens: 1_500
+ ))
+
+ XCTAssertEqual(state.samples.map(\.lifetimeTokens), [1_200, 1_500])
+ }
+
+ func testNegativeLifetimeTokenReadingIsPreservedAsABreak() async throws {
let root = temporaryDirectory()
let history = UsageHistory(
localDirectory: root,
@@ -125,7 +151,7 @@ final class UsageHistoryTests: XCTestCase {
)
)
- XCTAssertTrue(state.samples.isEmpty)
+ XCTAssertEqual(state.samples.map(\.lifetimeTokens), [-1])
}
func testDailyFileRestoreKeepsValidSamplesBesideAnInvalidSample() async throws {
@@ -1556,12 +1582,14 @@ final class UsageHistoryTests: XCTestCase {
let firstSample = UsageSample(
observedAt: Date(timeIntervalSince1970: 1_900_000),
remainingPercent: 82,
- resetsAt: reset
+ resetsAt: reset,
+ lifetimeTokens: 1_000
)
let secondSample = UsageSample(
observedAt: Date(timeIntervalSince1970: 1_900_060),
remainingPercent: 81,
- resetsAt: reset
+ resetsAt: reset,
+ lifetimeTokens: 1_200
)
_ = await firstWriter.load()
@@ -1582,6 +1610,10 @@ final class UsageHistoryTests: XCTestCase {
XCTAssertEqual(firstState.samples, [firstSample, secondSample])
XCTAssertEqual(secondState.samples, [firstSample, secondSample])
+ XCTAssertEqual(
+ firstState.samples.compactMap(\.lifetimeTokens),
+ [1_000, 1_200]
+ )
XCTAssertNil(firstState.errorMessage)
XCTAssertNil(secondState.errorMessage)
}
diff --git a/Tests/CodexLimitsTests/UsageIntelligenceEngineTests.swift b/Tests/CodexLimitsTests/UsageIntelligenceEngineTests.swift
index 01b9c1e..7b2790c 100644
--- a/Tests/CodexLimitsTests/UsageIntelligenceEngineTests.swift
+++ b/Tests/CodexLimitsTests/UsageIntelligenceEngineTests.swift
@@ -73,14 +73,929 @@ final class UsageIntelligenceEngineTests: XCTestCase {
)
)
- XCTAssertEqual(reader.accountTokenActivity.state, .exact)
+ XCTAssertEqual(reader.accountTokenActivity.state, .partial)
XCTAssertEqual(reader.accountTokenActivity.tokens, 600)
XCTAssertEqual(reader.accountTokenActivity.method, .lifetimeDelta)
XCTAssertEqual(
reader.accountTokenActivity.interval,
DateInterval(start: boundary.observedAt, end: now)
)
- XCTAssertNil(reader.accountTokenActivity.reason)
+ XCTAssertNil(reader.accountTokenActivity.reason)
+ }
+
+ func testRollingDayUsesContainedLifetimeIntervals() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let readings = [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-6 * 3_600),
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: now.addingTimeInterval(-4 * 3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_500
+ )
+ ]
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .oneDay
+
+ let activity = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: readings,
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ accountPartitionID: "account-a",
+ analyticsExploration: exploration
+ )
+ ).accountTokenActivity
+
+ XCTAssertEqual(
+ activity.range,
+ DateInterval(
+ start: now.addingTimeInterval(-86_400),
+ end: now
+ )
+ )
+ XCTAssertEqual(activity.tokens, 500)
+ XCTAssertEqual(activity.method, .lifetimeDelta)
+ XCTAssertEqual(activity.intervals.map(\.tokenDelta), [200, 300])
+ XCTAssertEqual(
+ activity.interval,
+ DateInterval(
+ start: readings[0].observedAt,
+ end: readings[2].observedAt
+ )
+ )
+ XCTAssertNil(activity.reason)
+ }
+
+ func testRollingDayStaysAtNowWhenLatestReadingIsStale() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let observedAt = now.addingTimeInterval(-6 * 3_600)
+ let account = makeSnapshot(remaining: 80, fetchedAt: observedAt)
+ let reset = account.mainLimit!.window.resetsAt
+ let readings = [
+ UsageSample(
+ observedAt: observedAt.addingTimeInterval(-4 * 3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: observedAt,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ]
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .oneDay
+
+ let activity = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: readings,
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ analyticsExploration: exploration
+ )
+ ).accountTokenActivity
+
+ XCTAssertEqual(activity.range?.end, now)
+ XCTAssertEqual(activity.range?.duration, 86_400)
+ XCTAssertEqual(activity.tokens, 300)
+ XCTAssertEqual(activity.intervals.last?.end, observedAt)
+ }
+
+ func testRollingDayExcludesCrossingIntervalsAndIncludesExactBoundaries() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let start = now.addingTimeInterval(-86_400)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let readings = [
+ UsageSample(
+ observedAt: start.addingTimeInterval(-60),
+ remainingPercent: 95,
+ resetsAt: reset,
+ lifetimeTokens: 900
+ ),
+ UsageSample(
+ observedAt: start,
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: start.addingTimeInterval(3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_500
+ )
+ ]
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .oneDay
+
+ let activity = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: readings,
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ analyticsExploration: exploration
+ )
+ ).accountTokenActivity
+
+ XCTAssertEqual(activity.tokens, 500)
+ XCTAssertEqual(activity.intervals.count, 2)
+ XCTAssertEqual(activity.intervals.first?.start, start)
+ XCTAssertEqual(activity.intervals.last?.end, now)
+ }
+
+ func testRollingDayKeepsLongAndZeroIntervalsFactual() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let readings = [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-23 * 3_600),
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ )
+ ]
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .oneDay
+
+ let activity = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: readings,
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ analyticsExploration: exploration
+ )
+ ).accountTokenActivity
+
+ XCTAssertEqual(activity.tokens, 0)
+ XCTAssertEqual(activity.intervals.count, 1)
+ XCTAssertEqual(activity.intervals.first?.tokenDelta, 0)
+ XCTAssertEqual(activity.intervals.first?.duration, 22 * 3_600)
+ }
+
+ func testRollingDayNamesMissingCompleteIntervals() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let onlyReading = UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ )
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .oneDay
+
+ let beforeRange = [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-30 * 3_600),
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 800
+ ),
+ UsageSample(
+ observedAt: now.addingTimeInterval(-25 * 3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 900
+ )
+ ]
+ let crossingEnd = [
+ onlyReading,
+ UsageSample(
+ observedAt: now.addingTimeInterval(60),
+ remainingPercent: 75,
+ resetsAt: reset,
+ lifetimeTokens: 1_100
+ )
+ ]
+
+ for samples in [[onlyReading], beforeRange, crossingEnd] {
+ let activity = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: samples,
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ analyticsExploration: exploration
+ )
+ ).accountTokenActivity
+
+ XCTAssertNil(activity.tokens)
+ XCTAssertTrue(activity.intervals.isEmpty)
+ XCTAssertEqual(
+ activity.reason,
+ "No account readings in this range"
+ )
+ }
+ }
+
+ func testRollingDayMergesInterleavedReadingsWithoutDoubleCounting() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let start = now.addingTimeInterval(-6 * 3_600)
+ let first = UsageSample(
+ observedAt: start,
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ )
+ let middle = UsageSample(
+ observedAt: start.addingTimeInterval(2 * 3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ )
+ let sameCounter = UsageSample(
+ observedAt: start.addingTimeInterval(4 * 3_600),
+ remainingPercent: 83,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ )
+ let last = UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_600
+ )
+ let singleMac = rollingDayActivity(
+ account: account,
+ samples: [first, last],
+ now: now
+ )
+ let merged = rollingDayActivity(
+ account: account,
+ samples: [first, middle, middle, sameCounter, last],
+ now: now
+ )
+ let reversed = rollingDayActivity(
+ account: account,
+ samples: [first, middle, middle, sameCounter, last].reversed(),
+ now: now
+ )
+
+ XCTAssertEqual(singleMac.tokens, 600)
+ XCTAssertEqual(singleMac.intervals.map(\.tokenDelta), [600])
+ XCTAssertEqual(merged.tokens, singleMac.tokens)
+ XCTAssertEqual(merged.intervals.map(\.tokenDelta), [200, 0, 400])
+ XCTAssertEqual(merged, reversed)
+ }
+
+ func testRollingDayOrdersEqualTimestampsDeterministically() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let readings = [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3 * 3_600),
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: now.addingTimeInterval(-2 * 3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_100
+ ),
+ UsageSample(
+ observedAt: now.addingTimeInterval(-2 * 3_600),
+ remainingPercent: 84,
+ resetsAt: reset,
+ lifetimeTokens: 1_150
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ]
+
+ let ordered = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now
+ )
+ XCTAssertEqual(
+ ordered,
+ rollingDayActivity(
+ account: account,
+ samples: readings.reversed(),
+ now: now
+ )
+ )
+ XCTAssertEqual(
+ ordered.breaks.map(\.reason),
+ [.conflictingObservation]
+ )
+ XCTAssertTrue(ordered.intervals.isEmpty)
+ }
+
+ func testRollingDayDoesNotJoinAcrossAllowanceResetsOrAccounts() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let start = now.addingTimeInterval(-4 * 3_600)
+ let readings = [
+ UsageSample(
+ observedAt: start,
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: start.addingTimeInterval(3_600),
+ remainingPercent: 88,
+ resetsAt: reset.addingTimeInterval(7 * 86_400),
+ lifetimeTokens: 1_100
+ ),
+ UsageSample(
+ observedAt: start.addingTimeInterval(2 * 3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ]
+ let firstAccount = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now,
+ accountPartitionID: "account-a"
+ )
+ let secondAccount = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now,
+ accountPartitionID: "account-b"
+ )
+
+ XCTAssertEqual(firstAccount.tokens, 100)
+ XCTAssertEqual(firstAccount.intervals.count, 1)
+ XCTAssertEqual(firstAccount.intervals.first?.start, readings[2].observedAt)
+ XCTAssertEqual(
+ firstAccount.breaks.map(\.reason),
+ [.allowanceWindowChange, .allowanceWindowChange]
+ )
+ XCTAssertEqual(
+ firstAccount.intervals.map(\.accountPartitionID),
+ ["account-a"]
+ )
+ XCTAssertEqual(
+ secondAccount.intervals.map(\.accountPartitionID),
+ ["account-b"]
+ )
+ }
+
+ func testRollingDayPreservesIntervalsOnBothSidesOfCounterDecrease() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let start = now.addingTimeInterval(-4 * 3_600)
+ let readings = [
+ UsageSample(
+ observedAt: start,
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: start.addingTimeInterval(3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_500
+ ),
+ UsageSample(
+ observedAt: start.addingTimeInterval(2 * 3_600),
+ remainingPercent: 83,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_400
+ )
+ ]
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now
+ )
+
+ XCTAssertEqual(activity.tokens, 700)
+ XCTAssertEqual(activity.intervals.map(\.tokenDelta), [500, 200])
+ XCTAssertEqual(
+ activity.breaks,
+ [AccountTokenActivityBreak(
+ timestamp: readings[2].observedAt,
+ reason: .counterDecrease
+ )]
+ )
+ XCTAssertTrue(activity.intervals.allSatisfy { $0.tokenDelta >= 0 })
+ XCTAssertFalse(activity.intervals.contains {
+ $0.start < readings[2].observedAt
+ && $0.end >= readings[2].observedAt
+ })
+ }
+
+ func testRollingDayBreaksAtInvalidAndCorrectionReadings() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let start = now.addingTimeInterval(-5 * 3_600)
+ let readings = [
+ UsageSample(
+ observedAt: start,
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: start.addingTimeInterval(3_600),
+ remainingPercent: 88,
+ resetsAt: reset,
+ lifetimeTokens: nil
+ ),
+ UsageSample(
+ observedAt: start.addingTimeInterval(2 * 3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ ),
+ UsageSample(
+ observedAt: start.addingTimeInterval(3 * 3_600),
+ remainingPercent: 83,
+ resetsAt: reset,
+ lifetimeTokens: 1_300,
+ comparisonBreak: true
+ ),
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 81,
+ resetsAt: reset,
+ lifetimeTokens: 1_400
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_500
+ )
+ ]
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now
+ )
+
+ XCTAssertEqual(activity.tokens, 200)
+ XCTAssertEqual(activity.intervals.map(\.tokenDelta), [100, 100])
+ XCTAssertEqual(
+ activity.breaks.map(\.reason),
+ [.invalidCounter, .correction]
+ )
+ }
+
+ func testRollingDayBreaksAtAccountObservationEpoch() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let epoch = now.addingTimeInterval(-2 * 3_600)
+ let readings = [
+ UsageSample(
+ observedAt: epoch.addingTimeInterval(-3_600),
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: epoch.addingTimeInterval(3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ]
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now,
+ accountEpochStartedAt: epoch
+ )
+
+ XCTAssertEqual(activity.tokens, 100)
+ XCTAssertEqual(activity.breaks.map(\.reason), [.accountChange])
+ XCTAssertEqual(activity.breaks.first?.timestamp, epoch)
+ }
+
+ func testRollingDayLifetimeIntervalsTakePrecedenceOverDailyBuckets() throws {
+ let now = try date("2026-08-03T00:00:00Z")
+ let bucketStart = try date("2026-08-02T00:00:00Z")
+ let account = makeSnapshot(
+ remaining: 80,
+ fetchedAt: now,
+ tokenHistory: [TokenDay(
+ date: bucketStart,
+ tokens: 900,
+ completeness: .complete
+ )]
+ )
+ let reset = account.mainLimit!.window.resetsAt
+ let readings = [
+ UsageSample(
+ observedAt: bucketStart.addingTimeInterval(6 * 3_600),
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: bucketStart.addingTimeInterval(12 * 3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ]
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now
+ )
+
+ XCTAssertEqual(activity.method, .lifetimeDelta)
+ XCTAssertEqual(activity.tokens, 300)
+ XCTAssertEqual(activity.intervals.count, 1)
+ XCTAssertEqual(activity.intervals.first?.method, .lifetimeDelta)
+ }
+
+ func testRollingDayFallsBackToACompleteUTCDailyBucket() throws {
+ let now = try date("2026-08-03T00:00:00Z")
+ let bucketStart = try date("2026-08-02T00:00:00Z")
+ let account = makeSnapshot(
+ remaining: 80,
+ fetchedAt: now,
+ tokenHistory: [TokenDay(
+ date: bucketStart,
+ tokens: 900,
+ completeness: .complete
+ )]
+ )
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: [],
+ now: now
+ )
+
+ XCTAssertEqual(activity.method, .dailyBuckets)
+ XCTAssertEqual(activity.tokens, 900)
+ XCTAssertEqual(activity.sourceDescription, "Codex UTC daily token totals")
+ XCTAssertEqual(
+ activity.interval,
+ DateInterval(start: bucketStart, end: now)
+ )
+ XCTAssertEqual(activity.intervals.map(\.method), [.dailyBuckets])
+ }
+
+ func testCompleteContiguousUTCDaysExactlyCoverAThreeDayRange() throws {
+ let now = try date("2026-08-04T00:00:00Z")
+ let account = makeSnapshot(
+ remaining: 80,
+ fetchedAt: now,
+ tokenHistory: [1, 2, 3].map { day in
+ TokenDay(
+ date: now.addingTimeInterval(TimeInterval(-day * 86_400)),
+ tokens: Int64(day * 100),
+ completeness: .complete
+ )
+ }
+ )
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: [],
+ now: now,
+ timeRange: .threeDays
+ )
+
+ XCTAssertEqual(activity.state, .exact)
+ XCTAssertEqual(activity.tokens, 600)
+ XCTAssertEqual(activity.intervals.count, 3)
+ }
+
+ func testUTCFallbackPreservesDetectedCounterBreaks() throws {
+ let now = try date("2026-08-03T00:00:00Z")
+ let account = makeSnapshot(
+ remaining: 80,
+ fetchedAt: now,
+ tokenHistory: [TokenDay(
+ date: now.addingTimeInterval(-86_400),
+ tokens: 900,
+ completeness: .complete
+ )]
+ )
+ let reset = account.mainLimit!.window.resetsAt
+ let readings = [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-7_200),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 2_000
+ ),
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ )
+ ]
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now
+ )
+
+ XCTAssertEqual(activity.method, .dailyBuckets)
+ XCTAssertEqual(activity.breaks.map(\.reason), [.counterDecrease])
+ }
+
+ func testRollingDayExcludesPartialUTCDailyBuckets() throws {
+ let now = try date("2026-08-03T12:00:00Z")
+ let account = makeSnapshot(
+ remaining: 80,
+ fetchedAt: now,
+ tokenHistory: [
+ TokenDay(
+ date: try date("2026-08-02T00:00:00Z"),
+ tokens: 800,
+ completeness: .complete
+ ),
+ TokenDay(
+ date: try date("2026-08-03T00:00:00Z"),
+ tokens: 900,
+ completeness: .complete
+ )
+ ]
+ )
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: [],
+ now: now
+ )
+
+ XCTAssertNil(activity.tokens)
+ XCTAssertTrue(activity.intervals.isEmpty)
+ XCTAssertEqual(activity.reason, "No account readings in this range")
+ }
+
+ func testEveryRollingTokenRangeUsesExactContainedIntervals() {
+ let now = Date(timeIntervalSince1970: 10_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let cases: [(AnalyticsTimeRange, TimeInterval)] = [
+ (.threeDays, 3 * 86_400),
+ (.fourWeeks, 28 * 86_400),
+ (.twelveWeeks, 84 * 86_400)
+ ]
+
+ for (timeRange, duration) in cases {
+ let rangeStart = now.addingTimeInterval(-duration)
+ let readings = [
+ UsageSample(
+ observedAt: rangeStart.addingTimeInterval(-60),
+ remainingPercent: 95,
+ resetsAt: reset,
+ lifetimeTokens: 900
+ ),
+ UsageSample(
+ observedAt: rangeStart,
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: rangeStart.addingTimeInterval(3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_600
+ )
+ ]
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now,
+ timeRange: timeRange
+ )
+
+ XCTAssertEqual(
+ activity.range,
+ DateInterval(start: rangeStart, end: now),
+ timeRange.rawValue
+ )
+ XCTAssertEqual(activity.range?.duration, duration, timeRange.rawValue)
+ XCTAssertEqual(activity.tokens, 600, timeRange.rawValue)
+ XCTAssertEqual(
+ activity.interval,
+ DateInterval(start: rangeStart, end: now),
+ timeRange.rawValue
+ )
+ XCTAssertEqual(activity.intervals.count, 2, timeRange.rawValue)
+ XCTAssertFalse(activity.intervals.contains {
+ $0.start < rangeStart
+ }, timeRange.rawValue)
+ XCTAssertFalse(activity.intervals.contains {
+ $0.end > now
+ }, timeRange.rawValue)
+ }
+ }
+
+ func testSelectedTokenRangeKeepsExactPersistedBoundariesAcrossRefresh() {
+ let now = Date(timeIntervalSince1970: 10_000_000)
+ let account = makeSnapshot(remaining: 80, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let selected = DateInterval(
+ start: now.addingTimeInterval(-10 * 86_400),
+ end: now.addingTimeInterval(-5 * 86_400)
+ )
+ let readings = [
+ UsageSample(
+ observedAt: selected.start.addingTimeInterval(-60),
+ remainingPercent: 95,
+ resetsAt: reset,
+ lifetimeTokens: 900
+ ),
+ UsageSample(
+ observedAt: selected.start,
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: selected.start.addingTimeInterval(3_600),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_200
+ ),
+ UsageSample(
+ observedAt: selected.end,
+ remainingPercent: 80,
+ resetsAt: reset,
+ lifetimeTokens: 1_500
+ ),
+ UsageSample(
+ observedAt: selected.end.addingTimeInterval(60),
+ remainingPercent: 79,
+ resetsAt: reset,
+ lifetimeTokens: 1_600
+ )
+ ]
+
+ let activity = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now,
+ timeRange: .selected,
+ visibleRange: selected
+ )
+ let refreshed = rollingDayActivity(
+ account: account,
+ samples: readings,
+ now: now.addingTimeInterval(86_400),
+ timeRange: .selected,
+ visibleRange: selected
+ )
+
+ XCTAssertEqual(activity.range, selected)
+ XCTAssertEqual(activity.tokens, 500)
+ XCTAssertEqual(activity.interval, selected)
+ XCTAssertEqual(activity.intervals.count, 2)
+ XCTAssertEqual(refreshed.range, selected)
+ XCTAssertEqual(refreshed.intervals, activity.intervals)
+ XCTAssertEqual(refreshed.tokens, activity.tokens)
+ }
+
+ func testTokenSourcePrecedenceIsEvaluatedInsideEachRange() throws {
+ let now = try date("2026-08-03T00:00:00Z")
+ let account = makeSnapshot(
+ remaining: 80,
+ fetchedAt: now,
+ tokenHistory: [
+ TokenDay(
+ date: try date("2026-08-01T00:00:00Z"),
+ tokens: 100,
+ completeness: .complete
+ ),
+ TokenDay(
+ date: try date("2026-08-02T00:00:00Z"),
+ tokens: 200,
+ completeness: .complete
+ )
+ ]
+ )
+ let reset = account.mainLimit!.window.resetsAt
+ let lifetimeReadings = [
+ UsageSample(
+ observedAt: try date("2026-07-23T00:00:00Z"),
+ remainingPercent: 90,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: try date("2026-07-24T00:00:00Z"),
+ remainingPercent: 85,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ]
+
+ let threeDays = rollingDayActivity(
+ account: account,
+ samples: lifetimeReadings,
+ now: now,
+ timeRange: .threeDays
+ )
+ let twelveWeeks = rollingDayActivity(
+ account: account,
+ samples: lifetimeReadings,
+ now: now,
+ timeRange: .twelveWeeks
+ )
+
+ XCTAssertEqual(threeDays.method, .dailyBuckets)
+ XCTAssertEqual(threeDays.tokens, 300)
+ XCTAssertEqual(threeDays.intervals.count, 2)
+ XCTAssertEqual(twelveWeeks.method, .lifetimeDelta)
+ XCTAssertEqual(twelveWeeks.tokens, 300)
+ XCTAssertEqual(twelveWeeks.intervals.count, 1)
}
func testReaderPublishesBoundedCurrentUsagePerTokenFacts() throws {
@@ -568,13 +1483,64 @@ final class UsageIntelligenceEngineTests: XCTestCase {
)
)
- XCTAssertEqual(reader.accountTokenActivity.state, .exact)
+ XCTAssertEqual(reader.accountTokenActivity.state, .partial)
XCTAssertEqual(
reader.accountTokenActivity.interval,
DateInterval(start: boundary.observedAt, end: observedAt)
)
}
+ func testPreservedLifetimeReadingDoesNotInheritANewerAllowanceReset() {
+ let now = Date(timeIntervalSince1970: 2_000_000)
+ let observedAt = now.addingTimeInterval(-6 * 86_400)
+ let oldReset = now.addingTimeInterval(-4 * 86_400)
+ let account = makeSnapshot(
+ remaining: 80,
+ fetchedAt: now,
+ accountFacts: AccountFacts(
+ lifetimeTokens: 1_200,
+ peakDailyTokens: nil,
+ longestRunningTurnSeconds: nil,
+ currentStreakDays: nil,
+ longestStreakDays: nil,
+ credits: nil,
+ spendControl: nil,
+ lifetimeTokensObservedAt: observedAt
+ )
+ )
+ let samples = [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-7 * 86_400),
+ remainingPercent: 90,
+ resetsAt: oldReset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: observedAt,
+ remainingPercent: 85,
+ resetsAt: oldReset,
+ lifetimeTokens: 1_200
+ )
+ ]
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .twelveWeeks
+
+ let activity = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: samples,
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ analyticsExploration: exploration
+ )
+ ).accountTokenActivity
+
+ XCTAssertEqual(activity.tokens, 200)
+ XCTAssertTrue(activity.breaks.isEmpty)
+ }
+
func testCompleteDailyBucketsProducePartialAccountTokenActivity() throws {
let fetchedAt = try XCTUnwrap(
ISO8601DateFormatter().date(from: "2026-07-28T12:00:00Z")
@@ -648,10 +1614,7 @@ final class UsageIntelligenceEngineTests: XCTestCase {
)
)
)
- XCTAssertEqual(
- reader.accountTokenActivity.reason,
- "Only complete daily token totals are available"
- )
+ XCTAssertNil(reader.accountTokenActivity.reason)
}
func testAlignedCompleteDailyBucketsProduceExactAccountTokenActivity() throws {
@@ -689,7 +1652,7 @@ final class UsageIntelligenceEngineTests: XCTestCase {
)
)
- XCTAssertEqual(reader.accountTokenActivity.state, .exact)
+ XCTAssertEqual(reader.accountTokenActivity.state, .partial)
XCTAssertEqual(reader.accountTokenActivity.tokens, 500)
XCTAssertEqual(reader.accountTokenActivity.method, .dailyBuckets)
XCTAssertEqual(
@@ -738,8 +1701,8 @@ final class UsageIntelligenceEngineTests: XCTestCase {
XCTAssertEqual(reader.accountTokenActivity.state, .unavailable)
XCTAssertNil(reader.accountTokenActivity.tokens)
XCTAssertEqual(
- reader.accountTokenActivity.reason,
- "Lifetime token counter decreased"
+ reader.accountTokenActivity.breaks.map(\.reason),
+ [.counterDecrease]
)
}
@@ -779,8 +1742,8 @@ final class UsageIntelligenceEngineTests: XCTestCase {
XCTAssertEqual(reader.accountTokenActivity.state, .unavailable)
XCTAssertNil(reader.accountTokenActivity.tokens)
XCTAssertEqual(
- reader.accountTokenActivity.reason,
- "Lifetime token reading is invalid"
+ reader.accountTokenActivity.breaks.map(\.reason),
+ [.invalidCounter]
)
}
@@ -818,7 +1781,10 @@ final class UsageIntelligenceEngineTests: XCTestCase {
reader.guidance?.message,
"At this pace, your limit may run out 23 hours early."
)
- XCTAssertEqual(reader.guidance?.suggestedPace, "Up to 8.5% a day")
+ XCTAssertEqual(
+ reader.guidance?.suggestedPace,
+ "Up to 8.5 percentage points a day"
+ )
XCTAssertNotNil(reader.guidance?.runway)
XCTAssertNil(reader.guidance?.remainingAtResetRange)
XCTAssertNil(reader.guidance?.caveat)
@@ -924,12 +1890,19 @@ final class UsageIntelligenceEngineTests: XCTestCase {
XCTAssertNil(reader.guidance)
XCTAssertTrue(reader.chart.currentProjection.isEmpty)
XCTAssertTrue(reader.chart.historicalProjection.isEmpty)
- XCTAssertEqual(reader.guidanceTitle, "Not enough data")
+ XCTAssertEqual(
+ reader.guidanceTitle,
+ "Couldn’t read Codex usage. Try refreshing again."
+ )
XCTAssertEqual(
reader.guidanceMessage,
"Couldn’t read Codex usage. Try refreshing again."
)
- XCTAssertEqual(reader.suggestedPaceText, "Not enough data")
+ XCTAssertNil(reader.suggestedPacePercentPerDay)
+ XCTAssertEqual(
+ reader.suggestedPaceText,
+ "Couldn’t read Codex usage. Try refreshing again."
+ )
}
func testReaderFormatsFreshnessFromSuppliedTime() {
@@ -976,7 +1949,7 @@ final class UsageIntelligenceEngineTests: XCTestCase {
XCTAssertEqual(reader.evidence.coverage, .low)
XCTAssertEqual(reader.evidence.confidence, .low)
XCTAssertEqual(reader.evidence.reason, "Less than half of this weekly window is observed")
- XCTAssertNil(reader.guidance)
+ XCTAssertNotNil(reader.guidance)
XCTAssertFalse(reader.chart.currentProjection.isEmpty)
}
@@ -1029,7 +2002,7 @@ final class UsageIntelligenceEngineTests: XCTestCase {
)
XCTAssertEqual(reader.evidence.confidence, .low)
- XCTAssertNil(reader.guidance)
+ XCTAssertNotNil(reader.guidance)
XCTAssertEqual(reader.chart.currentProjection.count, 2)
XCTAssertEqual(reader.chart.historicalProjection.count, 2)
XCTAssertTrue(reader.chart.estimatedBackfill.isEmpty)
@@ -1170,7 +2143,7 @@ final class UsageIntelligenceEngineTests: XCTestCase {
)
}
- func testUnknownCorrectionWithholdsGuidance() {
+ func testCorrectionRestoresGuidanceAfterSecondReading() {
let now = Date(timeIntervalSince1970: 5_900_000)
let account = makeSnapshot(remaining: 70, fetchedAt: now)
var samples = denseSamples(
@@ -1200,7 +2173,7 @@ final class UsageIntelligenceEngineTests: XCTestCase {
XCTAssertEqual(reader.evidence.coverage, .low)
XCTAssertEqual(reader.evidence.confidence, .low)
XCTAssertEqual(reader.evidence.reason, "Unknown reset or correction")
- XCTAssertNil(reader.guidance)
+ XCTAssertNotNil(reader.guidance)
XCTAssertEqual(reader.chart.observedSegments.count, 2)
XCTAssertEqual(reader.chart.observedSegments.flatMap { $0 }, reader.chart.observed)
}
@@ -1304,7 +2277,10 @@ final class UsageIntelligenceEngineTests: XCTestCase {
XCTAssertNil(reader.weeklyUsageRemaining)
XCTAssertEqual(reader.evidence.coverage, .unavailable)
- XCTAssertEqual(reader.evidence.reason, "Weekly usage unavailable")
+ XCTAssertEqual(
+ reader.evidence.reason,
+ "No current weekly allowance reading"
+ )
XCTAssertNil(reader.guidance)
XCTAssertTrue(reader.chart.observed.isEmpty)
}
@@ -1429,14 +2405,315 @@ final class UsageIntelligenceEngineTests: XCTestCase {
account: account,
samples: [
UsageSample(
- observedAt: beforeEpoch,
+ observedAt: beforeEpoch,
+ remainingPercent: 80,
+ resetsAt: reset
+ ),
+ UsageSample(
+ observedAt: afterEpoch,
+ remainingPercent: 72,
+ resetsAt: reset
+ )
+ ],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ accountEpochStartedAt: now.addingTimeInterval(-3_600)
+ )
+ )
+
+ XCTAssertTrue(
+ reader.chart.observed.contains { $0.date == beforeEpoch }
+ )
+ }
+
+ func testSparseHistoryWithholdsGuidance() {
+ let now = Date(timeIntervalSince1970: 6_000_000)
+ let reader = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: makeSnapshot(remaining: 55, fetchedAt: now),
+ samples: [],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil
+ )
+ )
+
+ XCTAssertEqual(reader.evidence.coverage, .low)
+ XCTAssertEqual(reader.evidence.confidence, .low)
+ XCTAssertEqual(reader.evidence.reason, "Not enough account history")
+ XCTAssertNil(reader.guidance)
+ XCTAssertEqual(
+ reader.guidanceTitle,
+ "At least two compatible account readings are needed"
+ )
+ }
+
+ func testRecentMovementUsesTwoReadingsFromLessThanTwentyFourHours() throws {
+ let now = Date(timeIntervalSince1970: 6_005_000)
+ let account = makeSnapshot(remaining: 70, fetchedAt: now)
+ let first = UsageSample(
+ observedAt: now.addingTimeInterval(-6 * 3_600),
+ remainingPercent: 75,
+ resetsAt: account.mainLimit!.window.resetsAt
+ )
+
+ let reader = makeReader(account: account, samples: [first], now: now)
+ let forecast = try XCTUnwrap(reader.guidance?.forecast)
+
+ XCTAssertEqual(forecast.currentPercentPerDay, 20, accuracy: 0.000_001)
+ XCTAssertEqual(reader.chart.currentProjection.first?.date, now)
+ XCTAssertEqual(reader.chart.currentProjection.first?.remaining, 70)
+ }
+
+ func testRecentMovementUsesExactRollingDayAndExcludesOlderReadings() throws {
+ let now = Date(timeIntervalSince1970: 6_006_000)
+ let account = makeSnapshot(remaining: 70, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let boundary = UsageSample(
+ observedAt: now.addingTimeInterval(-86_400),
+ remainingPercent: 90,
+ resetsAt: reset
+ )
+ let reader = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-25 * 3_600),
+ remainingPercent: 100,
+ resetsAt: reset
+ ),
+ boundary
+ ],
+ now: now
+ )
+ let forecast = try XCTUnwrap(reader.guidance?.forecast)
+
+ XCTAssertEqual(forecast.currentPercentPerDay, 20, accuracy: 0.000_001)
+ }
+
+ func testLongCompatibleIntervalIsFactualCurrentMovement() throws {
+ let now = Date(timeIntervalSince1970: 6_007_000)
+ let account = makeSnapshot(remaining: 70, fetchedAt: now)
+ let gap = 8 * 3_600 + 36 * 60
+ let reader = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-Double(gap)),
+ remainingPercent: 80,
+ resetsAt: account.mainLimit!.window.resetsAt
+ )
+ ],
+ now: now
+ )
+ let forecast = try XCTUnwrap(reader.guidance?.forecast)
+
+ XCTAssertEqual(
+ forecast.currentPercentPerDay,
+ 10 / (Double(gap) / 86_400),
+ accuracy: 0.000_001
+ )
+ XCTAssertNotNil(reader.guidance)
+ XCTAssertEqual(reader.chart.currentProjection.count, 2)
+ }
+
+ func testZeroRecentMovementLastsThroughResetAndProjectsFlat() throws {
+ let now = Date(timeIntervalSince1970: 6_008_000)
+ let account = makeSnapshot(remaining: 50, fetchedAt: now)
+ let reader = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 50,
+ resetsAt: account.mainLimit!.window.resetsAt
+ )
+ ],
+ now: now
+ )
+
+ XCTAssertEqual(reader.guidance?.runway, .throughReset)
+ XCTAssertEqual(reader.runwayText, "Limit should last through reset")
+ XCTAssertEqual(reader.guidance?.forecast.currentPercentPerDay, 0)
+ XCTAssertEqual(
+ reader.chart.currentProjection.last,
+ UsageChartPoint(
+ date: account.mainLimit!.window.resetsAt,
+ remaining: 50
+ )
+ )
+ }
+
+ func testZeroRecentMovementKeepsPrimaryGuidanceConsistentWithFastHistory() {
+ let day: TimeInterval = 86_400
+ let now = Date(timeIntervalSince1970: 6_008_500)
+ let account = makeSnapshot(
+ remaining: 50,
+ fetchedAt: now,
+ tokenHistory: (-33 ... -6).map {
+ TokenDay(
+ date: now.addingTimeInterval(Double($0) * day),
+ tokens: 400
+ )
+ } + (-5 ... -1).map {
+ TokenDay(
+ date: now.addingTimeInterval(Double($0) * day),
+ tokens: 100
+ )
+ }
+ )
+ let reader = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 50,
+ resetsAt: account.mainLimit!.window.resetsAt
+ )
+ ],
+ now: now
+ )
+
+ XCTAssertEqual(reader.guidance?.forecast.currentPercentPerDay, 0)
+ XCTAssertEqual(reader.guidance?.status, .roomToUseMore)
+ XCTAssertEqual(reader.guidance?.runway, .throughReset)
+ XCTAssertEqual(
+ reader.chart.currentProjection.last?.remaining,
+ 50
+ )
+ }
+
+ func testPastEstimateRemainsWhenCurrentMovementNeedsAnotherReading() {
+ let now = Date(timeIntervalSince1970: 6_008_750)
+ let account = makeSnapshot(remaining: 70, fetchedAt: now)
+ let priorReset = account.mainLimit!.window.startsAt
+ let history = weeklySamples(
+ start: priorReset.addingTimeInterval(-12 * 3_600),
+ end: priorReset,
+ reset: priorReset,
+ startRemaining: 80,
+ endRemaining: 60,
+ startTokens: 1_000,
+ endTokens: 2_000
+ )
+ let reader = makeReader(account: account, samples: history, now: now)
+
+ XCTAssertNil(reader.guidance)
+ XCTAssertTrue(reader.chart.currentProjection.isEmpty)
+ XCTAssertEqual(reader.chart.historicalProjection.count, 2)
+ }
+
+ func testAllowanceBreaksNeedOneThenTwoNewReadings() {
+ let now = Date(timeIntervalSince1970: 6_009_000)
+ let account = makeSnapshot(
+ remaining: 70,
+ fetchedAt: now,
+ resetAfter: 7 * 86_400 - 4 * 3_600
+ )
+ let reset = account.mainLimit!.window.resetsAt
+ let previousReset = account.mainLimit!.window.startsAt
+
+ let afterResetWaiting = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: previousReset,
+ remainingPercent: 40,
+ resetsAt: previousReset
+ )
+ ],
+ now: now
+ )
+ let afterResetRestored = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: previousReset,
+ remainingPercent: 40,
+ resetsAt: previousReset
+ ),
+ UsageSample(
+ observedAt: previousReset.addingTimeInterval(3_600),
+ remainingPercent: 80,
+ resetsAt: reset
+ )
+ ],
+ now: now
+ )
+ let afterCorrectionWaiting = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 70,
+ resetsAt: reset,
+ comparisonBreak: true
+ )
+ ],
+ now: now
+ )
+ let afterCorrectionRestored = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 80,
+ resetsAt: reset,
+ comparisonBreak: true
+ )
+ ],
+ now: now
+ )
+ let afterIncreaseWaiting = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 50,
+ resetsAt: reset
+ )
+ ],
+ now: now
+ )
+ let afterIncreaseRestored = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-2 * 3_600),
+ remainingPercent: 50,
+ resetsAt: reset
+ ),
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
+ remainingPercent: 80,
+ resetsAt: reset
+ )
+ ],
+ now: now
+ )
+ let accountChanged = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: [],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ accountEpochStartedAt: now
+ )
+ )
+ let accountRecovered = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-3_600),
remainingPercent: 80,
- resetsAt: reset
- ),
- UsageSample(
- observedAt: afterEpoch,
- remainingPercent: 72,
- resetsAt: reset
+ resetsAt: reset,
+ comparisonBreak: true
)
],
safetyBuffer: 3,
@@ -1447,30 +2724,247 @@ final class UsageIntelligenceEngineTests: XCTestCase {
)
)
- XCTAssertTrue(
- reader.chart.observed.contains { $0.date == beforeEpoch }
+ XCTAssertEqual(
+ afterResetWaiting.guidanceUnavailableReason,
+ "Waiting for a second account reading after reset or correction"
+ )
+ XCTAssertNotNil(afterResetRestored.guidance)
+ XCTAssertEqual(
+ afterCorrectionWaiting.guidanceUnavailableReason,
+ "Waiting for a second account reading after reset or correction"
+ )
+ XCTAssertNotNil(afterCorrectionRestored.guidance)
+ XCTAssertEqual(
+ afterIncreaseWaiting.guidanceUnavailableReason,
+ "Waiting for a second account reading after reset or correction"
+ )
+ XCTAssertNotNil(afterIncreaseRestored.guidance)
+ XCTAssertEqual(
+ accountChanged.guidanceUnavailableReason,
+ "Account changed — waiting for a second reading"
)
+ XCTAssertNotNil(accountRecovered.guidance)
}
- func testSparseHistoryWithholdsGuidance() {
- let now = Date(timeIntervalSince1970: 6_000_000)
- let reader = UsageIntelligenceEngine.evaluate(
- UsageIntelligenceInput(
- account: makeSnapshot(remaining: 55, fetchedAt: now),
- samples: [],
- safetyBuffer: 3,
- sourceState: .available,
- now: now,
- previousStatus: nil
+ func testRunwayAndCurrentEstimateSharePaceAndLatestReading() throws {
+ let now = Date(timeIntervalSince1970: 6_009_500)
+ let account = makeSnapshot(remaining: 20, fetchedAt: now)
+ let reader = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: now.addingTimeInterval(-12 * 3_600),
+ remainingPercent: 80,
+ resetsAt: account.mainLimit!.window.resetsAt
+ )
+ ],
+ now: now
+ )
+ let forecast = try XCTUnwrap(reader.guidance?.forecast)
+ let firstProjection = try XCTUnwrap(reader.chart.currentProjection.first)
+ let lastProjection = try XCTUnwrap(reader.chart.currentProjection.last)
+ guard case let .exhausts(runwayEnd, _) = reader.guidance?.runway else {
+ return XCTFail("Expected current pace to exhaust the allowance")
+ }
+
+ XCTAssertEqual(firstProjection.date, account.fetchedAt)
+ XCTAssertEqual(firstProjection.remaining, 20)
+ XCTAssertEqual(forecast.currentPercentPerDay, 120, accuracy: 0.000_001)
+ XCTAssertEqual(lastProjection.date, runwayEnd)
+ }
+
+ func testRefreshFailureRecoversWithoutExtendingObservedFacts() {
+ let now = Date(timeIntervalSince1970: 6_009_750)
+ let fetchedAt = now.addingTimeInterval(-10 * 60)
+ let account = makeSnapshot(remaining: 70, fetchedAt: fetchedAt)
+ let samples = [
+ UsageSample(
+ observedAt: fetchedAt.addingTimeInterval(-3_600),
+ remainingPercent: 80,
+ resetsAt: account.mainLimit!.window.resetsAt
)
+ ]
+ let failed = makeReader(
+ account: account,
+ samples: samples,
+ sourceState: .failed("Account source failed"),
+ now: now
)
+ let recovered = makeReader(
+ account: account,
+ samples: samples,
+ now: now
+ )
+
+ XCTAssertNil(failed.guidance)
+ XCTAssertEqual(failed.guidanceTitle, "Account source failed")
+ XCTAssertEqual(failed.chart.observed.last?.date, fetchedAt)
+ XCTAssertTrue(failed.chart.currentProjection.isEmpty)
+ XCTAssertNil(failed.suggestedPacePercentPerDay)
+ XCTAssertNotNil(recovered.guidance)
+ XCTAssertFalse(recovered.chart.currentProjection.isEmpty)
+ XCTAssertNotNil(recovered.suggestedPacePercentPerDay)
+ }
- XCTAssertEqual(reader.evidence.coverage, .low)
- XCTAssertEqual(reader.evidence.confidence, .low)
- XCTAssertEqual(reader.evidence.reason, "Not enough account history")
+ func testProductionGapShapeStillProducesCurrentGuidance() throws {
+ let now = Date(timeIntervalSince1970: 6_009_900)
+ let account = makeSnapshot(remaining: 60, fetchedAt: now)
+ let reset = account.mainLimit!.window.resetsAt
+ let gapStart = now.addingTimeInterval(-(47 * 3_600 + 27 * 60))
+ let denseStart = gapStart.addingTimeInterval(8 * 3_600 + 36 * 60)
+ var samples = [
+ UsageSample(
+ observedAt: gapStart,
+ remainingPercent: 92,
+ resetsAt: reset
+ )
+ ]
+ samples += stride(
+ from: denseStart.timeIntervalSince1970,
+ to: now.timeIntervalSince1970,
+ by: 20 * 60
+ ).map { timestamp in
+ let progress = (timestamp - denseStart.timeIntervalSince1970)
+ / now.timeIntervalSince(denseStart)
+ return UsageSample(
+ observedAt: Date(timeIntervalSince1970: timestamp),
+ remainingPercent: 85 - 25 * progress,
+ resetsAt: reset
+ )
+ }
+ let maximumGap = zip(samples, samples.dropFirst())
+ .map { $1.observedAt.timeIntervalSince($0.observedAt) }
+ .max() ?? 0
+ let reader = makeReader(account: account, samples: samples, now: now)
+ let withoutHistory = makeReader(account: account, now: now)
+
+ XCTAssertGreaterThan(maximumGap, 6 * 3_600)
+ XCTAssertGreaterThan(now.timeIntervalSince(denseStart), 24 * 3_600)
+ XCTAssertNotNil(reader.guidance)
+ XCTAssertFalse(reader.chart.currentProjection.isEmpty)
+ XCTAssertEqual(
+ try XCTUnwrap(reader.suggestedPacePercentPerDay),
+ try XCTUnwrap(withoutHistory.suggestedPacePercentPerDay),
+ accuracy: 0.000_001
+ )
+ }
+
+ func testSuggestedPaceNeedsOnlyCurrentReadingAndUsesExactFractionalDay() throws {
+ let now = Date(timeIntervalSince1970: 6_010_000)
+ let reader = makeReader(
+ account: makeSnapshot(
+ remaining: 23,
+ fetchedAt: now,
+ resetAfter: 12 * 60 * 60
+ ),
+ now: now
+ )
+
+ let pace = try XCTUnwrap(reader.suggestedPacePercentPerDay)
+ XCTAssertEqual(pace, 40, accuracy: 0.000_001)
+ XCTAssertTrue(pace.isFinite)
+ XCTAssertGreaterThanOrEqual(pace, 0)
+ XCTAssertEqual(
+ reader.suggestedPaceText,
+ "Up to 40.0 percentage points a day"
+ )
XCTAssertNil(reader.guidance)
}
+ func testSuggestedPaceIgnoresLongAccountHistoryGap() throws {
+ let now = Date(timeIntervalSince1970: 6_020_000)
+ let account = makeSnapshot(remaining: 40, fetchedAt: now)
+ let withoutHistory = makeReader(account: account, now: now)
+ let withLongGap = makeReader(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: account.mainLimit!.window.startsAt,
+ remainingPercent: 100,
+ resetsAt: account.mainLimit!.window.resetsAt
+ ),
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 40,
+ resetsAt: account.mainLimit!.window.resetsAt
+ )
+ ],
+ now: now
+ )
+
+ XCTAssertEqual(withLongGap.evidence.coverage, .low)
+ XCTAssertEqual(
+ try XCTUnwrap(withLongGap.suggestedPacePercentPerDay),
+ try XCTUnwrap(withoutHistory.suggestedPacePercentPerDay),
+ accuracy: 0.000_001
+ )
+ }
+
+ func testSuggestedPaceClampsAllowanceAtOrBelowBufferToZero() throws {
+ let now = Date(timeIntervalSince1970: 6_030_000)
+ let cases: [(remaining: Double, expected: Double)] = [
+ (100, 48.5),
+ (3, 0),
+ (2, 0),
+ (0, 0)
+ ]
+
+ for testCase in cases {
+ let reader = makeReader(
+ account: makeSnapshot(
+ remaining: testCase.remaining,
+ fetchedAt: now
+ ),
+ now: now
+ )
+ let pace = try XCTUnwrap(reader.suggestedPacePercentPerDay)
+
+ XCTAssertEqual(pace, testCase.expected, accuracy: 0.000_001)
+ XCTAssertTrue(pace.isFinite)
+ XCTAssertGreaterThanOrEqual(pace, 0)
+ }
+ }
+
+ func testSuggestedPaceUsesSpecificUnavailableReasons() {
+ let now = Date(timeIntervalSince1970: 6_040_000)
+ let sourceError = "Couldn’t read Codex usage. Try refreshing again."
+ let missing = makeReader(account: nil, now: now)
+ let atReset = makeReader(
+ account: makeSnapshot(
+ remaining: 50,
+ fetchedAt: now.addingTimeInterval(-2 * 86_400)
+ ),
+ now: now
+ )
+ let expired = makeReader(
+ account: makeSnapshot(
+ remaining: 50,
+ fetchedAt: now.addingTimeInterval(-3 * 86_400)
+ ),
+ now: now
+ )
+ let failed = makeReader(
+ account: makeSnapshot(remaining: 50, fetchedAt: now),
+ sourceState: .failed(sourceError),
+ now: now
+ )
+
+ XCTAssertNil(missing.suggestedPacePercentPerDay)
+ XCTAssertEqual(missing.suggestedPaceText, "Weekly usage unavailable")
+ XCTAssertNil(atReset.suggestedPacePercentPerDay)
+ XCTAssertEqual(
+ atReset.suggestedPaceText,
+ "Current allowance window unavailable"
+ )
+ XCTAssertNil(expired.suggestedPacePercentPerDay)
+ XCTAssertEqual(
+ expired.suggestedPaceText,
+ "Current allowance window unavailable"
+ )
+ XCTAssertNil(failed.suggestedPacePercentPerDay)
+ XCTAssertEqual(failed.suggestedPaceText, sourceError)
+ }
+
func testCompleteCoverageNeedsTheWholeWindowWithoutMaterialGaps() {
let step: TimeInterval = 30 * 60
let now = Date(timeIntervalSince1970: 7_000_000)
@@ -1540,6 +3034,225 @@ final class UsageIntelligenceEngineTests: XCTestCase {
XCTAssertTrue(ended.chart.historicalProjection.isEmpty)
}
+ func testCurrentWindowUsesFullAllowanceFrameAndStopsObservedSeries() throws {
+ let start = try date("2026-08-01T12:13:00Z")
+ let latestObservation = try date("2026-08-03T12:13:00Z")
+ let reset = try date("2026-08-08T12:13:00Z")
+ let account = UsageSnapshot(
+ mainLimit: LimitReading(
+ limitId: "codex",
+ name: "Codex",
+ window: UsageWindow(
+ remainingPercent: 70,
+ resetsAt: reset,
+ durationMinutes: 10_080
+ )
+ ),
+ otherLimits: [],
+ tokenHistory: [
+ TokenDay(
+ date: latestObservation.addingTimeInterval(-86_400),
+ tokens: 1_000,
+ completeness: .complete
+ )
+ ],
+ emergencyResetCount: 0,
+ fetchedAt: latestObservation
+ )
+ let reader = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: start,
+ remainingPercent: 100,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: latestObservation,
+ remainingPercent: 70,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: latestObservation,
+ previousStatus: nil
+ )
+ )
+
+ XCTAssertEqual(reader.interval?.startsAt, start)
+ XCTAssertEqual(reader.interval?.resetsAt, reset)
+ XCTAssertEqual(reader.chart.target.map(\.date), [start, reset])
+ XCTAssertEqual(reader.chart.observed.map(\.date).max(), latestObservation)
+ XCTAssertFalse(reader.chart.observed.contains { $0.date > latestObservation })
+ XCTAssertEqual(
+ reader.accountTokenActivity.range,
+ DateInterval(start: start, end: reset)
+ )
+ XCTAssertEqual(reader.accountTokenActivity.tokens, 300)
+ XCTAssertEqual(
+ reader.accountTokenActivity.interval,
+ DateInterval(start: start, end: latestObservation)
+ )
+ XCTAssertEqual(reader.accountTokenActivity.intervals.count, 1)
+ XCTAssertTrue(
+ reader.accountTokenActivity.currentWindowAccessibilityValue
+ .contains("300 account tokens so far")
+ )
+
+ let refreshedWithoutReading = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: start,
+ remainingPercent: 100,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ ),
+ UsageSample(
+ observedAt: latestObservation,
+ remainingPercent: 70,
+ resetsAt: reset,
+ lifetimeTokens: 1_300
+ )
+ ],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: latestObservation.addingTimeInterval(3_600),
+ previousStatus: nil
+ )
+ )
+ XCTAssertEqual(
+ refreshedWithoutReading.accountTokenActivity.intervals,
+ reader.accountTokenActivity.intervals
+ )
+ }
+
+ func testCurrentWindowDistinguishesZeroFromNoInterval() throws {
+ let start = try date("2026-08-01T12:13:00Z")
+ let now = try date("2026-08-03T12:13:00Z")
+ let reset = try date("2026-08-08T12:13:00Z")
+ let account = UsageSnapshot(
+ mainLimit: LimitReading(
+ limitId: "codex",
+ name: "Codex",
+ window: UsageWindow(
+ remainingPercent: 70,
+ resetsAt: reset,
+ durationMinutes: 10_080
+ )
+ ),
+ otherLimits: [],
+ tokenHistory: [],
+ emergencyResetCount: 0,
+ fetchedAt: now
+ )
+ let first = UsageSample(
+ observedAt: start,
+ remainingPercent: 100,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ )
+ let zero = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: [
+ first,
+ UsageSample(
+ observedAt: now,
+ remainingPercent: 70,
+ resetsAt: reset,
+ lifetimeTokens: 1_000
+ )
+ ],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil
+ )
+ ).accountTokenActivity
+ let missing = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: [first],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil
+ )
+ ).accountTokenActivity
+
+ XCTAssertEqual(zero.tokens, 0)
+ XCTAssertEqual(zero.intervals.first?.tokenDelta, 0)
+ XCTAssertTrue(
+ zero.currentWindowAccessibilityValue
+ .contains("observed zero-token interval")
+ )
+ XCTAssertTrue(
+ zero.currentWindowAccessibilityValue
+ .contains("missing, not zero")
+ )
+ XCTAssertTrue(
+ zero.currentWindowAccessibilityValue
+ .contains("future and has no observation")
+ )
+ XCTAssertNil(missing.tokens)
+ XCTAssertEqual(missing.reason, "No account readings in this range")
+ }
+
+ func testExpiredCurrentWindowIsUnavailableButKeepsHistoricalSamples() throws {
+ let reset = try date("2026-08-08T12:13:00Z")
+ let historicalObservation = try date("2026-08-03T12:13:00Z")
+ let account = UsageSnapshot(
+ mainLimit: LimitReading(
+ limitId: "codex",
+ name: "Codex",
+ window: UsageWindow(
+ remainingPercent: 70,
+ resetsAt: reset,
+ durationMinutes: 10_080
+ )
+ ),
+ otherLimits: [],
+ tokenHistory: [],
+ emergencyResetCount: 0,
+ fetchedAt: historicalObservation
+ )
+ let reader = UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: [
+ UsageSample(
+ observedAt: historicalObservation,
+ remainingPercent: 75,
+ resetsAt: reset
+ )
+ ],
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: try date("2026-08-09T12:13:00Z"),
+ previousStatus: nil
+ )
+ )
+
+ XCTAssertNil(reader.weeklyUsageRemaining)
+ XCTAssertNil(reader.interval)
+ XCTAssertEqual(reader.evidence.reason, "Current allowance window unavailable")
+ XCTAssertEqual(reader.guidanceTitle, "Current allowance window unavailable")
+ XCTAssertTrue(reader.chart.allObserved.contains {
+ $0.date == historicalObservation
+ })
+ XCTAssertEqual(
+ reader.accountTokenActivity.reason,
+ "Current allowance window unavailable"
+ )
+ XCTAssertTrue(reader.accountTokenActivity.intervals.isEmpty)
+ }
+
func testQuietDenseHistoryLeavesRoomToUseMore() {
let now = Date(timeIntervalSince1970: 9_000_000)
let account = makeSnapshot(remaining: 40, fetchedAt: now)
@@ -2162,6 +3875,7 @@ final class UsageIntelligenceEngineTests: XCTestCase {
private func makeSnapshot(
remaining: Double,
fetchedAt: Date,
+ resetAfter: TimeInterval = 2 * 86_400,
tokenHistory: [TokenDay] = [],
accountFacts: AccountFacts? = nil,
emergencyResetCount: Int = 0,
@@ -2170,7 +3884,7 @@ final class UsageIntelligenceEngineTests: XCTestCase {
) -> UsageSnapshot {
let window = UsageWindow(
remainingPercent: remaining,
- resetsAt: fetchedAt.addingTimeInterval(2 * 86_400),
+ resetsAt: fetchedAt.addingTimeInterval(resetAfter),
durationMinutes: 10_080
)
return UsageSnapshot(
@@ -2185,6 +3899,29 @@ final class UsageIntelligenceEngineTests: XCTestCase {
)
}
+ private func makeReader(
+ account: UsageSnapshot?,
+ samples: [UsageSample] = [],
+ safetyBuffer: Double = 3,
+ sourceState: UsageSourceState = .available,
+ now: Date
+ ) -> UsageReaderSnapshot {
+ UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: samples,
+ safetyBuffer: safetyBuffer,
+ sourceState: sourceState,
+ now: now,
+ previousStatus: nil
+ )
+ )
+ }
+
+ private func date(_ value: String) throws -> Date {
+ try XCTUnwrap(ISO8601DateFormatter().date(from: value))
+ }
+
private func timingFact(
id: String,
taskID: String,
@@ -2265,6 +4002,33 @@ final class UsageIntelligenceEngineTests: XCTestCase {
}
}
+ private func rollingDayActivity(
+ account: UsageSnapshot,
+ samples: S,
+ now: Date,
+ accountPartitionID: String? = nil,
+ accountEpochStartedAt: Date? = nil,
+ timeRange: AnalyticsTimeRange = .oneDay,
+ visibleRange: DateInterval? = nil
+ ) -> AccountTokenActivitySnapshot where S.Element == UsageSample {
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = timeRange
+ exploration.visibleRange = visibleRange
+ return UsageIntelligenceEngine.evaluate(
+ UsageIntelligenceInput(
+ account: account,
+ samples: Array(samples),
+ safetyBuffer: 3,
+ sourceState: .available,
+ now: now,
+ previousStatus: nil,
+ accountPartitionID: accountPartitionID,
+ accountEpochStartedAt: accountEpochStartedAt,
+ analyticsExploration: exploration
+ )
+ ).accountTokenActivity
+ }
+
private func weeklySamples(
start: Date,
end: Date,
diff --git a/Tests/CodexLimitsTests/UsageMonitorHistoryTests.swift b/Tests/CodexLimitsTests/UsageMonitorHistoryTests.swift
index bb79e6f..1f2ea65 100644
--- a/Tests/CodexLimitsTests/UsageMonitorHistoryTests.swift
+++ b/Tests/CodexLimitsTests/UsageMonitorHistoryTests.swift
@@ -410,7 +410,11 @@ final class UsageMonitorHistoryTests: XCTestCase {
XCTAssertNil(monitor.readerSnapshot.accountTokenActivity.tokens)
XCTAssertEqual(
monitor.readerSnapshot.accountTokenActivity.reason,
- "No lifetime token reading at the weekly boundary"
+ "No account readings in this range"
+ )
+ XCTAssertEqual(
+ monitor.readerSnapshot.accountTokenActivity.breaks.map(\.reason),
+ [.accountChange]
)
defaults.removeObject(
@@ -720,6 +724,51 @@ final class UsageMonitorHistoryTests: XCTestCase {
XCTAssertEqual(monitor.readerSnapshot.menuBarText, "80%")
}
+ func testPreferenceChangeRecalculatesTokenRangeWhenIdle() async throws {
+ let suiteName = "UsageMonitorHistoryTests-\(UUID().uuidString)"
+ let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+ let evaluationStarted = expectation(
+ description: "account evaluation started"
+ )
+ let evaluator = BlockingUsageEvaluator(started: evaluationStarted)
+ let fetchedAt = Date(timeIntervalSince1970: 1_900_000)
+ let source = FetchSequence([
+ makeFetchResult(
+ identity: "user@example.com",
+ fetchedAt: fetchedAt,
+ remaining: 80
+ )
+ ])
+ let monitor = UsageMonitor(
+ defaults: defaults,
+ historyDirectory: temporaryDirectory(),
+ startsAutomatically: false,
+ fetchUsage: { try await source.next() },
+ evaluateUsage: { evaluator.evaluate($0) }
+ )
+ let refresh = Task { await monitor.refresh() }
+ await fulfillment(of: [evaluationStarted], timeout: 2)
+ evaluator.release()
+ await refresh.value
+ let initialCalls = evaluator.callCount
+ var exploration = AnalyticsExplorationState.initial
+ exploration.timeRange = .threeDays
+
+ monitor.analyticsPreferencesDidChange(
+ exploration: exploration,
+ dispositions: [:]
+ )
+ let deadline = ContinuousClock.now + .seconds(2)
+ while evaluator.callCount == initialCalls,
+ ContinuousClock.now < deadline {
+ await Task.yield()
+ }
+
+ XCTAssertGreaterThan(evaluator.callCount, initialCalls)
+ XCTAssertEqual(evaluator.lastExploration, exploration)
+ }
+
func testFailedRefreshRetainsTheLastReaderSnapshot() async throws {
let suiteName = "UsageMonitorHistoryTests-\(UUID().uuidString)"
let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))