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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.43.1 — Unreleased

### Fixed
- 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!
- 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!
- Menu bar: prevent duplicate provider items when usage updates re-enter initial status-item setup (#2162). Thanks @ss251!
- Codex cost usage: count restarted subagent token counters without subtracting the parent's unrelated cumulative baseline (#2193). Thanks @qiuruiyu and @harjothkhara!
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 = "f3142eea669758bf"
static let value = "b7dd32352b439226"
}
3 changes: 3 additions & 0 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ enum CostUsageCacheIO {
/// totals are counted, so every earlier cache must be rebuilt.
private static let compatibleCodexProducerKeys: Set<String> = []

/// Parsing and attribution changes rotate the Codex parser producer key.
/// Increment this artifact version only when the stored schema or cache layout becomes incompatible.
private static func artifactVersion(for provider: UsageProvider) -> Int {
switch provider {
case .codex:
Expand Down Expand Up @@ -141,6 +143,7 @@ struct CostUsageFileUsage: Codable {
var lastCodexTurnID: String?
var sessionId: String?
var forkedFromId: String?
var forkBaselineDependencyKey: String?
var projectPath: String?
var canonicalProjectPath: String?
var codexCostCacheComplete: Bool?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ extension CostUsageScanner {
}

func load(_ loader: (URL?) -> ModelsDevCatalog?) -> ModelsDevCatalog {
if let catalog { return catalog }
if let catalog {
return catalog
}
let loaded = loader(self.cacheRoot) ?? ModelsDevCatalog(providers: [:])
self.catalog = loaded
return loaded
Expand Down Expand Up @@ -285,6 +287,7 @@ extension CostUsageScanner {
lastCodexTurnID: String? = nil,
sessionId: String? = nil,
forkedFromId: String? = nil,
forkBaselineDependencyKey: String? = nil,
projectPath: String? = nil,
canonicalProjectPath: String? = nil,
codexCostCacheComplete: Bool? = true,
Expand Down Expand Up @@ -314,6 +317,7 @@ extension CostUsageScanner {
lastCodexTurnID: lastCodexTurnID,
sessionId: sessionId,
forkedFromId: forkedFromId,
forkBaselineDependencyKey: forkBaselineDependencyKey,
projectPath: projectPath,
canonicalProjectPath: canonicalProjectPath,
codexCostCacheComplete: codexCostCacheComplete,
Expand Down Expand Up @@ -705,6 +709,7 @@ extension CostUsageScanner {
lastCodexTurnID: usage.lastCodexTurnID,
sessionId: usage.sessionId,
forkedFromId: usage.forkedFromId,
forkBaselineDependencyKey: usage.forkBaselineDependencyKey,
projectPath: usage.projectPath,
canonicalProjectPath: usage.canonicalProjectPath,
codexCostNanos: Self.mergeCostMaps(
Expand Down Expand Up @@ -883,7 +888,7 @@ extension CostUsageScanner {
input: CodexFileScanInput,
context: CodexFileScanContext,
cache: inout CostUsageCache,
state: inout CodexScanState) -> Bool
state: inout CodexScanState) throws -> Bool
{
guard let cached = input.cached else { return false }
let needsSessionId = cached.sessionId == nil
Expand All @@ -900,6 +905,13 @@ extension CostUsageScanner {
if Self.cachedCodexRowsNeedIdentityRescan(cached) {
return false
}
if let parentSessionId = cached.forkedFromId {
guard let cachedDependencyKey = cached.forkBaselineDependencyKey else { return false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve warm-cache hits for subagent forks

When the cached file is a Codex subagent with forkedFromId, the parser intentionally treats the parent id as lineage rather than a baseline and never records a parent dependency key, so forkBaselineDependencyKey remains nil. This guard then rejects an otherwise fresh cache entry on every refresh, causing unchanged subagent logs to be fully reparsed repeatedly; in subagent-heavy histories this defeats the warm cache and makes refresh cost scale with old logs. Consider skipping this dependency check for independent/subagent forks or storing a non-parent sentinel.

Useful? React with 👍 / 👎.

let currentDependencyKey = try context.resources.inheritedResolver
.currentDependencyKey(for: parentSessionId)
guard cachedDependencyKey == currentDependencyKey else { return false }
}

if sessionAlreadyContributed {
guard !cachedRows.isEmpty else { return false }
let uniqueRows = Self.uniqueCodexRows(
Expand Down Expand Up @@ -1119,6 +1131,11 @@ extension CostUsageScanner {
range: context.range,
inheritedTotalsResolver: context.resources.inheritedResolver.inheritedTotals(for:atOrBefore:),
checkCancellation: context.checkCancellation)
let forkBaselineDependencyKey: String? = if let parentSessionId = parsed.forkedFromId {
context.resources.inheritedResolver.dependencyKeyUsed(for: parentSessionId)
} else {
nil
}
let sessionId = parsed.sessionId ?? input.cached?.sessionId
let projectPath = parsed.projectPath ?? input.cached?.projectPath
let canonicalProjectPath = parsed.projectPath.map {
Expand Down Expand Up @@ -1163,6 +1180,7 @@ extension CostUsageScanner {
lastCodexTurnID: parsed.lastCodexTurnID,
sessionId: sessionId,
forkedFromId: parsed.forkedFromId,
forkBaselineDependencyKey: forkBaselineDependencyKey,
projectPath: projectPath,
canonicalProjectPath: canonicalProjectPath,
codexCostNanos: Self.mergeCostMaps(
Expand Down Expand Up @@ -1501,16 +1519,24 @@ extension Data {

extension [Int] {
subscript(safe index: Int) -> Int? {
if index < 0 { return nil }
if index >= self.count { return nil }
if index < 0 {
return nil
}
if index >= self.count {
return nil
}
return self[index]
}
}

extension [UInt8] {
subscript(safe index: Int) -> UInt8? {
if index < 0 { return nil }
if index >= self.count { return nil }
if index < 0 {
return nil
}
if index >= self.count {
return nil
}
return self[index]
}
}
Expand Down
109 changes: 83 additions & 26 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -608,9 +608,14 @@ enum CostUsageScanner {
}

final class CodexInheritedTotalsResolver {
private struct SnapshotResolution {
let dependencyKey: String?
let snapshots: [CodexTimestampedTotals]?
}

private let fileIndex: CodexSessionFileIndex
private let checkCancellation: CancellationCheck?
private var snapshotsBySessionId: [String: [CodexTimestampedTotals]] = [:]
private var snapshotResolutions: [String: SnapshotResolution] = [:]

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

private func snapshots(for sessionId: String) throws -> [CodexTimestampedTotals]? {
if let cached = self.snapshotsBySessionId[sessionId] {
func currentDependencyKey(for sessionId: String) throws -> String {
guard let fileURL = try self.fileIndex.fileURL(for: sessionId) else {
return "missing:\(sessionId)"
}
return self.dependencyKey(for: sessionId, fileURL: fileURL)
}

func dependencyKeyUsed(for sessionId: String) -> String? {
self.snapshotResolutions[sessionId]?.dependencyKey
}

private func dependencyKey(for sessionId: String, fileURL: URL) -> String {
let metadata = CostUsageScanner.codexFileMetadata(fileURL: fileURL)
return [
"file",
sessionId,
fileURL.standardizedFileURL.path,
metadata.fileId ?? "unknown",
String(metadata.mtimeUnixMs),
String(metadata.size),
].joined(separator: "|")
}

private func snapshotResolution(for sessionId: String) throws -> SnapshotResolution {
if let cached = self.snapshotResolutions[sessionId] {
return cached
}
try self.checkCancellation?()
guard let fileURL = try self.fileIndex.fileURL(for: sessionId) else {
CostUsageScanner.log.warning(
"Codex cost usage parent session file not found",
metadata: ["sessionId": sessionId])
return nil
let resolution = SnapshotResolution(
dependencyKey: "missing:\(sessionId)",
snapshots: nil)
self.snapshotResolutions[sessionId] = resolution
return resolution
}
let parsed = try CostUsageScanner.parseCodexTokenSnapshots(
fileURL: fileURL,
checkCancellation: self.checkCancellation)
guard let parsedSessionId = parsed.sessionId else {
CostUsageScanner.log.warning(
"Codex cost usage parent session missing session metadata",
metadata: ["sessionId": sessionId, "path": fileURL.path])
return nil
}
if parsedSessionId != sessionId {
CostUsageScanner.log.warning(
"Codex cost usage parent session resolved to mismatched session id",
metadata: [
"requestedSessionId": sessionId,
"resolvedSessionId": parsedSessionId,
"path": fileURL.path,
])
return nil

for _ in 0..<2 {
let dependencyKeyBeforeParse = self.dependencyKey(for: sessionId, fileURL: fileURL)
let parsed = try CostUsageScanner.parseCodexTokenSnapshots(
fileURL: fileURL,
checkCancellation: self.checkCancellation)
let dependencyKeyAfterParse = self.dependencyKey(for: sessionId, fileURL: fileURL)
guard dependencyKeyBeforeParse == dependencyKeyAfterParse else { continue }

guard let parsedSessionId = parsed.sessionId else {
CostUsageScanner.log.warning(
"Codex cost usage parent session missing session metadata",
metadata: ["sessionId": sessionId, "path": fileURL.path])
let resolution = SnapshotResolution(
dependencyKey: dependencyKeyAfterParse,
snapshots: nil)
self.snapshotResolutions[sessionId] = resolution
return resolution
}
if parsedSessionId != sessionId {
CostUsageScanner.log.warning(
"Codex cost usage parent session resolved to mismatched session id",
metadata: [
"requestedSessionId": sessionId,
"resolvedSessionId": parsedSessionId,
"path": fileURL.path,
])
let resolution = SnapshotResolution(
dependencyKey: dependencyKeyAfterParse,
snapshots: nil)
self.snapshotResolutions[sessionId] = resolution
return resolution
}
let resolution = SnapshotResolution(
dependencyKey: dependencyKeyAfterParse,
snapshots: parsed.snapshots)
self.snapshotResolutions[sessionId] = resolution
return resolution
}
self.snapshotsBySessionId[sessionId] = parsed.snapshots
return parsed.snapshots

CostUsageScanner.log.warning(
"Codex cost usage parent session changed while reading; deferring inherited baseline",
metadata: ["sessionId": sessionId, "path": fileURL.path])
let resolution = SnapshotResolution(dependencyKey: nil, snapshots: nil)
self.snapshotResolutions[sessionId] = resolution
return resolution
}
}

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

let input = CodexFileScanInput(fileURL: fileURL, metadata: metadata, cached: cached)
if Self.keepCachedCodexFileIfFresh(input: input, context: context, cache: &cache, state: &state) {
if try Self.keepCachedCodexFileIfFresh(input: input, context: context, cache: &cache, state: &state) {
return
}
if try Self.appendCodexFileIncrementIfPossible(input: input, context: context, cache: &cache, state: &state) {
Expand Down
Loading