Skip to content

Commit 2acf34c

Browse files
authored
fix: invalidate cached Codex fork baselines (#2118)
Invalidate cached fork totals when their parent rollout changes, appears, or is reselected. Bind the recorded parent dependency to the exact snapshot parsed, including a bounded retry when the parent mutates during the read. Add regression coverage for concurrent parent/child growth, late parent creation, parent-file reselection, and snapshot/dependency consistency. Co-authored-by: Xu Xiang <xx205@outlook.com>
1 parent 4756e1c commit 2acf34c

6 files changed

Lines changed: 446 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 0.43.1 — Unreleased
44

55
### Fixed
6+
- Codex cost usage: invalidate cached fork totals when the parent session appears, changes, or resolves to a different file, preventing stale inherited baselines. Thanks @xx205!
67
- Cursor: bind interactive account login to one readable browser, preserve the active session on cancellation or failure, and prevent background refreshes from replacing the selected account. Thanks @chapati23!
78
- Menu bar: prevent duplicate provider items when usage updates re-enter initial status-item setup (#2162). Thanks @ss251!
89
- Codex cost usage: count restarted subagent token counters without subtracting the parent's unrelated cumulative baseline (#2193). Thanks @qiuruiyu and @harjothkhara!
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 = "f3142eea669758bf"
4+
static let value = "b7dd32352b439226"
55
}

Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ enum CostUsageCacheIO {
66
/// totals are counted, so every earlier cache must be rebuilt.
77
private static let compatibleCodexProducerKeys: Set<String> = []
88

9+
/// Parsing and attribution changes rotate the Codex parser producer key.
10+
/// Increment this artifact version only when the stored schema or cache layout becomes incompatible.
911
private static func artifactVersion(for provider: UsageProvider) -> Int {
1012
switch provider {
1113
case .codex:
@@ -141,6 +143,7 @@ struct CostUsageFileUsage: Codable {
141143
var lastCodexTurnID: String?
142144
var sessionId: String?
143145
var forkedFromId: String?
146+
var forkBaselineDependencyKey: String?
144147
var projectPath: String?
145148
var canonicalProjectPath: String?
146149
var codexCostCacheComplete: Bool?

Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ extension CostUsageScanner {
1818
}
1919

2020
func load(_ loader: (URL?) -> ModelsDevCatalog?) -> ModelsDevCatalog {
21-
if let catalog { return catalog }
21+
if let catalog {
22+
return catalog
23+
}
2224
let loaded = loader(self.cacheRoot) ?? ModelsDevCatalog(providers: [:])
2325
self.catalog = loaded
2426
return loaded
@@ -285,6 +287,7 @@ extension CostUsageScanner {
285287
lastCodexTurnID: String? = nil,
286288
sessionId: String? = nil,
287289
forkedFromId: String? = nil,
290+
forkBaselineDependencyKey: String? = nil,
288291
projectPath: String? = nil,
289292
canonicalProjectPath: String? = nil,
290293
codexCostCacheComplete: Bool? = true,
@@ -314,6 +317,7 @@ extension CostUsageScanner {
314317
lastCodexTurnID: lastCodexTurnID,
315318
sessionId: sessionId,
316319
forkedFromId: forkedFromId,
320+
forkBaselineDependencyKey: forkBaselineDependencyKey,
317321
projectPath: projectPath,
318322
canonicalProjectPath: canonicalProjectPath,
319323
codexCostCacheComplete: codexCostCacheComplete,
@@ -705,6 +709,7 @@ extension CostUsageScanner {
705709
lastCodexTurnID: usage.lastCodexTurnID,
706710
sessionId: usage.sessionId,
707711
forkedFromId: usage.forkedFromId,
712+
forkBaselineDependencyKey: usage.forkBaselineDependencyKey,
708713
projectPath: usage.projectPath,
709714
canonicalProjectPath: usage.canonicalProjectPath,
710715
codexCostNanos: Self.mergeCostMaps(
@@ -883,7 +888,7 @@ extension CostUsageScanner {
883888
input: CodexFileScanInput,
884889
context: CodexFileScanContext,
885890
cache: inout CostUsageCache,
886-
state: inout CodexScanState) -> Bool
891+
state: inout CodexScanState) throws -> Bool
887892
{
888893
guard let cached = input.cached else { return false }
889894
let needsSessionId = cached.sessionId == nil
@@ -900,6 +905,13 @@ extension CostUsageScanner {
900905
if Self.cachedCodexRowsNeedIdentityRescan(cached) {
901906
return false
902907
}
908+
if let parentSessionId = cached.forkedFromId {
909+
guard let cachedDependencyKey = cached.forkBaselineDependencyKey else { return false }
910+
let currentDependencyKey = try context.resources.inheritedResolver
911+
.currentDependencyKey(for: parentSessionId)
912+
guard cachedDependencyKey == currentDependencyKey else { return false }
913+
}
914+
903915
if sessionAlreadyContributed {
904916
guard !cachedRows.isEmpty else { return false }
905917
let uniqueRows = Self.uniqueCodexRows(
@@ -1119,6 +1131,11 @@ extension CostUsageScanner {
11191131
range: context.range,
11201132
inheritedTotalsResolver: context.resources.inheritedResolver.inheritedTotals(for:atOrBefore:),
11211133
checkCancellation: context.checkCancellation)
1134+
let forkBaselineDependencyKey: String? = if let parentSessionId = parsed.forkedFromId {
1135+
context.resources.inheritedResolver.dependencyKeyUsed(for: parentSessionId)
1136+
} else {
1137+
nil
1138+
}
11221139
let sessionId = parsed.sessionId ?? input.cached?.sessionId
11231140
let projectPath = parsed.projectPath ?? input.cached?.projectPath
11241141
let canonicalProjectPath = parsed.projectPath.map {
@@ -1163,6 +1180,7 @@ extension CostUsageScanner {
11631180
lastCodexTurnID: parsed.lastCodexTurnID,
11641181
sessionId: sessionId,
11651182
forkedFromId: parsed.forkedFromId,
1183+
forkBaselineDependencyKey: forkBaselineDependencyKey,
11661184
projectPath: projectPath,
11671185
canonicalProjectPath: canonicalProjectPath,
11681186
codexCostNanos: Self.mergeCostMaps(
@@ -1501,16 +1519,24 @@ extension Data {
15011519

15021520
extension [Int] {
15031521
subscript(safe index: Int) -> Int? {
1504-
if index < 0 { return nil }
1505-
if index >= self.count { return nil }
1522+
if index < 0 {
1523+
return nil
1524+
}
1525+
if index >= self.count {
1526+
return nil
1527+
}
15061528
return self[index]
15071529
}
15081530
}
15091531

15101532
extension [UInt8] {
15111533
subscript(safe index: Int) -> UInt8? {
1512-
if index < 0 { return nil }
1513-
if index >= self.count { return nil }
1534+
if index < 0 {
1535+
return nil
1536+
}
1537+
if index >= self.count {
1538+
return nil
1539+
}
15141540
return self[index]
15151541
}
15161542
}

Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -608,9 +608,14 @@ enum CostUsageScanner {
608608
}
609609

610610
final class CodexInheritedTotalsResolver {
611+
private struct SnapshotResolution {
612+
let dependencyKey: String?
613+
let snapshots: [CodexTimestampedTotals]?
614+
}
615+
611616
private let fileIndex: CodexSessionFileIndex
612617
private let checkCancellation: CancellationCheck?
613-
private var snapshotsBySessionId: [String: [CodexTimestampedTotals]] = [:]
618+
private var snapshotResolutions: [String: SnapshotResolution] = [:]
614619

615620
init(fileIndex: CodexSessionFileIndex, checkCancellation: CancellationCheck?) {
616621
self.fileIndex = fileIndex
@@ -630,7 +635,7 @@ enum CostUsageScanner {
630635
"Codex cost usage could not parse fork timestamp; falling back to lexical comparison",
631636
metadata: ["sessionId": sessionId, "timestamp": cutoffTimestamp])
632637
}
633-
guard let snapshots = try self.snapshots(for: sessionId) else { return .unresolved }
638+
guard let snapshots = try self.snapshotResolution(for: sessionId).snapshots else { return .unresolved }
634639
var inherited: CostUsageCodexTotals?
635640
for snapshot in snapshots {
636641
let isAtOrBefore: Bool = if let snapshotDate = snapshot.date, let cutoffDate {
@@ -645,38 +650,90 @@ enum CostUsageScanner {
645650
return .resolved(inherited)
646651
}
647652

648-
private func snapshots(for sessionId: String) throws -> [CodexTimestampedTotals]? {
649-
if let cached = self.snapshotsBySessionId[sessionId] {
653+
func currentDependencyKey(for sessionId: String) throws -> String {
654+
guard let fileURL = try self.fileIndex.fileURL(for: sessionId) else {
655+
return "missing:\(sessionId)"
656+
}
657+
return self.dependencyKey(for: sessionId, fileURL: fileURL)
658+
}
659+
660+
func dependencyKeyUsed(for sessionId: String) -> String? {
661+
self.snapshotResolutions[sessionId]?.dependencyKey
662+
}
663+
664+
private func dependencyKey(for sessionId: String, fileURL: URL) -> String {
665+
let metadata = CostUsageScanner.codexFileMetadata(fileURL: fileURL)
666+
return [
667+
"file",
668+
sessionId,
669+
fileURL.standardizedFileURL.path,
670+
metadata.fileId ?? "unknown",
671+
String(metadata.mtimeUnixMs),
672+
String(metadata.size),
673+
].joined(separator: "|")
674+
}
675+
676+
private func snapshotResolution(for sessionId: String) throws -> SnapshotResolution {
677+
if let cached = self.snapshotResolutions[sessionId] {
650678
return cached
651679
}
652680
try self.checkCancellation?()
653681
guard let fileURL = try self.fileIndex.fileURL(for: sessionId) else {
654682
CostUsageScanner.log.warning(
655683
"Codex cost usage parent session file not found",
656684
metadata: ["sessionId": sessionId])
657-
return nil
685+
let resolution = SnapshotResolution(
686+
dependencyKey: "missing:\(sessionId)",
687+
snapshots: nil)
688+
self.snapshotResolutions[sessionId] = resolution
689+
return resolution
658690
}
659-
let parsed = try CostUsageScanner.parseCodexTokenSnapshots(
660-
fileURL: fileURL,
661-
checkCancellation: self.checkCancellation)
662-
guard let parsedSessionId = parsed.sessionId else {
663-
CostUsageScanner.log.warning(
664-
"Codex cost usage parent session missing session metadata",
665-
metadata: ["sessionId": sessionId, "path": fileURL.path])
666-
return nil
667-
}
668-
if parsedSessionId != sessionId {
669-
CostUsageScanner.log.warning(
670-
"Codex cost usage parent session resolved to mismatched session id",
671-
metadata: [
672-
"requestedSessionId": sessionId,
673-
"resolvedSessionId": parsedSessionId,
674-
"path": fileURL.path,
675-
])
676-
return nil
691+
692+
for _ in 0..<2 {
693+
let dependencyKeyBeforeParse = self.dependencyKey(for: sessionId, fileURL: fileURL)
694+
let parsed = try CostUsageScanner.parseCodexTokenSnapshots(
695+
fileURL: fileURL,
696+
checkCancellation: self.checkCancellation)
697+
let dependencyKeyAfterParse = self.dependencyKey(for: sessionId, fileURL: fileURL)
698+
guard dependencyKeyBeforeParse == dependencyKeyAfterParse else { continue }
699+
700+
guard let parsedSessionId = parsed.sessionId else {
701+
CostUsageScanner.log.warning(
702+
"Codex cost usage parent session missing session metadata",
703+
metadata: ["sessionId": sessionId, "path": fileURL.path])
704+
let resolution = SnapshotResolution(
705+
dependencyKey: dependencyKeyAfterParse,
706+
snapshots: nil)
707+
self.snapshotResolutions[sessionId] = resolution
708+
return resolution
709+
}
710+
if parsedSessionId != sessionId {
711+
CostUsageScanner.log.warning(
712+
"Codex cost usage parent session resolved to mismatched session id",
713+
metadata: [
714+
"requestedSessionId": sessionId,
715+
"resolvedSessionId": parsedSessionId,
716+
"path": fileURL.path,
717+
])
718+
let resolution = SnapshotResolution(
719+
dependencyKey: dependencyKeyAfterParse,
720+
snapshots: nil)
721+
self.snapshotResolutions[sessionId] = resolution
722+
return resolution
723+
}
724+
let resolution = SnapshotResolution(
725+
dependencyKey: dependencyKeyAfterParse,
726+
snapshots: parsed.snapshots)
727+
self.snapshotResolutions[sessionId] = resolution
728+
return resolution
677729
}
678-
self.snapshotsBySessionId[sessionId] = parsed.snapshots
679-
return parsed.snapshots
730+
731+
CostUsageScanner.log.warning(
732+
"Codex cost usage parent session changed while reading; deferring inherited baseline",
733+
metadata: ["sessionId": sessionId, "path": fileURL.path])
734+
let resolution = SnapshotResolution(dependencyKey: nil, snapshots: nil)
735+
self.snapshotResolutions[sessionId] = resolution
736+
return resolution
680737
}
681738
}
682739

@@ -2394,7 +2451,7 @@ enum CostUsageScanner {
23942451
let cached = cache.files[metadata.path]
23952452

23962453
let input = CodexFileScanInput(fileURL: fileURL, metadata: metadata, cached: cached)
2397-
if Self.keepCachedCodexFileIfFresh(input: input, context: context, cache: &cache, state: &state) {
2454+
if try Self.keepCachedCodexFileIfFresh(input: input, context: context, cache: &cache, state: &state) {
23982455
return
23992456
}
24002457
if try Self.appendCodexFileIncrementIfPossible(input: input, context: context, cache: &cache, state: &state) {

0 commit comments

Comments
 (0)