Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f50968f
Bound Codex cost scans and index cache aliases
Quicksaver Aug 10, 2026
4771f20
Complete Codex catch-up for retained session files
Quicksaver Aug 10, 2026
72928dd
Reconcile completed Codex catch-up cache state
Quicksaver Aug 10, 2026
8ed95c9
Preserve Codex catch-up completion across relaunches
Quicksaver Aug 10, 2026
b3f9433
Normalize Codex cache identities and resume stale scans
Quicksaver Aug 10, 2026
c04fe12
Queue Codex catch-up restarts and reconcile completed caches
Quicksaver Aug 10, 2026
3d125f5
Harden Codex catch-up completion and cancellation
Quicksaver Aug 10, 2026
57b2775
Fix primary Codex catch-up cancellation
Quicksaver Aug 10, 2026
c1a745a
Bound Codex catch-up candidate selection
Quicksaver Aug 10, 2026
f78ee26
Bound Codex cache reconciliation
Quicksaver Aug 10, 2026
d5e5ee8
Bound Codex post-scan accounting
Quicksaver Aug 10, 2026
3048fc8
Bound Codex lookback preparation
Quicksaver Aug 10, 2026
d571edf
Defer exact Codex completion proof
Quicksaver Aug 11, 2026
ff44240
perf: bound Codex progress accounting
steipete Aug 10, 2026
9e502ff
docs: note large-history indexing fix
steipete Aug 10, 2026
46b9109
fix: advance bounded Codex catch-up queue
steipete Aug 11, 2026
f7e0dd3
fix: validate Codex queue completion attempts
steipete Aug 11, 2026
f151228
fix: bound Codex queue materialization
steipete Aug 11, 2026
80ab22d
fix: keep Codex queue completion scanner-owned
steipete Aug 11, 2026
979ce5d
fix: retain buffered Codex queue retries
steipete Aug 11, 2026
382782c
fix: bound Codex queue maintenance
steipete Aug 11, 2026
3171dd3
fix: cap Codex inventory reconciliation
steipete Aug 11, 2026
28c7f15
fix: extend active Codex catch-up queues
steipete Aug 11, 2026
26a07bb
fix: finalize bounded Codex queue validation
steipete Aug 11, 2026
d492338
fix: retain pending Codex cache work
steipete Aug 11, 2026
4479f23
fix: reseed bounded Codex migrations
steipete Aug 11, 2026
11b9cd7
fix: remove superseded Codex sentinel
steipete Aug 11, 2026
5cdc017
Bound Codex usage discovery
Quicksaver Aug 11, 2026
7addd7d
Merge remote-tracking branch 'upstream/main' into fix/codex-usage-ind…
Quicksaver Aug 11, 2026
6cf3443
Preserve Codex usage cache upgrades
Quicksaver Aug 11, 2026
9279695
Drain bounded identity validation
Quicksaver Aug 11, 2026
e50a58a
Merge remote-tracking branch 'upstream/main' into fix/codex-usage-ind…
Quicksaver Aug 11, 2026
087ca11
Merge origin/main into codex catch-up cluster branch
steipete Aug 13, 2026
95ec249
fix: accelerate the configured Codex history window from Finish now
steipete Aug 13, 2026
2c55f0d
fix: detect semantic Codex cost catch-up progress across bounded passes
steipete Aug 13, 2026
5570459
docs: changelog for Codex catch-up cluster fixes
steipete Aug 13, 2026
e4066aa
test: remove duplicate cost performance lint directive
steipete Aug 13, 2026
01f7261
test: update Codex catch-up architecture anchors
steipete Aug 13, 2026
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## 0.49.5 — Unreleased

### Fixed
- Codex: keep large Usage & Spend history indexing bounded, durable across relaunches, and resumable after appends without replaying completed sessions (#2815, #2849). Thanks @Quicksaver for the fix and @Yoroin, @xiehaibin18, and @Astro-Han for reports and diagnostics!
- Codex: make Finish now accelerate the full configured cost-history window while retaining the 30-day floor (#2861, #2864). Thanks @thomaschow19 for the report and fix!
- Codex: detect semantic cost-history progress across bounded passes and stop cyclic catch-up without treating live appends as progress (#2815, #2861, #2844). Thanks @pavbar!

## 0.49.4 — 2026-08-13

### Added
Expand Down
19 changes: 16 additions & 3 deletions Sources/CodexBar/UsageStore+CodexCostCatchUp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ extension UsageStore {
if self.codexCostCatchUpTask != nil,
self.codexCostCatchUpScopeSignature == scopeSignature
{
guard self.codexCostCatchUpMode != mode else { return }
if self.codexCostCatchUpMode == mode {
// Hydration can observe a complete cache while the foreground refresh that follows it
// creates new tail work. Keep one restart queued so that refresh cannot lose the race
// with the existing task's completion cleanup.
self.codexCostCatchUpRestartRequested = true
return
}
self.codexCostCatchUpMode = mode
// Never cancel a pass while it may be committing a resume checkpoint. The new mode
// applies immediately after that bounded pass completes.
Expand Down Expand Up @@ -53,6 +59,10 @@ extension UsageStore {
self.codexCostCatchUpTask = nil
self.codexCostCatchUpToken = nil
self.codexCostCatchUpScopeSignature = nil
if self.codexCostCatchUpRestartRequested {
self.codexCostCatchUpRestartRequested = false
self.startCodexCostCatchUpIfNeeded(mode: self.codexCostCatchUpMode)
}
}
}
await self.runCodexCostCatchUp(context: context)
Expand All @@ -66,6 +76,7 @@ extension UsageStore {
self.codexCostCatchUpScopeSignature = nil
self.codexCostCatchUpStopRequested = false
self.codexCostCatchUpPassIsRunning = false
self.codexCostCatchUpRestartRequested = false
self.codexCostCatchUpActivity = nil
}

Expand All @@ -80,6 +91,7 @@ extension UsageStore {
func stopCodexCostCatchUp() {
guard self.codexCostCatchUpTask != nil else { return }
self.codexCostCatchUpStopRequested = true
self.codexCostCatchUpRestartRequested = false
guard !self.codexCostCatchUpPassIsRunning else { return }
if let activity = self.codexCostCatchUpActivity {
self.codexCostCatchUpActivity = CodexCostCatchUpActivity(
Expand All @@ -106,7 +118,8 @@ extension UsageStore {
context: context,
phase: status.pending ? .indexing : .complete)
var didAdvance = false
var previousActiveDuration: TimeInterval?
var (previousActiveDuration, seenProgressKeys): (TimeInterval?, Set<String>) =
(nil, [status.progressKey])
while status.pending {
do {
guard self.codexCostCatchUpContextIsCurrent(context) else { return }
Expand Down Expand Up @@ -183,7 +196,7 @@ extension UsageStore {
pauseReason: .user)
return
}
if nextStatus.pending, nextStatus.progressKey == status.progressKey {
if nextStatus.pending, !seenProgressKeys.insert(nextStatus.progressKey).inserted {
self.publishCodexCostCatchUpActivity(
status: nextStatus,
context: context,
Expand Down
27 changes: 22 additions & 5 deletions Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,20 @@ extension UsageStore {
return
}

let scopeSignature = accounts
let historyDays = max(SpendDashboardSource.scanDays, self.settings.costUsageHistoryDays)
let accountScopeSignature = accounts
.map { "\($0.id)|\($0.cacheIdentity)" }
.joined(separator: "\u{0}")
let scopeSignature = "\(historyDays)\u{0}\(accountScopeSignature)"
if self.spendDashboardCodexCostCatchUpTask != nil,
self.spendDashboardCodexCostCatchUpScopeSignature == scopeSignature
{
guard self.spendDashboardCodexCostCatchUpMode != mode else { return }
if self.spendDashboardCodexCostCatchUpMode == mode {
// A dashboard reload can discover fresh tail work while the previous task is
// completing. Queue one restart so that the new pending status retains a worker.
self.spendDashboardCodexCostCatchUpRestartRequested = true
return
}
self.spendDashboardCodexCostCatchUpMode = mode
// A bounded parser pass may be committing a resume checkpoint. Let it finish and
// apply the new mode before scheduling the next account instead of cancelling it.
Expand All @@ -51,7 +58,7 @@ extension UsageStore {
let context = SpendDashboardCodexCostCatchUpContext(
token: token,
accounts: accounts,
historyDays: SpendDashboardSource.scanDays,
historyDays: historyDays,
scopeSignature: scopeSignature,
providerConfigRevision: self.settings.providerConfigRevision(for: .codex),
costUsageSettingsRevision: self.settings.costUsageSettingsRevision)
Expand All @@ -68,6 +75,12 @@ extension UsageStore {
self.spendDashboardCodexCostCatchUpTask = nil
self.spendDashboardCodexCostCatchUpToken = nil
self.spendDashboardCodexCostCatchUpScopeSignature = nil
if self.spendDashboardCodexCostCatchUpRestartRequested {
self.spendDashboardCodexCostCatchUpRestartRequested = false
self.startSpendDashboardCodexCostCatchUpIfNeeded(
accounts: context.accounts,
mode: self.spendDashboardCodexCostCatchUpMode)
}
}
}
await self.runSpendDashboardCodexCostCatchUp(context: context)
Expand All @@ -77,6 +90,7 @@ extension UsageStore {
func stopSpendDashboardCodexCostCatchUp() {
guard self.spendDashboardCodexCostCatchUpTask != nil else { return }
self.spendDashboardCodexCostCatchUpStopRequested = true
self.spendDashboardCodexCostCatchUpRestartRequested = false
guard !self.spendDashboardCodexCostCatchUpPassIsRunning else { return }
if let activity = self.spendDashboardCodexCostCatchUpActivity {
self.spendDashboardCodexCostCatchUpActivity = CodexCostCatchUpActivity(
Expand All @@ -102,6 +116,7 @@ extension UsageStore {
self.spendDashboardCodexCostCatchUpScopeSignature = nil
self.spendDashboardCodexCostCatchUpStopRequested = false
self.spendDashboardCodexCostCatchUpPassIsRunning = false
self.spendDashboardCodexCostCatchUpRestartRequested = false
self.spendDashboardCodexCostCatchUpActivity = nil
}

Expand All @@ -117,7 +132,8 @@ extension UsageStore {

var didChangeCache = false
var previousActiveDuration: TimeInterval?
var stalledCacheIdentities: Set<String> = []
var (stalledCacheIdentities, seenKeysByCache) =
(Set<String>(), statuses.mapValues { Set([$0.progressKey]) })
while Self.spendDashboardCodexCatchUpIsPending(statuses) {
do {
guard self.spendDashboardCodexCostCatchUpContextIsCurrent(context) else { return }
Expand Down Expand Up @@ -197,7 +213,7 @@ extension UsageStore {
didChangeCache = didChangeCache || nextStatus.progressKey != previousStatus?.progressKey
statuses[account.cacheIdentity] = nextStatus
if nextStatus.pending,
nextStatus.progressKey == previousStatus?.progressKey
!seenKeysByCache[account.cacheIdentity, default: []].insert(nextStatus.progressKey).inserted
{
stalledCacheIdentities.insert(account.cacheIdentity)
} else {
Expand Down Expand Up @@ -245,6 +261,7 @@ extension UsageStore {
&& self.spendDashboardCodexCostCatchUpScopeSignature == context.scopeSignature
&& self.settings.providerConfigRevision(for: .codex) == context.providerConfigRevision
&& self.settings.costUsageSettingsRevision == context.costUsageSettingsRevision
&& max(SpendDashboardSource.scanDays, self.settings.costUsageHistoryDays) == context.historyDays
&& self.settings.isCostUsageEffectivelyEnabled(for: .codex)
&& self.isEnabled(.codex)
&& context.accounts.allSatisfy(SpendDashboardSource.codexAuthFingerprintMatches)
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,14 @@ final class UsageStore {
@ObservationIgnored var codexCostCatchUpMode: CodexCostCatchUpMode = .automatic
@ObservationIgnored var codexCostCatchUpStopRequested = false
@ObservationIgnored var codexCostCatchUpPassIsRunning = false
@ObservationIgnored var codexCostCatchUpRestartRequested = false
@ObservationIgnored var spendDashboardCodexCostCatchUpTask: Task<Void, Never>?
@ObservationIgnored var spendDashboardCodexCostCatchUpToken: UUID?
@ObservationIgnored var spendDashboardCodexCostCatchUpScopeSignature: String?
@ObservationIgnored var spendDashboardCodexCostCatchUpMode: CodexCostCatchUpMode = .automatic
@ObservationIgnored var spendDashboardCodexCostCatchUpStopRequested = false
@ObservationIgnored var spendDashboardCodexCostCatchUpPassIsRunning = false
@ObservationIgnored var spendDashboardCodexCostCatchUpRestartRequested = false
@ObservationIgnored var forcedRefreshEnrichmentTask: Task<Void, Never>?
@ObservationIgnored var forcedRefreshEnrichmentToken: UUID?
@ObservationIgnored var pendingForcedRefreshEnrichmentTask: Task<Void, Never>?
Expand Down
115 changes: 106 additions & 9 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,19 +312,12 @@ public struct CostUsageFetcher: Sendable {
}

let scoped = CostUsageScanner.codexCache(cache, scopedTo: roots)
var progressHasher = Hasher()
for (path, usage) in scoped.files.sorted(by: { $0.key < $1.key }) {
progressHasher.combine(path)
progressHasher.combine(usage.codexScanFileId)
progressHasher.combine(usage.parsedBytes)
progressHasher.combine(usage.size)
progressHasher.combine(usage.codexScanComplete)
}
let progressKey = self.codexScanProgressKey(cache: cache, scopedFiles: scoped.files)
let hasIncompleteFile = scoped.files.values.contains { $0.codexScanComplete == false }
let pending = cache.codexScanCatchUpPending == true || hasIncompleteFile
return CodexScanCatchUpStatus(
pending: pending,
progressKey: "\(scoped.files.count):\(progressHasher.finalize())",
progressKey: progressKey,
processedBytes: cache.codexScanProcessedBytes ?? 0,
totalBytes: cache.codexScanTotalBytes ?? 0,
completedFiles: cache.codexScanCompletedFiles ?? 0,
Expand Down Expand Up @@ -1348,6 +1341,110 @@ public struct CostUsageFetcher: Sendable {
}

extension CostUsageFetcher {
static func codexScanProgressKey(
cache: CostUsageCache,
scopedFiles: [String: CostUsageFileUsage]) -> String
{
var progressHasher = Hasher()
progressHasher.combine(cache.codexScanCompletedFiles)

for (path, usage) in scopedFiles.sorted(by: { $0.key < $1.key }) {
progressHasher.combine(path)
progressHasher.combine(usage.codexScanFileId)
progressHasher.combine(usage.codexScanComplete)
if usage.codexScanComplete == false {
progressHasher.combine(usage.parsedBytes)
progressHasher.combine(usage.size)
progressHasher.combine(usage.codexJSONLResumeState?.offset)
}
let hasBufferedRetry = usage.hasBufferedCodexForkRetryLines
progressHasher.combine(hasBufferedRetry)
if hasBufferedRetry {
progressHasher.combine(usage.forkedFromId)
progressHasher.combine(usage.forkBaselineDependencyKey)
progressHasher.combine(usage.codexBufferedSubagentLines?.isEmpty == false)
progressHasher.combine(usage.codexBufferedUnresolvedForkLines?.isEmpty == false)
}
}

if let discovery = cache.codexSessionDiscovery {
progressHasher.combine(discovery.generation)
progressHasher.combine(discovery.directoryPaths.count)
progressHasher.combine(discovery.nextDirectoryIndex)
progressHasher.combine(discovery.filePaths.count)
progressHasher.combine(discovery.nextFileIndex)
progressHasher.combine(discovery.headScan?.path)
progressHasher.combine(discovery.headScan?.offset)
progressHasher.combine(discovery.headScan?.resumeState?.offset)
progressHasher.combine(discovery.filePathBySessionId.count)
progressHasher.combine(discovery.missingSessionIds.sorted())
progressHasher.combine(discovery.pendingSessionIds.sorted())
progressHasher.combine(discovery.validationDirectoryIndex)
progressHasher.combine(discovery.isComplete)
} else {
progressHasher.combine("no-discovery")
}

if let lookback = cache.codexActiveLookbackState {
progressHasher.combine(lookback.scanSinceKey)
progressHasher.combine(lookback.rootPaths.sorted())
progressHasher.combine("next-day")
for (root, dayKey) in lookback.nextDayKeyByRoot.sorted(by: { $0.key < $1.key }) {
progressHasher.combine(root)
progressHasher.combine(dayKey)
}
progressHasher.combine("next-directory-offset")
progressHasher.combine(lookback.nextDirectoryOffsetByRoot == nil)
for (root, offset) in (lookback.nextDirectoryOffsetByRoot ?? [:]).sorted(by: { $0.key < $1.key }) {
progressHasher.combine(root)
progressHasher.combine(offset)
}
progressHasher.combine(lookback.completedRootPaths.sorted())
progressHasher.combine(lookback.pendingFilePaths.sorted())
progressHasher.combine(lookback.legacyRecursivePendingRootPaths.sorted())
progressHasher.combine("current-window-next-day")
progressHasher.combine(lookback.currentWindowNextDayKeyByRoot == nil)
for (root, dayKey) in (lookback.currentWindowNextDayKeyByRoot ?? [:]).sorted(by: { $0.key < $1.key }) {
progressHasher.combine(root)
progressHasher.combine(dayKey)
}
progressHasher.combine("current-window-directory-offset")
progressHasher.combine(lookback.currentWindowDirectoryOffsetByRoot == nil)
for (root, offset) in (lookback.currentWindowDirectoryOffsetByRoot ?? [:])
.sorted(by: { $0.key < $1.key })
{
progressHasher.combine(root)
progressHasher.combine(offset)
}
progressHasher.combine("completed-current-window-roots")
progressHasher.combine(lookback.completedCurrentWindowRootPaths == nil)
progressHasher.combine((lookback.completedCurrentWindowRootPaths ?? []).sorted())
progressHasher.combine("current-window-flat-directory-offset")
progressHasher.combine(lookback.currentWindowFlatDirectoryOffsetByRoot == nil)
for (root, offset) in (lookback.currentWindowFlatDirectoryOffsetByRoot ?? [:])
.sorted(by: { $0.key < $1.key })
{
progressHasher.combine(root)
progressHasher.combine(offset)
}
progressHasher.combine("completed-current-window-flat-roots")
progressHasher.combine(lookback.completedCurrentWindowFlatRootPaths == nil)
progressHasher.combine((lookback.completedCurrentWindowFlatRootPaths ?? []).sorted())
progressHasher.combine(lookback.cacheWideMigrationQueueActive)
} else {
progressHasher.combine("no-lookback")
}

if let inventoryPaths = cache.codexScanInventoryPaths {
progressHasher.combine("inventory")
progressHasher.combine(inventoryPaths.sorted())
} else {
progressHasher.combine("no-inventory")
}

return "v2:\(scopedFiles.count):\(progressHasher.finalize())"
}

fileprivate static func loadRemoteTokenSnapshot(
provider: UsageProvider,
environment: [String: String],
Expand Down
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 = "47144baa8daccf52"
static let value = "e2899fcb0234e5c1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct CostUsageCache: Codable, Equatable, @unchecked Sendable {
var codexScanTotalBytes: Int64?
var codexScanCompletedFiles: Int?
var codexScanTotalFiles: Int?
var codexScanInventoryPaths: [String]?
var codexPreviousReport: CostUsageCodexPreviousReport?
var codexSessionDiscovery: CostUsageCodexSessionDiscovery?
var codexActiveLookbackState: CostUsageCodexActiveLookbackState?
Expand All @@ -30,9 +31,16 @@ struct CostUsageCodexActiveLookbackState: Codable, Equatable {
var scanSinceKey: String
var rootPaths: [String]
var nextDayKeyByRoot: [String: String] = [:]
var nextDirectoryOffsetByRoot: [String: Int64]?
var completedRootPaths: [String] = []
var pendingFilePaths: [String] = []
var legacyRecursivePendingRootPaths: [String] = []
var currentWindowNextDayKeyByRoot: [String: String]?
var currentWindowDirectoryOffsetByRoot: [String: Int64]?
var completedCurrentWindowRootPaths: [String]?
var currentWindowFlatDirectoryOffsetByRoot: [String: Int64]?
var completedCurrentWindowFlatRootPaths: [String]?
var cacheWideMigrationQueueActive: Bool?
}

struct CostUsageCodexSessionDiscovery: Codable, Equatable {
Expand Down
Loading