feat(spend): cache-first and 5m TTL for dashboard - #3107
Conversation
|
🦞👀 Pull request received. I will update this pull request when review starts. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f4bbb2cc7
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !force, | ||
| let lastAt = self.lastSpendDashboardTokenFetchAt[provider.instanceID], | ||
| let lastScope = self.lastSpendDashboardTokenFetchScope[provider.instanceID], | ||
| lastScope == costScopeSignature, | ||
| self.spendDashboardTokenSnapshotPublicationForCurrentConfig(for: provider) != nil, |
There was a problem hiding this comment.
Route a non-forced request through the TTL check
This TTL cannot fire through production code: the sole caller in SpendDashboardSource.makeRequest always passes force: true, while .refreshMissing invokes that caller only when no current publication exists—even though this condition requires one. Consequently, the new five-minute guard cannot suppress any dashboard token scan; the caller needs to preserve the build mode's forced/non-forced semantics or perform the TTL decision before the missing-publication predicate.
Useful? React with 👍 / 👎.
|
Codex review: needs real behavior proof before merge. Reviewed August 21, 2026, 6:14 PM ET / 22:14 UTC. ClawSweeper reviewWhat this changesThe PR adds cache-first dashboard loading, a five-minute token-snapshot TTL, and bounded usage-log/cache hydration to reduce repeated spend-dashboard scans. Merge readiness⛔ Blocked until real behavior proof is added - 8 items remain This PR is still necessary, but two cache-first paths can publish incorrect spend data and need repair before merge. Priority: P2 Review scores
Verification
Live VerificationCommand: Result: FAIL (failed) — execution before step 1 Assertions:
How this fits togetherThe spend dashboard collects provider token snapshots and local usage logs, then builds and publishes a cost report to dashboard panes. This change decides when existing data is reused versus rescanned and how cached usage is hydrated. flowchart LR
A[Provider snapshots and local logs] --> B[Spend dashboard request]
B --> C[Freshness and scope check]
C --> D[Cache or provider refresh]
D --> E[Cost report builder]
E --> F[Dashboard pane]
Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Copy recommended automerge instructionTechnical reviewBest possible solution: Keep cache hydration report-equivalent to a full scan and reuse legacy data only after proving it covers the dashboard’s 365-day scope. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows aggregate hydration drops row metadata used by the report builder, and legacy freshness can suppress the required 365-day dashboard refresh. Is this the best way to solve the issue? No. The fast path needs report-equivalence coverage and the legacy fallback must verify dashboard-window coverage before it can safely skip a fetch. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 416ef870aaf0. LabelsLabel changes:
Label justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (5 earlier review cycles)
|
|
CI failure is a real compile error:
|
- shouldPrimeCachedCodex now also primes when loadedInputs is empty even in forcing phase, so cold-start 全部 with empty model gets 50ms cached Codex snapshot instead of 2s empty → groups. Covers the Refresh-while-empty path. - refreshSpendDashboardTokenUsageNow respects 5m TTL for non-forced pane re-open: reuses existing spendDashboardTokenPublication when scope unchanged and last fetch <5m, avoiding 365d rescan on every tab switch. Evidence: - SpendDashboardController.swift:1083 shouldPrimeCachedCodex - UsageStore+SpendDashboardTokenCost.swift:72 TTL gate Before: tab switch → 365d scan; cold 全部 empty 2s. After: warm tab switch 0s, cold 全部 50ms cached.
4582171 to
d3596a6
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d3596a6a84
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let shouldPrimeCachedCodex: Bool = self.cachedLoader != nil | ||
| && !Set(Self.codexOwnershipByID(configuration.codexAccountIdentities).keys) | ||
| .isSubset(of: Set(self.loadedInputs.map(\.id))) | ||
| && (phase == .ordinary || self.loadedInputs.isEmpty) |
There was a problem hiding this comment.
Pattern-match the load phase instead of comparing it
LoadPhase does not conform to Equatable, and its reconciling(ForcedOutcome) payload prevents synthesized conformance, so phase == .ordinary produces a “binary operator '==' cannot be applied” compiler error and blocks every build. Preserve the prior if case .ordinary = phase pattern or introduce an explicit case-checking property; the repository-required test run would have caught this before handoff.
AGENTS.md reference: AGENTS.md:L23-L24
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac856d5c36
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let identity = "\(logURL.path)|\(size)|\(mtime)" | ||
|
|
||
| if let cached = self.readCachedEntries(identity: identity), !cached.isEmpty { | ||
| if let cached = self.readCachedEntries(identity: identity, since: since) { |
There was a problem hiding this comment.
Apply the report cutoff on cache misses
When usage.jsonl has changed since the previous read—as it normally does whenever a new request is appended—the identity check here misses and loadEntries reparses, deduplicates, sorts, and returns the entire lifetime log without applying since. Consequently, the new window only limits SQLite cache-hit reads, while active users continue hitting the unbounded latency and memory path this change is meant to avoid; apply the cutoff in the parser/miss path as well, while retaining the full cache if needed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 73bc2a1a34
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for baseline in providerBaselines where mode.shouldRefresh( | ||
| hasPublication: baseline.publication != nil, | ||
| isDashboardTokenStale: store.spendDashboardTokenFetchIsStale(for: baseline.provider)) |
There was a problem hiding this comment.
Trigger the TTL check when reopening the pane
When the pane is reopened after five minutes without a configuration change, SpendDashboardPane.onAppear calls update(configuration:), but the equality guard in SpendDashboardController.update returns before building a request, so the stale predicate added here is never evaluated. Fresh evidence in the current tree is that the patch now correctly passes the non-forced mode to the loader, while the unchanged controller guard still prevents that path from running on an ordinary reopen. The cached snapshot can therefore remain beyond the advertised TTL until a configuration/date change or manual refresh triggers another load.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
When usage.jsonl changes, this cache-miss path still parses the entire lifetime file, materializes every entry in unique, sorts the full collection, and rewrites all entries before the since filter runs. Thus active logs remain subject to unbounded peak memory and refresh latency even though the returned array is windowed. Fresh evidence in the current tree is that the follow-up only filters deduped after these full-history operations; pass the cutoff into parsing or otherwise avoid materializing old entries on the report path.
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b577455ac9
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let rowsByPath = mode == .scanReady | ||
| ? Dictionary(grouping: snapshot.usageRows, by: \.path) | ||
| : [:] |
There was a problem hiding this comment.
Preserve row metadata in aggregate report hydration
When cached Codex usage contains reasoning tokens or usage preceding a historical pricing cutoff, .aggregateReport discards the persisted rows and reconstructs them from day/model aggregates. Those synthesized rows omit both reasoning and timestampUnixMs, while buildCodexReportFromCache derives displayed reasoning totals and date-sensitive pricing from those fields. Consequently, the cache-first snapshot can report zero reasoning and price historical usage at current rates; retain the required metadata in the aggregate representation or decode the rows needed for an equivalent report.
Useful? React with 👍 / 👎.
| guard self.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) != nil, | ||
| let legacyLast = self.lastTokenFetchAt[provider.instanceID] | ||
| else { return true } | ||
| return Date().timeIntervalSince(legacyLast) >= 5 * 60 |
There was a problem hiding this comment.
Require full dashboard coverage before adopting legacy freshness
When the regular token pipeline has just published its default 30-day snapshot before the first dashboard open, this branch treats it as fresh solely from its timestamp even though the dashboard requests scanDays == 365. capturedTokenPublication then falls back to that legacy snapshot and skips the independent dashboard fetch, so the dashboard's 365-day/All view is populated with only 30 days of history until another refresh is triggered. Reuse legacy freshness only when its history coverage and scope satisfy the dashboard request.
Useful? React with 👍 / 👎.
Complements #3105 (parallel) and #3106 (silent) with cache-first.
Cache-first
SpendDashboardController.swift:1083shouldPrimeCachedCodexwasphase == .ordinaryonly. Cold全部withloadedInputs.isEmptyandforceRefresh(user taps Refresh while empty) never primed cache → 2s empty正在刷新. Now also primes when empty, so first paint usesloadCached:33050ms sqlite snapshot.TTL
UsageStore+SpendDashboardTokenCost.swift:72refreshSpendDashboardTokenUsageNowhad no TTL beyondinFlight, so every pane re-open (makeRequest:237refreshMissing) forced a 365d rescan. Add 5m TTL for non-forced calls when scope unchanged and publication exists. Pane tab switch now 0s.Evidence
SpendDashboardController.swift:1083primingUsageStore+SpendDashboardTokenCost.swift:72TTLswiftformat+swiftlint --strictcleanFollow-up for full cross-restart persistence is tracked separately.