Skip to content

Commit 2920019

Browse files
xx205Peter Steinberger
andauthored
fix: preserve fork accuracy during bounded Codex cost catch-up (#2525)
* Fix resumable Codex fork accounting * Expose Codex cost catch-up progress * Add adaptive Codex cost catch-up controls * Preserve Codex cost history during cache rebuilds * Bind dashboard catch-up to account caches * fix: quiesce missing Codex fork parents --------- Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
1 parent 5b0b9fa commit 2920019

52 files changed

Lines changed: 4696 additions & 366 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Menu: the compact multi-account layout now covers every stacked multi-account list — token accounts on any provider and Codex accounts (flat lists; workspace-grouped Codex lists keep their sections).
88

99
### Fixed
10+
- Codex: persist and budget fork-parent discovery so missing parents quiesce between inventory changes instead of sweeping every rollout on each refresh (#2525, #2538). Thanks @xx205, and @Helmi and @kiranmagic7 for the investigation!
1011
- Claude: Auto cold boot with Keychain disabled loads without manual refresh (#2494, fixes #2493). Thanks @gmkbenjamin!
1112
- Menu: no more stray floating "Refresh" tooltip beside the menu when switching tabs with the cursor over the actions area.
1213
- Providers: write the Factory and Cursor session files (bearer/refresh tokens, auth cookies) owner-only (0600), matching the codex/kimi/antigravity credential stores.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import Foundation
2+
import IOKit.ps
3+
4+
enum CodexCostCatchUpMode: String, Sendable {
5+
case automatic
6+
case accelerated
7+
}
8+
9+
enum CodexCostCatchUpPowerSource: String, Sendable {
10+
case ac
11+
case battery
12+
case unknown
13+
14+
static func current() -> Self {
15+
guard let info = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(),
16+
let source = IOPSGetProvidingPowerSourceType(info)?.takeUnretainedValue() as String?
17+
else {
18+
return .unknown
19+
}
20+
if source == kIOPSACPowerValue as String {
21+
return .ac
22+
}
23+
if source == kIOPSBatteryPowerValue as String {
24+
return .battery
25+
}
26+
return .unknown
27+
}
28+
}
29+
30+
enum CodexCostCatchUpPauseReason: Sendable, Equatable {
31+
case lowPower
32+
case thermal
33+
case user
34+
case noProgress
35+
case error(String)
36+
}
37+
38+
struct CodexCostCatchUpActivity: Sendable, Equatable {
39+
enum Phase: Sendable, Equatable {
40+
case indexing
41+
case paused
42+
case complete
43+
}
44+
45+
let phase: Phase
46+
let mode: CodexCostCatchUpMode
47+
let processedBytes: Int64
48+
let totalBytes: Int64
49+
let completedFiles: Int
50+
let totalFiles: Int
51+
let pauseReason: CodexCostCatchUpPauseReason?
52+
let staleSnapshotUpdatedAt: Date?
53+
54+
var fractionCompleted: Double? {
55+
guard self.totalBytes > 0 else {
56+
guard self.totalFiles > 0 else { return nil }
57+
return min(1, max(0, Double(self.completedFiles) / Double(self.totalFiles)))
58+
}
59+
return min(1, max(0, Double(self.processedBytes) / Double(self.totalBytes)))
60+
}
61+
}
62+
63+
struct CodexCostCatchUpPolicy: Sendable {
64+
struct Input: Sendable {
65+
let mode: CodexCostCatchUpMode
66+
let previousActiveDuration: TimeInterval?
67+
let powerSource: CodexCostCatchUpPowerSource
68+
let lowPowerModeEnabled: Bool
69+
let thermalState: ProcessInfo.ThermalState
70+
}
71+
72+
struct Decision: Sendable, Equatable {
73+
enum Action: Sendable, Equatable {
74+
case runAfter(TimeInterval)
75+
case pause(TimeInterval, CodexCostCatchUpPauseReason)
76+
}
77+
78+
let action: Action
79+
let targetDutyCycle: Double?
80+
}
81+
82+
static let automaticBurstDuration: TimeInterval = 2
83+
static let constrainedRetryDelay: TimeInterval = 60
84+
85+
func decision(for input: Input) -> Decision {
86+
if input.thermalState == .critical {
87+
return Decision(
88+
action: .pause(Self.constrainedRetryDelay, .thermal),
89+
targetDutyCycle: nil)
90+
}
91+
if input.mode == .automatic {
92+
if input.lowPowerModeEnabled {
93+
return Decision(
94+
action: .pause(Self.constrainedRetryDelay, .lowPower),
95+
targetDutyCycle: nil)
96+
}
97+
if input.thermalState == .serious {
98+
return Decision(
99+
action: .pause(Self.constrainedRetryDelay, .thermal),
100+
targetDutyCycle: nil)
101+
}
102+
}
103+
if input.mode == .accelerated {
104+
return Decision(action: .runAfter(0), targetDutyCycle: 1)
105+
}
106+
107+
let dutyCycle = switch input.powerSource {
108+
case .ac: 0.20
109+
case .battery: 0.05
110+
case .unknown: 0.15
111+
}
112+
let activeDuration = max(0, input.previousActiveDuration ?? Self.automaticBurstDuration)
113+
let delay = activeDuration * (1 - dutyCycle) / dutyCycle
114+
return Decision(action: .runAfter(delay), targetDutyCycle: dutyCycle)
115+
}
116+
}

Sources/CodexBar/PreferencesSpendDashboardPane.swift

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ func spendDashboardCoverageText(covered: Int, requested: Int) -> String {
2727
"\(L("Coverage")): \(codexBarLocalizedInteger(covered)) / \(codexBarLocalizedInteger(requested))"
2828
}
2929

30+
func codexCostCatchUpProgressText(_ activity: CodexCostCatchUpActivity) -> String {
31+
if activity.totalBytes > 0 {
32+
let processed = ByteCountFormatter.string(
33+
fromByteCount: activity.processedBytes,
34+
countStyle: .file)
35+
let total = ByteCountFormatter.string(
36+
fromByteCount: activity.totalBytes,
37+
countStyle: .file)
38+
return "\(processed) / \(total)"
39+
}
40+
if activity.totalFiles > 0 {
41+
return "\(codexBarLocalizedInteger(activity.completedFiles)) / "
42+
+ codexBarLocalizedInteger(activity.totalFiles)
43+
}
44+
return L("Loading…")
45+
}
46+
3047
enum SpendDashboardModelHistoryPresentation: Equatable {
3148
case unavailable
3249
case empty
@@ -48,6 +65,7 @@ struct SpendDashboardPane: View {
4865
@Bindable var settings: SettingsStore
4966
@Bindable var store: UsageStore
5067
@State private var controller: SpendDashboardController
68+
@State private var isVisible = false
5169

5270
init(settings: SettingsStore, store: UsageStore) {
5371
self.settings = settings
@@ -61,6 +79,7 @@ struct SpendDashboardPane: View {
6179
ScrollView {
6280
VStack(alignment: .leading, spacing: 18) {
6381
self.header
82+
self.codexCostCatchUpPanel
6483
self.content
6584
self.provenance
6685
self.shareAction
@@ -69,13 +88,26 @@ struct SpendDashboardPane: View {
6988
}
7089
.background(FocusResigningBackground())
7190
.onAppear {
91+
self.isVisible = true
7292
self.controller.refreshDateWindow()
7393
self.controller.update(configuration: self.configuration)
94+
if !self.controller.isRefreshing {
95+
self.synchronizeCodexCostCatchUp()
96+
}
7497
}
7598
.onChange(of: self.configuration) { _, configuration in
7699
self.controller.update(configuration: configuration)
100+
if self.isVisible, !self.controller.isRefreshing {
101+
self.synchronizeCodexCostCatchUp()
102+
}
103+
}
104+
.onChange(of: self.controller.isRefreshing) { _, isRefreshing in
105+
if self.isVisible, !isRefreshing {
106+
self.synchronizeCodexCostCatchUp()
107+
}
77108
}
78109
.onDisappear {
110+
self.isVisible = false
79111
self.controller.stop()
80112
}
81113
.onReceive(NotificationCenter.default.publisher(for: .NSCalendarDayChanged)) { _ in
@@ -124,6 +156,131 @@ struct SpendDashboardPane: View {
124156
}
125157
}
126158

159+
@ViewBuilder
160+
private var codexCostCatchUpPanel: some View {
161+
if let activity = self.store.spendDashboardCodexCostCatchUpActivity,
162+
activity.phase != .complete
163+
{
164+
SpendDashboardPanel {
165+
VStack(alignment: .leading, spacing: 10) {
166+
HStack(spacing: 8) {
167+
Label(
168+
self.codexCostCatchUpTitle(activity),
169+
systemImage: activity.phase == .paused ? "pause.circle" : "externaldrive")
170+
.font(.headline)
171+
Spacer()
172+
Text(codexCostCatchUpProgressText(activity))
173+
.font(.caption.monospacedDigit())
174+
.foregroundStyle(.secondary)
175+
}
176+
177+
if let progress = activity.fractionCompleted {
178+
ProgressView(value: progress)
179+
} else if activity.phase == .indexing {
180+
ProgressView()
181+
.controlSize(.small)
182+
}
183+
184+
if let staleSnapshotUpdatedAt = activity.staleSnapshotUpdatedAt {
185+
HStack(spacing: 6) {
186+
Label(L("stale data"), systemImage: "clock.badge.exclamationmark")
187+
Text(L(
188+
"Updated relative %@",
189+
staleSnapshotUpdatedAt.relativeDescription()))
190+
}
191+
.font(.caption.weight(.medium))
192+
.foregroundStyle(.orange)
193+
}
194+
195+
Text(self.codexCostCatchUpDetail(activity))
196+
.font(.caption)
197+
.foregroundStyle(.secondary)
198+
199+
HStack {
200+
if activity.pauseReason == .user
201+
|| activity.pauseReason == .noProgress
202+
|| self.codexCostCatchUpHasError(activity)
203+
{
204+
Button(L("Refresh")) {
205+
self.startCodexCostCatchUp(mode: .automatic)
206+
}
207+
} else if activity.mode == .automatic {
208+
Button(L("Finish now")) {
209+
self.startCodexCostCatchUp(mode: .accelerated)
210+
}
211+
} else {
212+
Button(L("Continue in background")) {
213+
self.startCodexCostCatchUp(mode: .automatic)
214+
}
215+
}
216+
217+
if activity.pauseReason != .user,
218+
activity.pauseReason != .noProgress,
219+
!self.codexCostCatchUpHasError(activity)
220+
{
221+
Button(L("Cancel")) {
222+
self.store.stopSpendDashboardCodexCostCatchUp()
223+
}
224+
}
225+
}
226+
.controlSize(.small)
227+
}
228+
}
229+
}
230+
}
231+
232+
private func codexCostCatchUpHasError(_ activity: CodexCostCatchUpActivity) -> Bool {
233+
if case .error = activity.pauseReason {
234+
return true
235+
}
236+
return false
237+
}
238+
239+
private func synchronizeCodexCostCatchUp() {
240+
self.store.synchronizeSpendDashboardCodexCostCatchUp(
241+
accounts: self.codexSpendScanRequests)
242+
}
243+
244+
private func startCodexCostCatchUp(mode: CodexCostCatchUpMode) {
245+
self.store.startSpendDashboardCodexCostCatchUpIfNeeded(
246+
accounts: self.codexSpendScanRequests,
247+
mode: mode)
248+
}
249+
250+
private var codexSpendScanRequests: [CodexSpendScanRequest] {
251+
guard self.configuration.costUsageEnabled,
252+
self.configuration.providerIDs.contains(UsageProvider.codex.rawValue)
253+
else { return [] }
254+
return SpendDashboardSource.codexRequests(settings: self.settings, store: self.store)
255+
}
256+
257+
private func codexCostCatchUpTitle(_ activity: CodexCostCatchUpActivity) -> String {
258+
let prefix = L("Local estimated history")
259+
switch activity.phase {
260+
case .indexing:
261+
return "\(prefix) · \(L("Refreshing"))"
262+
case .paused:
263+
return "\(prefix) · \(L("Inactive"))"
264+
case .complete:
265+
return "\(prefix) · \(L("Done"))"
266+
}
267+
}
268+
269+
private func codexCostCatchUpDetail(_ activity: CodexCostCatchUpActivity) -> String {
270+
switch activity.pauseReason {
271+
case .lowPower:
272+
L("Battery Saver")
273+
case .thermal, .user:
274+
L("Inactive")
275+
case .noProgress:
276+
L("Error")
277+
case let .error(message):
278+
L("cost_status_error", L("Cost"), message)
279+
case nil:
280+
L("Estimated from local Codex logs for the selected account.")
281+
}
282+
}
283+
127284
@ViewBuilder
128285
private var content: some View {
129286
if !self.settings.costUsageEnabled {

Sources/CodexBar/Resources/ar.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,3 +1353,5 @@
13531353
"Cost today unavailable" = "تكلفة اليوم: غير متوفر";
13541354
"30-day cost unavailable" = "تكلفة 30 يوماً: غير متوفر";
13551355
"Resets" = "إعادات الضبط";
1356+
"Finish now" = "إنهاء الآن";
1357+
"Continue in background" = "المتابعة في الخلفية";

Sources/CodexBar/Resources/ca.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,3 +1352,5 @@
13521352
"Cost today unavailable" = "Cost d’avui: No disponible";
13531353
"30-day cost unavailable" = "Cost de 30 dies: No disponible";
13541354
"Resets" = "Reinicis";
1355+
"Finish now" = "Finalitza ara";
1356+
"Continue in background" = "Continua en segon pla";

Sources/CodexBar/Resources/de.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,3 +1350,5 @@
13501350
"Cost today unavailable" = "Kosten heute: Nicht verfügbar";
13511351
"30-day cost unavailable" = "Kosten 30 Tage: Nicht verfügbar";
13521352
"Resets" = "Zurücksetzungen";
1353+
"Finish now" = "Jetzt abschließen";
1354+
"Continue in background" = "Im Hintergrund fortfahren";

Sources/CodexBar/Resources/en.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,3 +1354,5 @@
13541354
"Cost today unavailable" = "Cost today unavailable";
13551355
"30-day cost unavailable" = "30-day cost unavailable";
13561356
"Resets" = "Resets";
1357+
"Finish now" = "Finish now";
1358+
"Continue in background" = "Continue in background";

Sources/CodexBar/Resources/es.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,3 +1348,5 @@
13481348
"Cost today unavailable" = "Coste de hoy: No disponible";
13491349
"30-day cost unavailable" = "Coste de 30 días: No disponible";
13501350
"Resets" = "Reinicios";
1351+
"Finish now" = "Finalizar ahora";
1352+
"Continue in background" = "Continuar en segundo plano";

Sources/CodexBar/Resources/fa.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,3 +1353,5 @@
13531353
"Cost today unavailable" = "هزینهٔ امروز: در دسترس نیست";
13541354
"30-day cost unavailable" = "هزینهٔ ۳۰ روز: در دسترس نیست";
13551355
"Resets" = "بازنشانی‌ها";
1356+
"Finish now" = "اکنون تمام شود";
1357+
"Continue in background" = "ادامه در پس‌زمینه";

Sources/CodexBar/Resources/fr.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,3 +1349,5 @@
13491349
"Cost today unavailable" = "Coût aujourd’hui: Indisponible";
13501350
"30-day cost unavailable" = "Coût sur 30 j: Indisponible";
13511351
"Resets" = "Réinitialisations";
1352+
"Finish now" = "Terminer maintenant";
1353+
"Continue in background" = "Continuer en arrière-plan";

0 commit comments

Comments
 (0)