Repository navigation
fix(spend): stop per-refresh dashboard reloads for snapshot-backed sources - #5071
daniel-inderos wants to merge 4 commits into
Conversation
…urces Snapshot-backed spend sources (Grok, OpenAI API, OpenRouter, Mistral, xAI, OpenCode Go) republish their token snapshot on every successful provider refresh. The shared Usage & Spend configuration keyed them by publication revision and exact scan timestamp, so every refresh tick looked like a new source revision and reloaded the dashboard, rescanning all-time Codex history and the 365-day activity cache outside the 15-minute token TTL. Key those sources by content instead, with updatedAt reduced to the bucket day the dashboard actually projects. Unchanged republications are no-ops; changed spend, empty/data transitions, and day rollovers still advance. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1eaeb73d87
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| guard let snapshot = current.snapshot else { return "\(provider.rawValue):empty" } | ||
| let revision = self.snapshotRevision( | ||
| snapshot, | ||
| updatedAtGranularity: .bucketDay(settings.costUsageBucketCalendar)) |
There was a problem hiding this comment.
Use xAI's UTC day for revision bucketing
For xAI with a non-UTC costUsageBucketTimeZoneIdentifier, scans immediately before and after 00:00 UTC can fall on the same local bucket day, so this value remains unchanged when the sparse daily points and spend are unchanged. SpendDashboardModel.bucketCalendar(for:) nevertheless computes xAI coverage in UTC, so the loaded source window should advance at that refresh; configuration equality instead keeps the previous coverage/zero-filled day until another reload. Bucket updatedAt with the provider's coverage calendar (UTC for xAI/Mistral/OpenRouter), rather than unconditionally with the display calendar.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 390a3e2: updatedAt is now bucketed with the source's coverage calendar via SpendDashboardModel.bucketCalendar(for:displayCalendar:), so xAI, Mistral and OpenRouter advance at the UTC boundary. Covered by UTC-bucketed source advances at its own day boundary.
| let revision = self.snapshotRevision( | ||
| snapshot, | ||
| updatedAtGranularity: .bucketDay(settings.costUsageBucketCalendar)) | ||
| return "\(provider.rawValue):snapshot:\(revision)" |
There was a problem hiding this comment.
Hash the same Mistral window that the dashboard loads
When the menu cost range is narrower than Usage & Spend (for example costUsageHistoryDays == 7 while the dashboard shows 30/90/All), current.snapshot here is Mistral's regular 7-day publication because tokenSnapshot(fromProviderSnapshot:) uses that setting when historyDays is nil, whereas makeRequest reprojects the stored Mistral usage with SpendDashboardSource.scanDays. A same-day billing correction to an older Mistral bucket therefore changes the input the dashboard would load but not this revision, so SpendDashboardController.update sees an equal configuration and leaves stale spend; compute the revision from the same wide projection consumed by dashboardTokenSnapshot.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 390a3e2: the revision now hashes the same scanDays projection that dashboardTokenSnapshot loads, falling back to the publication only when no provider snapshot exists. Covered by correction outside the menu range still advances the shared configuration.
| let revision = self.snapshotRevision( | ||
| snapshot, | ||
| updatedAtGranularity: .bucketDay(settings.costUsageBucketCalendar)) | ||
| return "\(provider.rawValue):snapshot:\(revision)" |
There was a problem hiding this comment.
Preserve OpenRouter reasoning updates in revisions
If an OpenRouter activity refresh fills or corrects only reasoning_tokens, the plugin mapper changes the daily entry and model-breakdown reasoning fields, and SpendDashboardModel folds those fields into the displayed token mix, while prompt/completion totals, requests, cost, and dates can remain identical. snapshotRevision does not append either reasoning field, and this branch now drops the publication revision, so the configuration remains equal and the dashboard/export retains the old reasoning attribution; include all model-consumed fields or hash the complete projected snapshot before using it as the sole revision.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 390a3e2: the content revision now also covers reasoning tokens, per-model input/output/cache/reasoning splits, coverage counts, modelsUsed, historyScanIsPartial and meteredCostUSD. Covered by reasoning-only correction advances the shared configuration.
Address review findings on the content-keyed revision: - hash the wide projection the dashboard loads instead of the menu-range publication, so corrections outside the menu range advance it - bucket updatedAt with the source's coverage calendar (UTC for Mistral, OpenRouter, and xAI) rather than the display calendar - cover every model-consumed field, including reasoning tokens and per-model token splits, now that no publication revision backs it up Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Codex review: needs maintainer review before merge. What this changesThis PR prevents unchanged snapshot-backed spend publications from triggering dashboard reloads and repeated Codex history scans. Example: Codex and Grok enabled with one-minute refreshes and Usage & Spend closed.
Review scores
ProductKind: Bug fix · Worth it: Yes · Fix scope: Complete Merge readiness✅ Ready for maintainer review This PR fixes a concrete background CPU problem still present on main. No actionable correctness or security findings remain in the reviewed change. Priority: P2 Before mergeNone. FindingsNone. Agent review detailsHow this fits togetherUsageStore publishes provider snapshots into a shared dashboard configuration; configuration changes trigger history loading and produce spend data for Overview and Usage & Spend. flowchart LR
A[Provider refresh] --> B[UsageStore publication]
B --> C[Projected spend content]
C --> D[Source revision encoder]
D --> E[Dashboard configuration comparison]
E --> F[History loader]
F --> G[Overview and Usage and Spend]
Technical reviewBest possible solution: Use content and coverage-day revisions within the existing dashboard owner while preserving independent-source refresh triggers and ownership checks. Do we have a high-confidence way to reproduce the issue? Current-main source establishes the publication-to-reload path, corroborated by the contributor's packaged-app comparison; no target code was executed during review. Is this the best way to solve the issue? The fix addresses invalidation at its existing owner rather than throttling the resulting scans, while retaining revisions for real content and coverage changes. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 7447bc77c249. Provenance checked
TestingProof path: shipped entry point. SecurityNone. EvidenceWhat I checked:
Likely related people:
Preflight sourceSource unknown. Source comparison at the observed PR snapshot only. Completion is self-reported; body, policy, tests, execution, and report bytes are not verified. Review metrics
LabelsLabel changes:
Label justifications:
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment HistoryReview history (1 earlier review cycle)
Reviewed November 12, 2026, 10:12 AM ET / 15:12 UTC (Revision 2). |
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Added real behavior proof in @clawsweeper re-review |
|
🦞👀 Re-review progress:
|
Summary
With Codex plus any snapshot-backed spend source enabled (Grok, OpenAI API, OpenRouter, Mistral, xAI, OpenCode Go), the shared Usage & Spend publisher reloads on every provider refresh tick, and each reload rescans all-time Codex history plus the 365-day activity cache. At a 1-minute refresh interval that is a 3–9 CPU-second burst every 60 seconds, forever, with the dashboard closed. It bypasses
minimumTokenFetchTTLbecause the scan is started by the dashboard controller, notrefreshTokenUsage.Cause
SpendDashboardSource.sourceRevisionskeys non-Codex sources as<provider>:snapshot:<publicationRevision>:<snapshotRevision>, andsnapshotRevisionhashes the exactupdatedAt.For providers where
tokenCostRequiresProviderSnapshotis true, every successful provider refresh callspublishTokenSnapshot/publishConfirmedEmptyTokenSnapshot(UsageStore+Refresh.swift), which bumps the publication revision, and the derived snapshot carries a fresh scan timestamp. So the configuration differs after every refresh even when spend is identical,SpendDashboardController.update(configuration:)falls through tostartLoad, and the Codex accounts are rescanned.Fix
For snapshot-backed sources, key the revision by content only:
<provider>:snapshot:<contentRevision>,<provider>:empty)updatedAtat the granularity of the source's coverage day (UTC for Mistral, OpenRouter and xAI), which is all the dashboard projects from it (sourceCoverageInterval)scanDaysprojection the dashboard loads, and every field the model consumes, since no publication revision backs the content hashUnchanged republications are now no-ops. Changed spend, empty ↔ data transitions and day rollovers still advance the revision. Independent sources (Claude, Cursor, …) and Codex are untouched.
Real behavior proof
Release builds of
main(7447bc7) and this branch (390a3e2, the current code; the head commit only adds these proof files), run back to back as the packaged app on 2026-11-12 against the same real corpus (Codex 1.7 GB of sessions, Grok 170 sessions). Refresh frequency 1 minute, cost usage on, Usage & Spend closed, 150 s warm-up, then process CPU time sampled every 5 s for 5 minutes.cost-usage-scanqueue (70 s profile)mainArtifacts in
.github/pr-proof/:spend-snapshot-backed-refresh-cpu.log: scenario, build identities, and the raw 5-second CPU series for both runs.mainbursts 2–5 s at every refresh tick; this branch stays at about 0.25 s per tick.spend-snapshot-backed-refresh-profile.log:sampleoutput reduced to queue totals and symbolicated frames. Onmaineach cycle runsSpendDashboardController.startLoad→SpendDashboardSource.load→CostUsageFetcher.loadLocalTokenScanResultandloadCachedCodexTokenActivity; on this branch none of those frames appear and the only scan-queue work left iscodexScanCatchUpStatus.Limits of this proof, stated in the logs: the symbolicated
mainframes come from a profile of the same binary in the same scenario a day earlier (2026-11-11), because its unstripped build products were replaced before the 2026-11-12 run could be symbolicated; the queue totals above are from the 2026-11-12 runs. The one remaining ~2 s burst on this branch fell outside that run's profile window; an earlier profile of the same scenario attributed it toProviderStorageScanner(storage footprints), which is unrelated. The first measurement of this change (first commit, 2026-11-11) gave 18.9 s → 3.0 s.The same per-minute scan was first observed and symbolicated on a stock 0.83.0 install (48 min CPU in 26 h).
Tests
New
SpendDashboardSnapshotBackedRevisionTests:The two "unchanged" tests fail on
mainand pass here.Commands run:
make test-fast FILTER='SpendDashboardSnapshotBackedRevisionTests'(7 passed; the unchanged-republication tests fail onmain, and the three review-follow-up tests fail on the first commit)make test-fast FILTER='SpendDashboardPublicationTests|SpendDashboardFreshnessTests|SpendDashboardFreshnessScannerTests|GrokXAISpendCatalogTests|GrokTokenSnapshotProjectionTests|SpendDashboardSnapshotBackedRevisionTests|SpendDashboardControllerTests|SpendDashboardSourceTests'(120 tests in 10 suites passed, withSpendDashboardModeladded to the filter)Not run locally: the full suite and
make check(SwiftFormat/SwiftLint are not installed on this machine), so formatting relies on CI.Possible follow-up
A real change in a non-Codex source still reloads every source, including the Codex scans. Skipping Codex rescans when only another source's revision moved would help users who are actively using one of these providers, but it touches the controller's reconciliation paths, so I left it out of this change.
🤖 Generated with Claude Code