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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "4969a789db679c93"
static let value = "f5fdba377006d7be"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3059,6 +3059,7 @@ enum CostUsageScanner {
state: &state)
}

/// Keep waiting paths ahead of revisits when changing pricing or priority metadata reseeds the inventory.
private static func reseedCodexActiveLookbackPathKeys(
_ pathKeys: some Sequence<String>,
state: inout CostUsageCodexActiveLookbackState)
Expand All @@ -3070,10 +3071,10 @@ enum CostUsageScanner {
guard queuedPaths.insert(pathKey).inserted else { return }
reseededPaths.append(pathKey)
}
for path in pathKeys {
for path in state.pendingFilePaths {
append(path)
}
for path in state.pendingFilePaths {
for path in pathKeys {
append(path)
}
state.pendingFilePaths = reseededPaths
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ actor CostUsageStore {
parserHash: CodexParserHash.value)
static let cacheGeneration = "sqlite:\(CostUsageStore.schemaVersion)"
static let compatiblePredecessorParserHashes: Set<String> = [
"4969a789db679c93", // 0.58.0 native rows, checkpoints, and reports survive queue reordering.
"c4fa7db2cf54bc41", // Parser revisions reparse older native files while preserving stored rows and checkpoints.
"ca4bc3875600536f", // Reserve pricing stores retain compatible rows and checkpoints.
"7f00691fa96c78d1", // Current-main row and checkpoint formats remain compatible.
Expand Down
67 changes: 67 additions & 0 deletions Tests/CodexBarTests/CostUsageBoundedProgressTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,73 @@ struct CostUsageBoundedProgressTests {
completedCurrentWindowFlatRootPaths: roots)
}

@Test
func `cache-wide migration reseed keeps the stale queue head ahead of revisited files`() throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
var options = Self.boundedOptions(env: env)
let since = try #require(options.calendar.date(byAdding: .day, value: -364, to: day))
let files = try Self.writeSyntheticCorpus(env: env, day: day, fileCount: 600)
for (index, file) in files.enumerated() {
try FileManager.default.setAttributes(
[.modificationDate: day.addingTimeInterval(Double(-index))], ofItemAtPath: file.path)
}
options.maxCodexScanDurationPerRefresh = nil
_ = CostUsageScanner.loadDailyReport(
provider: .codex, since: since, until: day, now: day, options: options)
var cache = CostUsageStoreAccess.read(cacheRoot: env.cacheRoot)
#expect(cache.files.count == 600)
#expect(cache.codexScanCatchUpPending == false)

// The 88 oldest files still carry the previous parser revision; the newest 512 are current.
let stalePaths = files[512...].map { $0.resolvingSymlinksInPath().path }
for path in stalePaths {
#expect(cache.files[path] != nil)
cache.files[path]?.codexParserRevision = nil
}
cache.codexPricingKey = "migration-generation-1"
CostUsageStoreAccess.replace(cacheRoot: env.cacheRoot, cache: cache)

options.maxCodexScanDurationPerRefresh = 60
for pass in 1...2 {
if pass > 1 {
// A cache-wide requirement (pricing key, priority turns) can change on every pass.
var mutated = CostUsageStoreAccess.read(cacheRoot: env.cacheRoot)
mutated.codexPricingKey = "migration-generation-\(pass)"
CostUsageStoreAccess.replace(cacheRoot: env.cacheRoot, cache: mutated)
}
let recorder = CostUsageScanner.CodexScanWorkRecorder()
options.codexScanWorkRecorderForTesting = recorder
_ = CostUsageScanner.loadDailyReport(
provider: .codex,
since: since,
until: day,
now: day.addingTimeInterval(Double(pass)),
options: options)
#expect(recorder.snapshot().codexFileScanAttempts == CostUsageScanner.codexCatchUpScanCandidateLimit)
}
let migrated = CostUsageStoreAccess.read(cacheRoot: env.cacheRoot)
let remainingStale = stalePaths.filter { migrated.files[$0]?.hasCurrentCodexParser != true }
#expect(remainingStale.isEmpty, "stale files never reached the bounded pass: \(remainingStale.count)")
#expect(migrated.codexActiveLookbackState?.pendingFilePaths.count == 88)

options.codexScanWorkRecorderForTesting = nil
for index in 0..<3 {
_ = CostUsageScanner.loadDailyReport(
provider: .codex,
since: since,
until: day,
now: day.addingTimeInterval(Double(index + 10)),
options: options)
}
let completed = CostUsageStoreAccess.read(cacheRoot: env.cacheRoot)
#expect(completed.codexScanCatchUpPending == false)
#expect(completed.codexActiveLookbackState == nil)
#expect(completed.codexScanCompletedFiles == 600)
#expect(completed.codexScanTotalFiles == 600)
}

private static func boundedOptions(env: CostUsageTestEnvironment) -> CostUsageScanner.Options {
var options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
Expand Down
6 changes: 3 additions & 3 deletions Tests/CodexBarTests/CostUsageCacheWideMigrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ struct CostUsageCacheWideMigrationTests {
}

@Test
func `cache wide migration reorders a partially drained queue newest first`() throws {
func `cache wide migration preserves discovered waiters before reseeded files`() throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
Expand Down Expand Up @@ -90,10 +90,10 @@ struct CostUsageCacheWideMigrationTests {
let migratedCache = CostUsageStoreAccess.read(cacheRoot: env.cacheRoot)
#expect(recorder.snapshot().codexFileScanAttempts == CostUsageScanner.codexCatchUpScanCandidateLimit)
#expect(recorder.attemptedCodexFilePaths().contains(newestURL.path))
#expect(!recorder.attemptedCodexFilePaths().contains(oldestURL.path))
#expect(recorder.attemptedCodexFilePaths().contains(oldestURL.path))
#expect(migratedCache.codexActiveLookbackState?.pendingFilePaths.count
== corpusSize - CostUsageScanner.codexCatchUpScanCandidateLimit)
#expect(migratedCache.codexActiveLookbackState?.pendingFilePaths.contains(oldestURL.path) == true)
#expect(migratedCache.codexActiveLookbackState?.pendingFilePaths.contains(oldestURL.path) == false)
}

@Test(arguments: CacheWideMigrationCase.allCases)
Expand Down
2 changes: 2 additions & 0 deletions Tests/CodexBarTests/CostUsageStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,7 @@ extension CostUsageStoreTests {

extension CostUsageStoreTests {
@Test(arguments: [
"4969a789db679c93", // Released in 0.58.0.
"c4fa7db2cf54bc41",
"ca4bc3875600536f",
"7f00691fa96c78d1",
Expand Down Expand Up @@ -1034,6 +1035,7 @@ extension CostUsageStoreTests {
let fixture = try StoreFixture()
defer { fixture.remove() }
#expect(CostUsageStore.compatiblePredecessorParserHashes == [
"4969a789db679c93",
"c4fa7db2cf54bc41",
"ca4bc3875600536f",
"7f00691fa96c78d1",
Expand Down
1 change: 1 addition & 0 deletions docs/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ is limited, using additional rows when needed.
- Automatic Codex catch-up scheduling in both usage and Spend Dashboard honors the app’s 30-minute Low Power Mode minimum after each pass. Explicit acceleration remains immediate, and physical low-power/thermal pauses retain their own retry policy. The setting applies when the next delay is computed; an already pending sleep is not replanned.
- Automatic catch-up reports thermal pressure when serious heat and Low Power Mode coexist. Both constraints keep the existing 60-second pause before rechecking resource state.
- A catch-up worker that loses its account or settings scope clears its abandoned Refreshing activity on exit. Legitimate pauses remain visible, and an older worker cannot clear a replacement worker's activity.
- Cache-wide migration reseeding keeps paths already waiting ahead of new revisits. Repeated pricing or priority-turn changes therefore cannot keep the same completed files ahead of the stale tail in each 512-candidate pass. Initial seeding still honors newest-first preference, and publication waits for exact inventory validation. Native Codex stores from published parser fingerprint `4969a789db679c93` adopt the new generation without rebuilding rows, checkpoints, or retained reports; Pi/OMP retains its existing one-time reparse on a parser-hash change.
- When a warm cost refresh reaches its time limit, it saves the remaining file work and completed discovery. Compatible shorter/wider history requests resume that work across the retained scan range; publication still waits for exact inventory validation.
- Inline cost charts preserve a slot for every day in that window, using the selected cost-bucket time zone and the snapshot's date. Missing days are zero only after history coverage is established; unscanned days and entries without prices remain unknown. Long windows fit within the menu width without dropping dates.
- **Hide personal information** replaces project/source names with numbered labels and hides their paths in the cost-history submenu; Usage & Spend also masks project names. Costs, tokens, grouping, and stored history are unchanged, and disabling the setting restores the original labels. This is display masking, not data deletion or export sanitization.
Expand Down
Loading