diff --git a/Sources/CodexLimits/UsageIntelligenceEngine.swift b/Sources/CodexLimits/UsageIntelligenceEngine.swift index 92c7099..2410150 100644 --- a/Sources/CodexLimits/UsageIntelligenceEngine.swift +++ b/Sources/CodexLimits/UsageIntelligenceEngine.swift @@ -745,9 +745,13 @@ private enum RecentMovementBreak { enum UsageIntelligenceEngine { static func evaluate(_ input: UsageIntelligenceInput) -> UsageReaderSnapshot { + let samples = UsageHistoryPolicy.aligningWindowResets( + input.samples, + currentReset: input.account?.mainLimit?.window.resetsAt + ) let currentSamples = input.account.flatMap { account in account.mainLimit.map { weeklyLimit in - input.samples + samples .filter { sample in sample.resetsAt == weeklyLimit.window.resetsAt && sample.observedAt >= weeklyLimit.window.startsAt @@ -792,7 +796,7 @@ enum UsageIntelligenceEngine { ) let recentMovement = recentAccountMovement( account: input.account, - samples: input.samples, + samples: samples, sourceState: input.sourceState, now: input.now, accountEpochStartedAt: input.accountEpochStartedAt @@ -802,7 +806,7 @@ enum UsageIntelligenceEngine { let weeklyLimit = account.mainLimit else { return nil } return ForecastEngine.evaluate( window: weeklyLimit.window, - samples: input.samples, + samples: samples, tokenHistory: account.tokenHistory, safetyBuffer: input.safetyBuffer, now: input.now, @@ -849,7 +853,7 @@ enum UsageIntelligenceEngine { } let chart = chart( account: input.account, - samples: input.samples, + samples: samples, forecast: forecast, recentMovement: recentMovement.movement, sourceState: input.sourceState, @@ -888,7 +892,7 @@ enum UsageIntelligenceEngine { let accountTokenActivity = if let selectedTokenRange { selectedRangeAccountTokenActivity( account: input.account, - samples: input.samples, + samples: samples, range: selectedTokenRange, now: input.now, accountPartitionID: input.accountPartitionID, @@ -979,7 +983,7 @@ enum UsageIntelligenceEngine { let evidence: WeeklyUsageEvidenceSet if let reusableLocalHistory, reusableLocalHistory.matches( - samples: input.samples, + samples: samples, observation: input.localActivityObservation, accountPartitionID: partitionID, limitID: weekly.limitId, @@ -990,7 +994,7 @@ enum UsageIntelligenceEngine { reusedWeeklyEvidence = true } else { evidence = WeeklyUsageEvidenceBuilder.build( - samples: input.samples, + samples: samples, localFacts: input.localActivityHistoryFacts, localObservation: input.localActivityObservation, accountPartitionID: partitionID, @@ -1068,7 +1072,7 @@ enum UsageIntelligenceEngine { let weekly = input.account?.mainLimit { localHistoryCache = LocalHistoryAggregateCache( factIndex: localHistoryFactIndex, - samples: input.samples, + samples: samples, observation: input.localActivityObservation, accountPartitionID: partitionID, limitID: weekly.limitId, diff --git a/Sources/CodexLimits/UsageModels.swift b/Sources/CodexLimits/UsageModels.swift index 79e7789..09bcc5d 100644 --- a/Sources/CodexLimits/UsageModels.swift +++ b/Sources/CodexLimits/UsageModels.swift @@ -148,6 +148,33 @@ enum UsageHistoryPolicy { static let tightBoundary: TimeInterval = 15 * 60 static let maximumComparableGap: TimeInterval = 30 * 60 + static func aligningWindowResets( + _ samples: [UsageSample], + currentReset: Date? + ) -> [UsageSample] { + var anchors = currentReset.map { [$0] } ?? [] + var aligned: [Date: Date] = [:] + for sample in samples.sorted(by: { $0.observedAt > $1.observedAt }) + where aligned[sample.resetsAt] == nil { + let anchor = anchors.first { + abs($0.timeIntervalSince(sample.resetsAt)) <= tightBoundary + } ?? sample.resetsAt + aligned[sample.resetsAt] = anchor + if anchor == sample.resetsAt { anchors.append(anchor) } + } + return samples.map { sample in + guard let reset = aligned[sample.resetsAt], + reset != sample.resetsAt else { return sample } + return UsageSample( + observedAt: sample.observedAt, + remainingPercent: sample.remainingPercent, + resetsAt: reset, + lifetimeTokens: sample.lifetimeTokens, + comparisonBreak: sample.comparisonBreak + ) + } + } + static func segments( _ samples: [UsageSample] ) -> [[UsageSample]] { diff --git a/Tests/CodexLimitsTests/UsageIntelligenceEngineTests.swift b/Tests/CodexLimitsTests/UsageIntelligenceEngineTests.swift index 7b2790c..2068827 100644 --- a/Tests/CodexLimitsTests/UsageIntelligenceEngineTests.swift +++ b/Tests/CodexLimitsTests/UsageIntelligenceEngineTests.swift @@ -2394,6 +2394,36 @@ final class UsageIntelligenceEngineTests: XCTestCase { ) } + func testResetTimeJitterDoesNotCreateASecondAllowanceWindow() { + let now = Date(timeIntervalSince1970: 6_400_000) + let account = makeSnapshot(remaining: 34, fetchedAt: now) + let reset = account.mainLimit!.window.resetsAt + let reader = UsageIntelligenceEngine.evaluate( + UsageIntelligenceInput( + account: account, + samples: [ + UsageSample( + observedAt: now.addingTimeInterval(-3 * 86_400), + remainingPercent: 70, + resetsAt: reset.addingTimeInterval(1) + ), + UsageSample( + observedAt: now.addingTimeInterval(-60), + remainingPercent: 34, + resetsAt: reset.addingTimeInterval(1) + ) + ], + safetyBuffer: 3, + sourceState: .available, + now: now, + previousStatus: nil + ) + ) + + XCTAssertEqual(reader.chart.allowanceWindows.count, 1) + XCTAssertEqual(reader.chart.observed.count, 3) + } + func testAccountEpochDoesNotHideStoredChartPoints() { let now = Date(timeIntervalSince1970: 6_400_000) let account = makeSnapshot(remaining: 70, fetchedAt: now)