Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 0.49.6 — Unreleased

### Fixed
- Codex: preserve the wider retained cost history when Usage & Spend saves a 7- or 30-day projection under cache row/byte pressure, instead of pruning older rows and narrowing the persisted scan window (#2914, #2915). Thanks @thomaschow19!

## 0.49.5 — 2026-08-13

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ extension CostUsageStore {
var cache = cache
Self.reconcileCompletedCodexCatchUp(cache: &cache)
let previous = self.readSnapshot()
let budgetProtectionWindow = Self.budgetProtectionWindow(
cache: cache,
requestedScanWindow: requestedScanWindow)
if skipIdenticalContent,
Self.persistedContentMatches(
previous: previous,
Expand All @@ -79,8 +82,8 @@ extension CostUsageStore {
let result = self.enforceBudgets(
maxRows: rowBudget,
maxFileBytes: fileBudgetBytes,
requestedSinceDay: requestedScanWindow.sinceKey,
requestedUntilDay: requestedScanWindow.untilKey,
requestedSinceDay: budgetProtectionWindow.sinceKey,
requestedUntilDay: budgetProtectionWindow.untilKey,
calendar: calendar)
guard !result.catchUpRequired else { return result }
Self.identicalContentPreLockCheckpointForTesting?()
Expand Down Expand Up @@ -159,8 +162,8 @@ extension CostUsageStore {
let result = self.enforceBudgets(
maxRows: rowBudget,
maxFileBytes: fileBudgetBytes,
requestedSinceDay: requestedScanWindow.sinceKey,
requestedUntilDay: requestedScanWindow.untilKey,
requestedSinceDay: budgetProtectionWindow.sinceKey,
requestedUntilDay: budgetProtectionWindow.untilKey,
calendar: calendar)
if result.catchUpRequired, self.fetchMetadata().previousReportPayload == nil,
let previous = Self.previousReport(cache: cache, calendar: calendar, reportWindow: reportWindow)
Expand Down Expand Up @@ -207,6 +210,21 @@ extension CostUsageStore {
usage.codexScanComplete = usage.codexScanComplete ?? true
return usage
}

private static func budgetProtectionWindow(
cache: CostUsageCache,
requestedScanWindow: (sinceKey: String, untilKey: String)) -> (sinceKey: String, untilKey: String)
{
// Report and dashboard windows are projections, not retention boundaries. Preserve the
// cache's retained coverage while still protecting any newly requested extension.
guard let retainedSinceKey = cache.scanSinceKey,
let retainedUntilKey = cache.scanUntilKey,
retainedSinceKey <= retainedUntilKey
else { return requestedScanWindow }
return (
sinceKey: min(retainedSinceKey, requestedScanWindow.sinceKey),
untilKey: max(retainedUntilKey, requestedScanWindow.untilKey))
}
}

// MARK: - Cache conversion
Expand Down
68 changes: 68 additions & 0 deletions Tests/CodexBarTests/CostUsageStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,74 @@ extension CostUsageStoreTests {
}

extension CostUsageStoreTests {
@Test
func `narrow dashboard windows preserve wider retained cache under byte pressure`() async throws {
let fixture = try StoreFixture()
defer { fixture.remove() }
let store = CostUsageStore(cacheRoot: fixture.root)
let calendar = CostUsageScanner.CostUsageDayRange.localGregorianCalendar()
let retainedWindow = (sinceKey: "2025-08-15", untilKey: "2026-08-14")
let olderDay = "2026-01-15"
let recentDay = "2026-08-14"
let model = "gpt-5.5"
let olderPath = "/rollouts/older.jsonl"
let recentPath = "/rollouts/recent.jsonl"

func usage(day: String, mtimeUnixMs: Int64) -> CostUsageFileUsage {
var value = CostUsageFileUsage(
mtimeUnixMs: mtimeUnixMs,
size: 500,
days: [day: [model: [1, 0, 0]]])
value.parsedBytes = 500
value.codexScanComplete = true
return value
}

let olderUsage = usage(day: olderDay, mtimeUnixMs: 1)
let recentUsage = usage(day: recentDay, mtimeUnixMs: 2)
var cache = CostUsageCache()
cache.scanSinceKey = retainedWindow.sinceKey
cache.scanUntilKey = retainedWindow.untilKey
cache.timeZoneIdentifier = calendar.timeZone.identifier
cache.files = [olderPath: olderUsage, recentPath: recentUsage]
cache.days = olderUsage.days.merging(recentUsage.days) { _, recent in recent }

let seeded = store.syncSaveCodexCache(
cache,
calendar: calendar,
requestedScanWindow: retainedWindow,
fileBudgetBytes: 1)
#expect(seeded.deletedRows == 0)
#expect(seeded.rowCount == 2)

let dashboardWindows = [
(sinceKey: "2026-07-16", skipIdenticalContent: true),
(sinceKey: "2026-08-08", skipIdenticalContent: false),
]
for dashboardWindow in dashboardWindows {
let result = store.syncSaveCodexCache(
cache,
calendar: calendar,
requestedScanWindow: (sinceKey: dashboardWindow.sinceKey, untilKey: recentDay),
fileBudgetBytes: 1,
skipIdenticalContent: dashboardWindow.skipIdenticalContent)
let report = await store.readReport(
sinceDay: retainedWindow.sinceKey,
untilDay: retainedWindow.untilKey)
let metadata = await store.fetchMetadata()

#expect(result.catchUpRequired == false)
#expect(result.deletedRows == 0)
#expect(result.rowCount == 2)
#expect(result.fileBytes > 1)
#expect(await store.fetchFile(path: olderPath) != nil)
#expect(await store.fetchFile(path: recentPath) != nil)
#expect(report.aggregates.map(\.day) == [olderDay, recentDay])
#expect(metadata.scanSinceDay == retainedWindow.sinceKey)
#expect(metadata.scanUntilDay == retainedWindow.untilKey)
}
}

@Test
func `row budget deletes oldest rows to cap`() async throws {
let fixture = try StoreFixture()
Expand Down