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 @@ -9,6 +9,7 @@
- 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.
- Agent Sessions: stop inactive local and remote refresh schedulers from waking while monitoring is disabled.
- Cost usage: refresh token-cost data when “Refresh all providers on menu open” is enabled, with a one-minute scan floor to avoid repeated work (#2388). Thanks @betive37!

## 0.49.1 — 2026-08-09

Expand Down
7 changes: 5 additions & 2 deletions Sources/CodexBar/MenuOpenRefreshPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ struct MenuOpenRefreshPlan: Equatable {
let providers: [ProviderInstanceID]
let scheduling: Scheduling
let refreshCodexDashboard: Bool
let refreshTokenCost: Bool

static func resolve(_ inputs: Inputs) -> Self {
if inputs.refreshAllOnOpen {
return Self(
providers: inputs.enabledProviders,
scheduling: .concurrent,
refreshCodexDashboard: inputs.enabledProviders.contains(.codex))
refreshCodexDashboard: inputs.enabledProviders.contains(.codex),
refreshTokenCost: !inputs.enabledProviders.isEmpty)
}

let enabled = Set(inputs.enabledProviders)
Expand All @@ -36,6 +38,7 @@ struct MenuOpenRefreshPlan: Equatable {
return Self(
providers: providers,
scheduling: .sequential,
refreshCodexDashboard: false)
refreshCodexDashboard: false,
refreshTokenCost: false)
}
}
7 changes: 6 additions & 1 deletion Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,9 @@ extension StatusItemController {
// feel frozen and can block keyboard focus from returning.
// Exception: when `refreshAllProvidersOnMenuOpen` is enabled, every enabled provider is refreshed on
// open regardless of freshness — still after the delay below, and still via the light usage-only
// primitive so the OpenAI dashboard scrape stays deferred until the menu closes.
// primitive so the OpenAI dashboard scrape stays deferred until the menu closes. Token cost is not
// part of that primitive, so the plan also schedules a forced cost rescan (fire-and-forget; the
// open menu picks up the published snapshot through the store observation).
let providersNeedingRetryAtOpen = self.delayedRefreshRetryProviders(for: menu).filter {
self.store.needsUsageRefreshRetry(for: $0)
}
Expand Down Expand Up @@ -1174,6 +1176,9 @@ extension StatusItemController {
if plan.refreshCodexDashboard {
self.deferOpenAIDashboardRefreshUntilMenuCloses(reason: "refresh all")
}
if plan.refreshTokenCost {
self.store.scheduleForcedTokenRefresh()
}
let retryInstanceIDs = plan.providers
let retryProviders = retryInstanceIDs.compactMap(\.firstPartyProvider)
guard !retryInstanceIDs.isEmpty else {
Expand Down
64 changes: 64 additions & 0 deletions Sources/CodexBar/UsageStore+TokenRefreshSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,55 @@ extension UsageStore {
case all
case provider(ProviderInstanceID)
case providers([ProviderInstanceID])

var coversAllProviders: Bool {
if case .all = self {
return true
}
return false
}
}

func scheduleTokenRefresh() {
guard self.tokenRefreshSequenceTask == nil, !self.hasForcedRefreshEnrichmentInFlight else { return }
if self.startPendingForcedTokenRefreshIfPossible() {
return
}
if self.startPendingTokenRefreshRetryIfPossible() {
return
}
self.startTokenRefreshSequence(force: false, scope: .all)
}

/// Minimum spacing between forced all-provider cost scans. Menu open may bypass the fetch TTL,
/// but rapid open/close cycles must not hammer the scanner more than once a minute.
static let forcedTokenRefreshMinInterval: TimeInterval = 60

/// Menu-open parity with the manual Refresh action: cost must rescan past the fetch TTL, but
/// without awaiting (AppKit menu tracking is modal) and without preempting an in-flight
/// sequence or forced-refresh enrichment tail. The enrichment tail and a forced all-provider
/// pass already end in fresh cost data, so re-requests coalesce into them. Any other active
/// sequence may skip TTL-fresh providers, so the request stays pending and one forced pass
/// runs once that sequence completes. The TTL bypass is floored: a forced pass that started
/// less than `forcedTokenRefreshMinInterval` ago already delivered fresh cost data, so the
/// request is dropped instead of queued.
func scheduleForcedTokenRefresh(now: Date = Date()) {
if let last = self.lastForcedTokenRefreshStartedAt,
now.timeIntervalSince(last) >= 0,
now.timeIntervalSince(last) < Self.forcedTokenRefreshMinInterval
{
return
}
guard !self.hasForcedRefreshEnrichmentInFlight else { return }
if self.tokenRefreshSequenceTask != nil {
if !self.tokenRefreshSequenceIsForcedAllPass {
self.pendingForcedTokenRefresh = true
}
return
}
self.startTokenRefreshSequence(force: true, scope: .all)
}

func refreshTokenUsageSequenceNow(force: Bool) async {
guard let task = await self.serializedTokenRefreshTask(force: force, scope: .all) else { return }
await self.awaitTokenRefreshSequence(task)
Expand Down Expand Up @@ -74,6 +113,13 @@ extension UsageStore {
// Publish the first owner before installing the task. A scoped forced refresh can arrive
// before the task gets its first MainActor turn and must not mistake this slot for unknown work.
self.tokenRefreshSequenceProvider = providers.first
self.tokenRefreshSequenceIsForcedAllPass = force && scope.coversAllProviders
if self.tokenRefreshSequenceIsForcedAllPass {
// A forced all-provider pass delivers everything a coalesced menu-open request wants.
self.pendingForcedTokenRefresh = false
// Manual Refresh lands here too, so a menu open right after it also honors the floor.
self.lastForcedTokenRefreshStartedAt = Date()
}
let task = Task(priority: .utility) { @MainActor [weak self] in
guard let self else { return }
await self.refreshTokenUsageSequence(providers: providers, force: force)
Expand All @@ -96,9 +142,27 @@ extension UsageStore {
self.tokenRefreshSequenceTask = nil
self.tokenRefreshSequenceToken = nil
self.tokenRefreshSequenceProvider = nil
self.tokenRefreshSequenceIsForcedAllPass = false
if self.startPendingForcedTokenRefreshIfPossible() {
return
}
self.startPendingTokenRefreshRetryIfPossible()
}

/// A cancelled sequence lost the slot to a serialized replacement pass, so the request stays
/// pending until a sequence completes normally; a forced all-provider replacement clears it
/// on start instead.
@discardableResult
private func startPendingForcedTokenRefreshIfPossible() -> Bool {
guard self.pendingForcedTokenRefresh, !Task.isCancelled else { return false }
self.pendingForcedTokenRefresh = false
guard !self.hasForcedRefreshEnrichmentInFlight else { return false }
// The forced all-provider pass rescans every enabled lane, so stale-retry lanes fold into it.
self.tokenRefreshRetryProviders.subtract(self.enabledProvidersForBackgroundWork())
self.startTokenRefreshSequence(force: true, scope: .all)
return true
}

func requestTokenRefreshAfterStaleCompletion(for provider: UsageProvider) {
self.tokenRefreshRetryProviders.insert(provider.instanceID)
Task { @MainActor [weak self] in
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@ final class UsageStore {
@ObservationIgnored var tokenRefreshSequenceTask: Task<Void, Never>?
@ObservationIgnored var tokenRefreshSequenceToken: UUID?
@ObservationIgnored var tokenRefreshSequenceProvider: ProviderInstanceID?
@ObservationIgnored var tokenRefreshSequenceIsForcedAllPass = false
@ObservationIgnored var pendingForcedTokenRefresh = false
@ObservationIgnored var lastForcedTokenRefreshStartedAt: Date?
@ObservationIgnored var tokenRefreshRetryProviders: Set<ProviderInstanceID> = []
@ObservationIgnored var codexCostCatchUpTask: Task<Void, Never>?
@ObservationIgnored var codexCostCatchUpToken: UUID?
Expand Down
17 changes: 17 additions & 0 deletions Tests/CodexBarTests/MenuOpenRefreshPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct MenuOpenRefreshPlanTests {
#expect(plan.providers == [.codex, .claude, .factory])
#expect(plan.scheduling == .concurrent)
#expect(plan.refreshCodexDashboard)
#expect(plan.refreshTokenCost)
}

@Test
Expand All @@ -30,6 +31,21 @@ struct MenuOpenRefreshPlanTests {

#expect(plan.providers == [.claude, .factory])
#expect(!plan.refreshCodexDashboard)
#expect(plan.refreshTokenCost)
}

@Test
func `refresh all skips token cost refresh when no providers are enabled`() {
let plan = MenuOpenRefreshPlan.resolve(.init(
refreshAllOnOpen: true,
enabledProviders: [],
visibleProviders: [],
refreshingProviders: [],
staleProviders: [],
missingProviders: []))

#expect(plan.providers.isEmpty)
#expect(!plan.refreshTokenCost)
}

@Test
Expand All @@ -45,6 +61,7 @@ struct MenuOpenRefreshPlanTests {
#expect(plan.providers == [.factory, .codex, .claude])
#expect(plan.scheduling == .sequential)
#expect(!plan.refreshCodexDashboard)
#expect(!plan.refreshTokenCost)
}

@Test
Expand Down
24 changes: 12 additions & 12 deletions Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1275,19 +1275,19 @@ struct ProviderArchitectureGatekeeperTests {
reason: "Claude widget quota ownership uses the selected Claude account's isolated snapshot key."),
SuppressedProviderReference(
path: "Sources/CodexBar/UsageStore.swift",
line: 1042,
line: 1045,
anchor: "provider: .deepseek,",
expectedProviderIDs: ["deepseek"],
reason: "This provider-specific app branch passes its already-selected identity to a shared helper."),
SuppressedProviderReference(
path: "Sources/CodexBar/UsageStore.swift",
line: 1144,
line: 1147,
anchor: "let sourceMode = self.sourceMode(for: .claude)",
expectedProviderIDs: ["claude"],
reason: "This provider-specific app branch passes its already-selected identity to a shared helper."),
SuppressedProviderReference(
path: "Sources/CodexBar/UsageStore.swift",
line: 1148,
line: 1151,
anchor: "provider: .claude,",
expectedProviderIDs: ["claude"],
reason: "This provider-specific app branch passes its already-selected identity to a shared helper."),
Expand Down Expand Up @@ -2099,8 +2099,8 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuOpenRefreshPlan.swift",
line: 27,
anchor: "refreshCodexDashboard: inputs.enabledProviders.contains(.codex))",
line: 28,
anchor: "refreshCodexDashboard: inputs.enabledProviders.contains(.codex),",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["codex@0"],
Expand Down Expand Up @@ -3219,47 +3219,47 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/UsageStore.swift",
line: 583,
line: 586,
anchor: "self.metadata(for: .codex).browserCookieOrder ?? Browser.defaultImportOrder",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["codex@0"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/UsageStore.swift",
line: 635,
line: 638,
anchor: "self.providerSpecs[provider]?.style ?? .codex",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["codex@0"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/UsageStore.swift",
line: 668,
line: 671,
anchor: "guard provider != .codex else { return true }",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["codex@0"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/UsageStore.swift",
line: 1016,
line: 1019,
anchor: "let claudeDebugConfiguration: ClaudeDebugLogConfiguration? = if provider == .claude {",
expectedProviderIDs: ["claude"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["claude@0"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/UsageStore.swift",
line: 1039,
line: 1042,
anchor: "let deepSeekHasTokenAccount = self.settings.selectedTokenAccount(for: .deepseek) != nil",
expectedProviderIDs: ["deepseek"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["deepseek@0"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/UsageStore.swift",
line: 1096,
line: 1099,
anchor: "case .amp:",
expectedProviderIDs: ["amp", "deepseek", "notion", "ollama", "warp"],
expectedReferenceCount: 7,
Expand All @@ -3275,7 +3275,7 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/UsageStore.swift",
line: 1151,
line: 1154,
anchor: "let claudeSettings = snapshot.claude ?? ProviderSettingsSnapshot.ClaudeProviderSettings(",
expectedProviderIDs: ["claude"],
expectedReferenceCount: 1,
Expand Down
Loading