Skip to content

fix(spend): stop per-refresh dashboard reloads for snapshot-backed sources - #5071

Open
daniel-inderos wants to merge 4 commits into
steipete:mainfrom
daniel-inderos:fix/spend-dashboard-provider-derived-churn
Open

daniel-inderos wants to merge 4 commits into
steipete:mainfrom
daniel-inderos:fix/spend-dashboard-provider-derived-churn

Conversation

@daniel-inderos

@daniel-inderos daniel-inderos commented Nov 12, 2026 •

Copy link
Copy Markdown

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 minimumTokenFetchTTL because the scan is started by the dashboard controller, not refreshTokenUsage.

Cause

SpendDashboardSource.sourceRevisions keys non-Codex sources as <provider>:snapshot:<publicationRevision>:<snapshotRevision>, and snapshotRevision hashes the exact updatedAt.

For providers where tokenCostRequiresProviderSnapshot is true, every successful provider refresh calls publishTokenSnapshot / 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 to startLoad, and the Codex accounts are rescanned.

Fix

For snapshot-backed sources, key the revision by content only:

  • drop the publication revision (<provider>:snapshot:<contentRevision>, <provider>:empty)
  • hash updatedAt at the granularity of the source's coverage day (UTC for Mistral, OpenRouter and xAI), which is all the dashboard projects from it (sourceCoverageInterval)
  • hash the scanDays projection the dashboard loads, and every field the model consumes, since no publication revision backs the content hash

Unchanged 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.

CPU time, main vs PR head

CPU time over 5 min Average Samples on cost-usage-scan queue (70 s profile)
main 16.17 s 5.5% 964
this branch 3.18 s 1.1% 30

Artifacts in .github/pr-proof/:

  • spend-snapshot-backed-refresh-cpu.log: scenario, build identities, and the raw 5-second CPU series for both runs. main bursts 2–5 s at every refresh tick; this branch stays at about 0.25 s per tick.
  • spend-snapshot-backed-refresh-profile.log: sample output reduced to queue totals and symbolicated frames. On main each cycle runs SpendDashboardController.startLoad → SpendDashboardSource.load → CostUsageFetcher.loadLocalTokenScanResult and loadCachedCodexTokenActivity; on this branch none of those frames appear and the only scan-queue work left is codexScanCatchUpStatus.

Limits of this proof, stated in the logs: the symbolicated main frames 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 to ProviderStorageScanner (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:

  • unchanged snapshot-backed republication keeps the shared configuration stable
  • unchanged confirmed-empty republication keeps it stable, and empty → data advances it
  • changed spend advances it
  • a scan crossing a bucket day advances it
  • a correction outside the menu range advances it (the revision hashes the full window the dashboard loads)
  • a UTC-bucketed source (OpenRouter) advances at the UTC day boundary
  • a reasoning-token-only correction advances it

The two "unchanged" tests fail on main and pass here.

Commands run:

  • make test-fast FILTER='SpendDashboardSnapshotBackedRevisionTests' (7 passed; the unchanged-republication tests fail on main, 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, with SpendDashboardModel added 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

daniel-inderos and others added 2 commits November 12, 2026 07:36
…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>
@clawsweeper

clawsweeper Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T13:43:55.239789Z 1eaeb73 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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))

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 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment on lines +762 to +765
let revision = self.snapshotRevision(
snapshot,
updatedAtGranularity: .bucketDay(settings.costUsageBucketCalendar))
return "\(provider.rawValue):snapshot:\(revision)"

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 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment on lines +762 to +765
let revision = self.snapshotRevision(
snapshot,
updatedAtGranularity: .bucketDay(settings.costUsageBucketCalendar))
return "\(provider.rawValue):snapshot:\(revision)"

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 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

@clawsweeper clawsweeper Bot added clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. labels Nov 12, 2026
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>
@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. labels Nov 12, 2026
@clawsweeper

clawsweeper Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Codex review: needs maintainer review before merge.

What this changes

This 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.

  • Before: Unchanged Grok publications trigger repeated Codex scans; the supplied app run consumed 16.17 CPU seconds over the measurement window.
  • After: Unchanged publications retain the same dashboard configuration; the matching patched run consumed 3.18 CPU seconds.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A focused, correct repair with shipped-app evidence and useful regressions; broader automated validation is not established by the available head checks.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (logs): The attached packaged-app CPU series, inspected chart, and reduced profiles exercise the changed dashboard path with real Codex and Grok history and resolve the previous request for inspectable runtime evidence; the logs explicitly disclose the baseline symbolication limitation.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Product

Kind: Bug fix · Worth it: Yes · Fix scope: Complete
User problem: CodexBar repeatedly consumes CPU rescanning unchanged history in the background when snapshot-backed spend providers refresh.
Reason: Removing false invalidations provides demonstrated user value without changing settings, provider ownership, or the intended spend output.

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
Reviewed head: eabbd0327775240cc8e5833616069fa4eef78217

Before merge

None.

Findings

None.

Agent review details

How this fits together

UsageStore 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]
Loading

Technical review

Best 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

Testing

Proof path: shipped entry point.

Security

None.

Evidence

What I checked:

Likely related people:

  • Alec Gutman, Chip: Raw commit 0397529 adds Sources/CodexBar/SpendDashboardController.swift:366 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 0397529ae6e5; files: Sources/CodexBar/SpendDashboardController.swift)
  • Yuxin Qiao: Raw commit d6a90ad adds Sources/CodexBar/SpendDashboardController.swift:506 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: d6a90ad88787; files: Sources/CodexBar/SpendDashboardController.swift)
  • Peter Steinberger: Raw commit 458afc6 adds Sources/CodexBar/SpendDashboard/SpendDashboardController.swift:1281 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 458afc6017c4; files: Sources/CodexBar/SpendDashboard/SpendDashboardController.swift)

Preflight source

Source 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

Metric Value Why it matters
Production and regression coverage Production +66/-3 lines; tests +295 lines Growth stays within existing dashboard files and adds seven distinct regressions for content stability, corrections, empty transitions, and calendar boundaries.

Labels

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR.
  • remove rating: 🧂 unranked krab: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P2: Repeated background history scans waste CPU for affected provider combinations without blocking the core app workflow.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR.
  • proof: sufficient: Contributor real behavior proof is sufficient.

Rating scale

6/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.

Workflow

ClawSweeper edits this one comment on every review. Comment @clawsweeper re-review for a fresh review only. Repair and merge execution lanes are retired.

History

Review history (1 earlier review cycle)
  • reviewed 2026-11-12T14:05:50.271Z sha 390a3e2 :: needs real behavior proof before merge. :: none

Reviewed November 12, 2026, 10:12 AM ET / 15:12 UTC (Revision 2).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@daniel-inderos

Copy link
Copy Markdown
Author

Added real behavior proof in .github/pr-proof/ (raw CPU series, reduced sample profiles, chart) and updated the PR body with the paired main vs. head run.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🦞👀
Exact review queued.

Re-review progress:

@clawsweeper clawsweeper Bot added clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. labels Nov 12, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant