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 @@ -6,6 +6,7 @@
- Plugins: honor the “Usage bars fill” remaining/used setting in user-installed provider cards, keeping percentage labels and bar direction aligned (#2749). Thanks @RyloRiz!
- Sub2API: localize and group menu-card quota labels and request, token, and cost totals into a compact usage summary (#2835). Thanks @weirdo-adam!
- Codex: avoid repeatedly converting historical token snapshots during cost-cache refreshes, preventing sustained CPU usage on large session histories.
- Codex: make automatic cost-history catch-up near-idle and limit local-history scans to provider refreshes with a 15-minute energy floor.

## 0.49.1 — 2026-08-09

Expand Down
6 changes: 3 additions & 3 deletions Sources/CodexBar/CodexCostCatchUpPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ struct CodexCostCatchUpPolicy: Sendable {
}

let dutyCycle = switch input.powerSource {
case .ac: 0.20
case .battery: 0.05
case .unknown: 0.15
case .ac: 0.001
case .battery: 0.0002
case .unknown: 0.0005
}
let activeDuration = max(0, input.previousActiveDuration ?? Self.automaticBurstDuration)
let delay = activeDuration * (1 - dutyCycle) / dutyCycle
Expand Down
17 changes: 1 addition & 16 deletions Sources/CodexBar/UsageStore+TokenRefreshSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,6 @@ extension UsageStore {
case providers([ProviderInstanceID])
}

func startTokenTimer() {
self.tokenTimerTask?.cancel()
guard let wait = self.tokenFetchTTL else { return }
self.tokenTimerTask = Task.detached(priority: .utility) { [weak self] in
while !Task.isCancelled {
do {
try await Task.sleep(for: .seconds(wait))
} catch {
return
}
await self?.scheduleTokenRefresh()
}
}
}

func scheduleTokenRefresh() {
guard self.tokenRefreshSequenceTask == nil, !self.hasForcedRefreshEnrichmentInFlight else { return }
if self.startPendingTokenRefreshRetryIfPossible() {
Expand All @@ -43,7 +28,7 @@ extension UsageStore {
activeProvider != provider.instanceID
{
// A scoped user refresh can run beside unrelated scheduled work. The scheduled
// sequence still owns the shared slot, so the timer cannot introduce a third pass.
// sequence still owns the shared slot, so provider refreshes cannot introduce a third pass.
await self.refreshTokenUsage(provider, force: true)
self.scheduleMemoryPressureRelief()
return
Expand Down
11 changes: 3 additions & 8 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ extension UsageStore {
self.probeLogs = [:]
guard self.startupBehavior.automaticallyStartsBackgroundWork else { return }
self.startTimer()
self.startTokenTimer()
self.updateProviderRuntimes()
let enabledNow = Set(self.settings.enabledProvidersOrdered(
metadataByProvider: self.providerMetadata))
Expand Down Expand Up @@ -342,7 +341,6 @@ final class UsageStore {
/// In-memory only; paths and session identities never enter the refresh policy.
@ObservationIgnored private(set) var lastCodingActivityAt: Date?
@ObservationIgnored var adaptiveRefreshScheduledAt: Date?
@ObservationIgnored var tokenTimerTask: Task<Void, Never>?
@ObservationIgnored var tokenRefreshSequenceTask: Task<Void, Never>?
@ObservationIgnored var tokenRefreshSequenceToken: UUID?
@ObservationIgnored var tokenRefreshSequenceProvider: ProviderInstanceID?
Expand Down Expand Up @@ -424,10 +422,9 @@ final class UsageStore {
@ObservationIgnored private var hasCompletedInitialRefresh: Bool = false
@ObservationIgnored private let providerAvailabilityCacheTTL: TimeInterval = 1
@ObservationIgnored let accountInfoCacheTTL: TimeInterval = 30
/// Token scans can cause an additional widget snapshot publication. Keep the shortest automatic
/// cadence at five minutes so one- and two-minute provider refreshes do not exhaust WidgetKit's
/// reload budget or repeatedly traverse large local histories.
static let minimumTokenFetchTTL: TimeInterval = 5 * 60
/// Energy/WidgetKit floor for expensive local-history scans and their additional snapshot publications.
/// Faster provider refreshes still update quota/status normally, but reuse token-cost history within this TTL.
static let minimumTokenFetchTTL: TimeInterval = 15 * 60

var tokenFetchTTL: TimeInterval? {
Self.tokenFetchTTL(
Expand Down Expand Up @@ -540,7 +537,6 @@ final class UsageStore {
}
Task { await self.refresh(enrichmentMode: .automatic) }
self.startTimer()
self.startTokenTimer()
}

var iconStyle: IconStyle {
Expand Down Expand Up @@ -929,7 +925,6 @@ final class UsageStore {

deinit {
self.timerTask?.cancel()
self.tokenTimerTask?.cancel()
self.tokenRefreshSequenceTask?.cancel()
self.codexCostCatchUpTask?.cancel()
self.forcedRefreshEnrichmentTask?.cancel()
Expand Down
222 changes: 214 additions & 8 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageClaudeCache.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,189 @@
import Foundation

struct CostUsageClaudeFileStamp: Equatable, Sendable {
let fileID: String
let size: Int64
let modifiedSeconds: Int64
let modifiedNanoseconds: Int64

var mtimeUnixMs: Int64 {
self.modifiedSeconds * 1000 + self.modifiedNanoseconds / 1_000_000
}

static func read(at url: URL) -> Self? {
var info = stat()
guard url.path.withCString({ fstatat(AT_FDCWD, $0, &info, 0) }) == 0 else { return nil }
guard info.st_mode & mode_t(S_IFMT) == mode_t(S_IFREG) else { return nil }
#if os(Linux)
let modifiedSeconds = Int64(info.st_mtim.tv_sec)
let modifiedNanoseconds = Int64(info.st_mtim.tv_nsec)
#else
let modifiedSeconds = Int64(info.st_mtimespec.tv_sec)
let modifiedNanoseconds = Int64(info.st_mtimespec.tv_nsec)
#endif
return Self(
fileID: "\(info.st_dev):\(info.st_ino)",
size: Int64(info.st_size),
modifiedSeconds: modifiedSeconds,
modifiedNanoseconds: modifiedNanoseconds)
}
}

struct CostUsageClaudeReportMemoKey: Equatable, Sendable {
let provider: UsageProvider
let providerFilter: String
let sinceKey: String
let untilKey: String
let scanSinceKey: String
let scanUntilKey: String
let timeZoneIdentifier: String
let roots: [String]
let cacheArtifactStamp: CostUsageClaudeFileStamp?
let pricingArtifactStamp: CostUsageClaudeFileStamp?

var scanConfiguration: ScanConfiguration {
ScanConfiguration(
provider: self.provider,
providerFilter: self.providerFilter,
timeZoneIdentifier: self.timeZoneIdentifier,
roots: self.roots)
}

struct ScanConfiguration: Equatable, Sendable {
let provider: UsageProvider
let providerFilter: String
let timeZoneIdentifier: String
let roots: [String]
}
}

final class CostUsageClaudeReportMemo: @unchecked Sendable {
struct Entry {
let sourceInventory: [String: CostUsageClaudeFileStamp]
let reportKey: CostUsageClaudeReportMemoKey
let report: CostUsageDailyReport
}

static let shared = CostUsageClaudeReportMemo()

private struct StoredEntry {
let entry: Entry
let generation: UInt64
}

private let lock = NSLock()
private let capacity = 8
private var generation: UInt64 = 0
private var entries: [String: StoredEntry] = [:]

func entry(provider: UsageProvider, canonicalCachePath: String) -> Entry? {
let key = Self.key(provider: provider, canonicalCachePath: canonicalCachePath)
self.lock.lock()
defer { self.lock.unlock() }
return self.entries[key]?.entry
}

func store(
provider: UsageProvider,
canonicalCachePath: String,
sourceInventory: [String: CostUsageClaudeFileStamp],
reportKey: CostUsageClaudeReportMemoKey,
report: CostUsageDailyReport)
{
let key = Self.key(provider: provider, canonicalCachePath: canonicalCachePath)
self.lock.lock()
defer { self.lock.unlock() }
self.generation &+= 1
self.entries[key] = StoredEntry(
entry: Entry(sourceInventory: sourceInventory, reportKey: reportKey, report: report),
generation: self.generation)
if self.entries.count > self.capacity,
let oldest = self.entries.min(by: { $0.value.generation < $1.value.generation })?.key
{
self.entries.removeValue(forKey: oldest)
}
}

#if DEBUG
func evict(provider: UsageProvider, canonicalCachePath: String) {
let key = Self.key(provider: provider, canonicalCachePath: canonicalCachePath)
self.lock.lock()
defer { self.lock.unlock() }
self.entries.removeValue(forKey: key)
}
#endif

private static func key(provider: UsageProvider, canonicalCachePath: String) -> String {
"\(provider.rawValue)|\(canonicalCachePath)"
}
}

#if DEBUG
extension CostUsageScanner {
enum ClaudeScanWork: Sendable {
case cacheDecode
case transcriptParse
case reconcile
case cacheEncode
case reprice
}

struct ClaudeScanWorkMetrics: Equatable, Sendable {
var cacheDecodes = 0
var transcriptParses = 0
var reconciliations = 0
var cacheEncodes = 0
var repricedRows = 0
}

final class ClaudeScanWorkRecorder: @unchecked Sendable {
private let lock = NSLock()
private var metrics = ClaudeScanWorkMetrics()

func record(_ work: ClaudeScanWork) {
self.lock.lock()
defer { self.lock.unlock() }
switch work {
case .cacheDecode: self.metrics.cacheDecodes += 1
case .transcriptParse: self.metrics.transcriptParses += 1
case .reconcile: self.metrics.reconciliations += 1
case .cacheEncode: self.metrics.cacheEncodes += 1
case .reprice: self.metrics.repricedRows += 1
}
}

func snapshot() -> ClaudeScanWorkMetrics {
self.lock.lock()
defer { self.lock.unlock() }
return self.metrics
}
}

@TaskLocal private static var claudeScanWorkRecorder: ClaudeScanWorkRecorder?

static func withClaudeScanWorkRecorderForTesting<T>(
_ recorder: ClaudeScanWorkRecorder,
operation: () throws -> T) rethrows -> T
{
try self.$claudeScanWorkRecorder.withValue(recorder) {
try operation()
}
}

static func recordClaudeScanWork(_ work: ClaudeScanWork) {
self.claudeScanWorkRecorder?.record(work)
}

static func evictClaudeReportMemoForTesting(provider: UsageProvider, cacheRoot: URL?) {
let cacheURL = CostUsageClaudeCacheIO.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
let canonicalCachePath = cacheURL.standardizedFileURL.resolvingSymlinksInPath().path
CostUsageClaudeReportMemo.shared.evict(
provider: provider,
canonicalCachePath: canonicalCachePath)
}
}
#endif

/// Claude and Vertex retain their small transcript cache. Codex deliberately has no route
/// through this JSON I/O boundary; its only persistence authority is `CostUsageStore`.
enum CostUsageClaudeCacheIO {
Expand All @@ -25,8 +209,11 @@ enum CostUsageClaudeCacheIO {
calendar: Calendar? = nil) -> CostUsageCache
{
let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
guard let data = try? Data(contentsOf: url),
let cache = try? JSONDecoder().decode(CostUsageCache.self, from: data),
guard let data = try? Data(contentsOf: url) else { return CostUsageCache() }
#if DEBUG
CostUsageScanner.recordClaudeScanWork(.cacheDecode)
#endif
guard let cache = try? JSONDecoder().decode(CostUsageCache.self, from: data),
cache.version == 1
else { return CostUsageCache() }
if let calendar, cache.timeZoneIdentifier != calendar.timeZone.identifier {
Expand All @@ -39,15 +226,34 @@ enum CostUsageClaudeCacheIO {
provider: UsageProvider,
cache: CostUsageCache,
cacheRoot: URL? = nil,
calendar: Calendar = .current)
calendar: Calendar = .current,
checkCancellation: CostUsageScanner.CancellationCheck? = nil) throws -> CostUsageClaudeFileStamp?
{
let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
try? FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
var cache = cache
cache.timeZoneIdentifier = calendar.timeZone.identifier
guard let data = try? JSONEncoder().encode(cache) else { return }
try? data.write(to: url, options: [.atomic])
#if DEBUG
CostUsageScanner.recordClaudeScanWork(.cacheEncode)
#endif
guard let data = try? JSONEncoder().encode(cache) else { return nil }
try checkCancellation?()
let directory = url.deletingLastPathComponent()
try? FileManager.default.createDirectory(
at: directory,
withIntermediateDirectories: true)
let temporaryURL = directory.appendingPathComponent(".claude-cache-\(UUID().uuidString).tmp")
do {
try data.write(to: temporaryURL)
guard let stamp = CostUsageClaudeFileStamp.read(at: temporaryURL),
rename(temporaryURL.path, url.path) == 0
else {
try? FileManager.default.removeItem(at: temporaryURL)
return nil
}
return stamp
} catch {
try? FileManager.default.removeItem(at: temporaryURL)
return nil
}
}
}
Loading