Skip to content

Commit d62f2e6

Browse files
committed
fix: preserve calendar in workspace usage
1 parent cdab5d3 commit d62f2e6

3 files changed

Lines changed: 69 additions & 16 deletions

File tree

Sources/CodexBarCore/CodexLocalProjectUsageIndexer.swift

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ enum CodexLocalProjectUsageIndexer {
4646
defer { CodexModelsTelemetry.end("IndexRefresh", id: refreshSignpost) }
4747
let clampedHistoryDays = max(1, min(365, historyDays))
4848
let until = now
49-
let since = Calendar.current.date(byAdding: .day, value: -(clampedHistoryDays - 1), to: now) ?? now
50-
let comparisonSince = self.modelsAnalyticsScanStart(since: since, until: until)
5149
var scannerOptions = options.scannerOptions
5250
if forceRefresh {
5351
scannerOptions.refreshMinIntervalSeconds = 0
5452
}
53+
let since = scannerOptions.calendar.date(
54+
byAdding: .day,
55+
value: -(clampedHistoryDays - 1),
56+
to: now) ?? now
57+
let comparisonSince = self.modelsAnalyticsScanStart(
58+
since: since,
59+
until: until,
60+
calendar: scannerOptions.calendar)
5561

5662
progress?(CodexLocalProjectUsageIndexProgress(phase: .scanningLogs))
5763
_ = try CostUsageScanner.loadDailyReportCancellable(
@@ -63,7 +69,10 @@ enum CodexLocalProjectUsageIndexer {
6369
checkCancellation: checkCancellation)
6470
try checkCancellation?()
6571

66-
let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: scannerOptions.cacheRoot)
72+
let cache = CostUsageCacheIO.load(
73+
provider: .codex,
74+
cacheRoot: scannerOptions.cacheRoot,
75+
calendar: scannerOptions.calendar)
6776
let catalogResult = CodexThreadCatalogReader.loadResult(options: scannerOptions)
6877
let catalog = catalogResult.catalog
6978
let sourceStatus = CodexLocalProjectUsageSourceStatus(catalog: catalogResult.completeness)
@@ -145,8 +154,14 @@ enum CodexLocalProjectUsageIndexer {
145154
checkCancellation: CostUsageScanner.CancellationCheck? = nil) throws -> CodexLocalProjectUsageSnapshot
146155
{
147156
let clampedHistoryDays = max(1, min(365, historyDays))
148-
let range = CostUsageScanner.CostUsageDayRange(since: since, until: until)
149-
let cache = cacheOverride ?? CostUsageCacheIO.load(provider: .codex, cacheRoot: options.cacheRoot)
157+
let range = CostUsageScanner.CostUsageDayRange(
158+
since: since,
159+
until: until,
160+
calendar: options.calendar)
161+
let cache = cacheOverride ?? CostUsageCacheIO.load(
162+
provider: .codex,
163+
cacheRoot: options.cacheRoot,
164+
calendar: options.calendar)
150165
let catalog = catalogOverride ?? CodexThreadCatalogReader.load(options: options)
151166
let expectedRoots = CostUsageScanner.codexRootsFingerprint(options: options)
152167
let scopeSignature = self.stableScopeSignature(options: options)
@@ -275,7 +290,8 @@ enum CodexLocalProjectUsageIndexer {
275290
since: since,
276291
until: until,
277292
historyDays: clampedHistoryDays,
278-
generatedAt: now),
293+
generatedAt: now,
294+
calendar: options.calendar),
279295
identity: ModelsAnalyticsIdentity(
280296
scopeSignature: scopeSignature,
281297
rootsFingerprint: rootsFingerprint),
@@ -297,6 +313,7 @@ extension CodexLocalProjectUsageIndexer {
297313
let until: Date
298314
let historyDays: Int
299315
let generatedAt: Date
316+
let calendar: Calendar
300317
}
301318

302319
fileprivate struct ModelsAnalyticsIdentity {
@@ -653,19 +670,27 @@ extension CodexLocalProjectUsageIndexer {
653670
{
654671
let aggregationSignpost = CodexModelsTelemetry.begin("SnapshotAggregation")
655672
defer { CodexModelsTelemetry.end("SnapshotAggregation", id: aggregationSignpost) }
656-
let periods = self.modelsAnalyticsPeriods(since: window.since, until: window.until)
673+
let periods = self.modelsAnalyticsPeriods(
674+
since: window.since,
675+
until: window.until,
676+
calendar: window.calendar)
657677
let previousInterval = periods.previous
658678
let previousRange = CostUsageScanner.CostUsageDayRange(
659679
since: previousInterval.start,
660-
until: previousInterval.end.addingTimeInterval(-1))
680+
until: previousInterval.end.addingTimeInterval(-1),
681+
calendar: window.calendar)
661682
let previousBuckets = try self.sessionBuckets(
662683
from: context.cache,
663684
range: previousRange,
664685
catalog: context.catalog,
665686
progress: nil,
666687
checkCancellation: checkCancellation).sessionBuckets
667-
let currentFragments = self.analyticsFragments(from: context.currentBuckets.values)
668-
let previousFragments = self.analyticsFragments(from: previousBuckets.values)
688+
let currentFragments = self.analyticsFragments(
689+
from: context.currentBuckets.values,
690+
calendar: window.calendar)
691+
let previousFragments = self.analyticsFragments(
692+
from: previousBuckets.values,
693+
calendar: window.calendar)
669694
let currentBucketsByProject = Dictionary(grouping: context.currentBuckets.values, by: \.projectId)
670695
let previousBucketsByProject = Dictionary(grouping: previousBuckets.values, by: \.projectId)
671696
let currentFragmentsByProject = Dictionary(grouping: currentFragments, by: \.workspaceID)
@@ -814,14 +839,16 @@ extension CodexLocalProjectUsageIndexer {
814839
}
815840

816841
fileprivate static func analyticsFragments(
817-
from buckets: Dictionary<String, SessionBucket>.Values) -> [CodexModelsUsageFragment]
842+
from buckets: Dictionary<String, SessionBucket>.Values,
843+
calendar: Calendar) -> [CodexModelsUsageFragment]
818844
{
819-
buckets.flatMap { bucket in
845+
let calendar = CostUsageScanner.CostUsageDayRange.localGregorianCalendar(matching: calendar)
846+
return buckets.flatMap { bucket in
820847
if bucket.hasCompleteEventRows, !bucket.usageRows.isEmpty {
821848
return bucket.usageRows.compactMap { row -> CodexModelsUsageFragment? in
822849
guard let timestampUnixMs = row.timestampUnixMs else { return nil }
823850
let timestamp = Date(timeIntervalSince1970: Double(timestampUnixMs) / 1000)
824-
let day = Calendar.current.startOfDay(for: timestamp)
851+
let day = calendar.startOfDay(for: timestamp)
825852
let inputTokens = max(0, row.input)
826853
let outputTokens = max(0, row.output)
827854
let totalTokens = Int64(inputTokens + outputTokens)
@@ -841,7 +868,8 @@ extension CodexLocalProjectUsageIndexer {
841868
}
842869
}
843870
return bucket.modelDailyTotals.flatMap { day, models -> [CodexModelsUsageFragment] in
844-
guard let date = CostUsageDateParser.parse(day) else { return [] }
871+
guard let parsedDate = CostUsageScanner.parseDayKey(day, calendar: calendar) else { return [] }
872+
let date = calendar.startOfDay(for: parsedDate)
845873
return models.map { model, totals in
846874
CodexModelsUsageFragment(
847875
workspaceID: bucket.projectId,
@@ -895,7 +923,7 @@ extension CodexLocalProjectUsageIndexer {
895923
}
896924

897925
fileprivate static func stableScopeSignature(options: CostUsageScanner.Options) -> String {
898-
CodexLocalDataScope.resolve(options: options).identifier
926+
"\(CodexLocalDataScope.resolve(options: options).identifier)|timeZone=\(options.calendar.timeZone.identifier)"
899927
}
900928

901929
fileprivate static func rootsFingerprint(_ roots: [String: Int64]) -> [String: Int64] {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.
22

33
enum CodexParserHash {
4-
static let value = "6f689d90f8eedcbd"
4+
static let value = "21dae5bee0a0ece1"
55
}

Tests/CodexBarTests/CostUsageCalendarTests.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,31 @@ struct CostUsageCalendarTests {
330330
#expect(cached?.projects.flatMap(\.daily).map(\.date) == [expectedDay])
331331
#expect(cached?.sessions.map(\.sessionID) == ["calendar-fetcher-time-zone.jsonl"])
332332
#expect(cached?.sessionTokens == 10)
333+
334+
let staleProjectSnapshot = await CostUsageFetcher.loadCachedCodexLocalProjectUsageSnapshot(
335+
now: now,
336+
historyDays: 1,
337+
hidePersonalInfo: false,
338+
scannerOptions: options)
339+
#expect(staleProjectSnapshot == nil)
340+
341+
let projectSnapshot = try await CostUsageFetcher.loadCodexLocalProjectUsageSnapshot(
342+
now: now,
343+
forceRefresh: true,
344+
historyDays: 1,
345+
hidePersonalInfo: false,
346+
scannerOptions: options)
347+
#expect(projectSnapshot.daily.map(\.day) == [expectedDay])
348+
#expect(projectSnapshot.projects.flatMap(\.daily).map(\.day) == [expectedDay])
349+
#expect(projectSnapshot.sessions.flatMap(\.daily).map(\.day) == [expectedDay])
350+
#expect(projectSnapshot.scopeSignature.contains("timeZone=\(calendar.timeZone.identifier)"))
351+
352+
let cachedProjectSnapshot = await CostUsageFetcher.loadCachedCodexLocalProjectUsageSnapshot(
353+
now: now,
354+
historyDays: 1,
355+
hidePersonalInfo: false,
356+
scannerOptions: options)
357+
#expect(cachedProjectSnapshot?.daily.map(\.day) == [expectedDay])
333358
}
334359

335360
private static func codexOptions(

0 commit comments

Comments
 (0)