Skip to content

Stop re-merging the Codex plan-utilization history with itself on every refresh - #3141

Merged
steipete merged 1 commit into
steipete:mainfrom
olddonkey:perf/plan-utilization-selfmerge
Aug 23, 2026
Merged

Stop re-merging the Codex plan-utilization history with itself on every refresh#3141
steipete merged 1 commit into
steipete:mainfrom
olddonkey:perf/plan-utilization-selfmerge

Conversation

@olddonkey

@olddonkey olddonkey commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

UsageStore.materializeCodexPlanUtilizationHistoryIfNeeded exists to fold legacy, opaque and unscoped Codex plan-utilization buckets into the canonical account bucket. Its scoped loop also appended the canonical bucket's own histories to historiesToMergematchesTargetContinuity is true for rawKey == canonicalKey (CodexHistoryOwnership.swift:58-61), and only the removal of the old key was guarded by if rawKey != canonicalKey. So guard !historiesToMerge.isEmpty never fired once the canonical bucket had any history, and the migration merge ran on every successful provider refresh and every menu open, merging the history with itself.

That merge is quadratic: updatedPlanUtilizationEntries copied the whole entry array per entry, scanned it linearly for the insertion point, and allocated the same-hour slice. Measured with an optimized standalone reproduction over a real three-month-old history (session 1909 entries, weekly 2239): 20.6 ms of MainActor time per call, scaling ~3.9× per doubling. planUtilizationMaxSamples is 17520 per series, so it keeps growing — at the retention cap this would be several hundred ms per series, on the main thread, while the menu is opening.

Reached from UsageStore+Refresh.swift (applyProviderRefreshSuccessrecordPlanUtilizationHistorySampleMainActor.runresolvePlanUtilizationAccountKeyresolveCodexPlanUtilizationAccountKey) and from the synchronous planUtilizationHistorySelection(for:) used while building the menu. Codex-only; Claude short-circuits earlier.

Changes

  • Only merge when a foreign source contributed. A flag is set in the three branches that add non-canonical data (legacy scoped key, opaque recovery, unscoped adoption) and required by the guard, so the canonical-only case returns without merging or rewriting anything. Every path where a legacy/opaque/unscoped bucket does contribute is untouched: legacyRawKeysToRemove is only ever populated in branches that also set the flag (so no removal is skipped), and providerBuckets.unscoped is cleared only inside the branch that sets it.
  • Make the genuine migration near-linear. updatedPlanUtilizationEntries mutates the array in place and finds the insertion point with a binary search for the same strict upper bound (plus a fast path for the common append); mergedPlanUtilizationHistories accumulates per series and constructs each history once.
  • Enforce the sortedness invariant the binary search relies on. Every in-app producer went through PlanUtilizationSeriesHistory's designated initializer, which sorts — except the synthesized Codable decoder, which assigned entries verbatim from JSON. An explicit init(from:) now routes decoding through that initializer, so a history file written by an older build (or edited by hand) cannot smuggle in an unsorted series. The JSON shape is unchanged.

The skipped self-merge was also an incidental normalizer — it re-ran per-hour peak canonicalization on read, which is not a fixpoint. Repairing legacy data belongs at load time, not on every refresh and menu open, so that is not reintroduced here; the visible effect is that at most one extra real observation per affected hour is kept. This is commented at the guard.

Measured on real data

Against this machine's real history/codex.json (4160 entries: session 1909, weekly 2251), driving the real UsageStore.planUtilizationHistory(for: .codex) read path (swift test debug builds, same machine; an optimized standalone reproduction of the same algorithm measured 20.6 ms per call for current main):

Path current main this PR
canonical-only (every refresh, every menu open) 243–274 ms 0.94–1.12 ms
genuine legacy migration 238–273 ms 13.8–16.8 ms

The migration result is unchanged: the legacy bucket is absorbed, one account bucket remains, all 4160 entries survive. Full terminal transcripts are in a comment below.

Tests

  • Canonical-only history is returned untouched and enqueues no persistence write (planUtilizationHistoryRevision unchanged) — the property that actually matters for the menu path.
  • A genuine foreign merge matches an explicit expected result across overlapping hours, out-of-order sources, distinct session/weekly series, and retention trimming.
  • The binary search's upper-bound contract is pinned directly through the DEBUG shim over a 65-entry array containing a run of equal capturedAt (a lower bound would return a different index), plus the endIndex/startIndex edges.
  • Decoding a series whose JSON entries are out of order yields a sorted series, and the shuffled payload produces the same forecast estimate as the sorted fixture.

The existing Codex-ownership migration suite (12 cases, all of which seed a foreign key) is unchanged and still green.

Process

Implemented by grok-4.6 (xhigh) via implementation-loop; reviewed hunk by hunk plus an independent deep review that confirmed both equivalence claims by differential fuzzing (200k sorted cases, zero mismatches; and an exhaustive path enumeration showing the foreign-source paths are byte-identical) and found the decoder gap, fixed in one iterate round. The line-anchored ProviderArchitectureGatekeeperTests entries for the touched file were re-verified independently after every edit. Full make check and make test (77/77 sharded groups, zero failures) pass locally on this head.

🤖 Generated with Claude Code

@clawsweeper

clawsweeper Bot commented Aug 22, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

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

olddonkey added a commit to olddonkey/CodexBar that referenced this pull request Aug 22, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 22, 2026
@clawsweeper

clawsweeper Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 22, 2026, 3:09 PM ET / 19:09 UTC.

ClawSweeper review

What this changes

The PR skips canonical-only Codex plan-history rewrites, makes genuine legacy-bucket merging cheaper, and normalizes decoded history ordering.

Regression provenance

Possible regression — probable (reviewed change; reproduction). No predecessor PR is attributed.

Merge readiness

⚠️ Ready for maintainer review - 1 item remains

Keep open: this is a focused, source-supported fix for an active main-thread history rewrite, with sufficient real-path terminal proof and no blocking correctness or security finding.

Priority: P2
Reviewed head: d45b9617c7c4d6d9e2e8d18c891242bd2ce199eb

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) Strong real-path terminal proof and focused regression coverage support a narrow repair with no identified correctness blocker.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The contributor supplied before-and-after terminal output from the real Codex persisted-history read path, showing both the improved result and preserved legacy migration outcome; private data should remain redacted in any future proof updates.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The contributor supplied before-and-after terminal output from the real Codex persisted-history read path, showing both the improved result and preserved legacy migration outcome; private data should remain redacted in any future proof updates.
Evidence reviewed 5 items Current-main defect and narrow guard: Current main merges whenever any matching bucket is present; the PR makes the merge contingent on a non-canonical scoped, opaque, or unscoped contributor, leaving canonical-only reads unchanged.
Migration preservation: The foreign-source branches retain the canonical history in the merge input while removing only old scoped keys or clearing unscoped history after it contributes.
Sorted persisted input: The explicit decoder routes persisted entries through the existing sorting initializer, establishing the captured-time ordering required by binary-search insertion without changing the encoded fields.
Findings None None.
Security None None.

Live Verification

Command: swift test --filter UsageStorePlanUtilizationCodexMergeTests

Result: FAIL (failed) — execution before step 1 run: sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

Assertions:

  • FAIL expect_output: Test run with

How this fits together

CodexBar stores provider utilization samples in account-keyed persisted history buckets. Codex account resolution runs during refreshes and menu history reads, optionally migrating legacy or unscoped data before menu and forecast consumers receive the selected history.

flowchart LR
A[Provider refresh] --> B[History buckets]
C[Menu history read] --> D[Codex account resolution]
B --> D
D --> E{Foreign history found?}
E -->|yes| F[Merge and persist canonical history]
E -->|no| G[Return canonical history unchanged]
F --> H[Menu and forecast]
G --> H
Loading

Before merge

  • Complete next step (P2) - No repair is requested; this PR can proceed through normal merge checks on its reviewed head.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production versus test delta 6 files; production +107/-28, tests +301/-9 The implementation change is accompanied by focused coverage for canonical-only reads, legacy merging, retention, binary-search edges, and decode normalization.

Technical review

Best possible solution:

Keep canonical-only reads as no-ops while preserving the existing guarded migration paths for legacy, opaque, and unscoped Codex history.

Do we have a high-confidence way to reproduce the issue?

Yes: current-main source directly permits the canonical bucket to satisfy the old non-empty merge guard, and the contributor provided before-and-after real-history terminal measurements for the read path.

Is this the best way to solve the issue?

Yes: the foreign-source guard fixes the unnecessary rewrite at the existing migration boundary, while the merge optimization and sorted decode invariant preserve the required migration path.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 27c7f334e3c4.

Labels

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The contributor supplied before-and-after terminal output from the real Codex persisted-history read path, showing both the improved result and preserved legacy migration outcome; private data should remain redacted in any future proof updates.
  • 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. Sufficient (terminal): The contributor supplied before-and-after terminal output from the real Codex persisted-history read path, showing both the improved result and preserved legacy migration outcome; private data should remain redacted in any future proof updates.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P2: The PR fixes normal Codex menu and refresh responsiveness without evidence of data loss, security impact, or a service outage.
  • 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. Sufficient (terminal): The contributor supplied before-and-after terminal output from the real Codex persisted-history read path, showing both the improved result and preserved legacy migration outcome; private data should remain redacted in any future proof updates.
  • proof: sufficient: Contributor real behavior proof is sufficient. The contributor supplied before-and-after terminal output from the real Codex persisted-history read path, showing both the improved result and preserved legacy migration outcome; private data should remain redacted in any future proof updates.

Evidence

What I checked:

Likely related people:

  • Peter Steinberger: Recent provider-architecture and plan-history work dominates the central file's history, including the current Codex account-resolution structure. (role: recent area contributor; confidence: high; commits: 7deae2acc44e, 01076474bab4; files: Sources/CodexBar/UsageStore+PlanUtilization.swift, Sources/CodexBar/PlanUtilizationHistoryStore.swift)
  • Yuxin Qiao: History loading and persistence work in this subsystem includes their prior changes, making them a useful secondary routing contact. (role: adjacent history contributor; confidence: medium; commits: 04e4898a92f8; files: Sources/CodexBar/UsageStore+PlanUtilization.swift, Sources/CodexBar/PlanUtilizationHistoryStore.swift)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (1 earlier review cycle)
  • reviewed 2026-08-22T09:34:54.546Z sha 331bd44 :: needs real behavior proof before merge. :: none

…ry refresh

`materializeCodexPlanUtilizationHistoryIfNeeded` exists to fold legacy, opaque
and unscoped Codex plan-utilization buckets into the canonical account bucket.
Its scoped loop also appended the canonical bucket's own histories to
`historiesToMerge` — `matchesTargetContinuity` is true for
`rawKey == canonicalKey`, and only the removal of the old key was guarded — so
`guard !historiesToMerge.isEmpty` never fired once the canonical bucket had any
history, and the migration merge ran on every successful provider refresh and
every menu open, merging the history with itself.

That merge is quadratic: `updatedPlanUtilizationEntries` copied the whole entry
array per entry, scanned it linearly for the insertion point, and allocated the
same-hour slice. Measured with an optimized standalone reproduction over a real
three-month-old history (session 1909 entries, weekly 2239): 20.6 ms of MainActor
time per call, scaling ~3.9x per doubling. `planUtilizationMaxSamples` allows
17520 entries per series, so it would keep growing.

Two changes:

- Track whether a foreign source actually contributed and require that in the
  guard, so the canonical-only case returns without merging or rewriting
  anything. Every path where a legacy, opaque or unscoped bucket contributes is
  untouched; `legacyRawKeysToRemove` is populated only in branches that also set
  the flag, so no removal is skipped, and `providerBuckets.unscoped` is cleared
  only inside the branch that sets it.
- Make the merge itself near-linear: `updatedPlanUtilizationEntries` mutates the
  array in place and finds the insertion point with a binary search for the same
  strict upper bound (with a fast path for the common append), and
  `mergedPlanUtilizationHistories` accumulates per series and builds each history
  once.

The binary search assumes entries are sorted by `capturedAt`, which every
in-app producer guaranteed through `PlanUtilizationSeriesHistory`'s designated
initializer — except the synthesized `Codable` decoder, which assigned entries
verbatim from JSON. An explicit `init(from:)` now routes decoding through that
initializer, so an on-disk history written by an older build or edited by hand
cannot smuggle in an unsorted series.

The skipped self-merge also incidentally re-canonicalized per-hour peaks on
read; that repair belongs at load time, not on every refresh, and is not
reintroduced here. The visible effect is that at most one extra real observation
per affected hour is kept.

Tests: canonical-only history is returned untouched and enqueues no persistence
write (the history revision is unchanged); a genuine foreign merge matches an
explicit expected result across overlapping hours, out-of-order sources, distinct
series and retention trimming; the binary search's upper-bound contract is pinned
directly through a DEBUG shim over an array with a run of equal timestamps (a
lower bound would return a different index); and decoding a series whose JSON
entries are out of order yields a sorted series.

Implemented by grok-4.6 (xhigh) via implementation-loop; reviewed hunk by hunk
plus an independent deep review that confirmed both equivalences by differential
fuzzing (200k sorted cases with no mismatch) and found the decoder gap, fixed in
one iterate round. Gatekeeper line anchors for the touched file were re-verified
independently.

The DEBUG sortedness assertion is checked once per merged series rather than
once per inserted entry: a per-entry check is itself O(n) and reintroduced, in
debug builds, exactly the quadratic scan this insertion path removes (measured
over the real 4160-entry history: a legacy migration took ~1000 ms with the
per-entry assertion versus ~15 ms without it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@olddonkey
olddonkey force-pushed the perf/plan-utilization-selfmerge branch from 331bd44 to d45b961 Compare August 22, 2026 19:04
@olddonkey

Copy link
Copy Markdown
Contributor Author

Real-behavior proof (requested before merge)

Measured against this machine's actual on-disk Codex plan-utilization historyhistory/codex.json, 4160 entries across 2 series (session window 300: 1909 entries, weekly window 10080: 2251 entries), roughly three months of normal use. The harness loads that real file, installs it under the canonical account key, and drives the real read path UsageStore.planUtilizationHistory(for: .codex) — the same call the menu build and recordPlanUtilizationHistorySample go through — printing wall-clock per call. It is a measurement harness, not an assertion suite: the numbers below are its stdout.

Both sides are swift test debug builds of the same package on the same machine, so the absolute numbers are roughly an order of magnitude above release; the ratio is the point. (An optimized standalone reproduction of the same algorithm over the same history measured 20.6 ms per call for the current-main behaviour.)

Before — 27c7f334e (current main)

MEASURE: loaded 2 series / 4160 entries from the real history file
MEASURE:   series session window=300 entries=1909
MEASURE:   series weekly window=10080 entries=2251
MEASURE: [canonical-only] 5 read-path calls: 249.11, 250.00, 242.93, 274.46, 243.06 ms
MEASURE: [legacy migration] 5 calls: 264.51, 257.88, 272.79, 262.58, 238.06 ms
MEASURE: [legacy migration] account buckets now: 1 (legacy key removed: true)
MEASURE: [legacy migration] merged series: 2, entries: 4160

After — this PR

MEASURE: loaded 2 series / 4160 entries from the real history file
MEASURE:   series session window=300 entries=1909
MEASURE:   series weekly window=10080 entries=2251
MEASURE: [canonical-only] 5 read-path calls: 1.12, 0.96, 0.96, 0.94, 0.94 ms
MEASURE: [legacy migration] 5 calls: 16.01, 13.83, 16.79, 13.78, 14.16 ms
MEASURE: [legacy migration] account buckets now: 1 (legacy key removed: true)
MEASURE: [legacy migration] merged series: 2, entries: 4160

Canonical-only refresh behaviour (the state every normal single-account user is in, on every provider refresh and every menu open): 243–274 ms → 0.94–1.12 ms. The remaining work is reading the bucket; the merge no longer runs.

A real migration outcome is unchanged in result and faster: the legacy bucket is still absorbed (legacy key removed: true, one account bucket remains) and the merged series still carry all 4160 entries — at 238–273 ms → 13.8–16.8 ms, which is the in-place insert plus binary search doing what it was meant to do.

One fix this measurement produced

The first version of this branch put the DEBUG sortedness assertion inside updatedPlanUtilizationEntries, i.e. an O(n) scan per inserted entry — which reintroduced, in debug and test builds only, exactly the quadratic scan this insertion path exists to remove (the legacy migration measured ~1000 ms with it, ~15 ms without). It is now checked once per merged series. Release builds were never affected, but the assertion as written would have slowed every developer's test run; thanks to whatever prompted the "show real behaviour" ask, since a release-only benchmark would have hidden it.

Also in this push

The CHANGELOG.md entry has been removed from this branch — docs/RELEASING.md reserves the changelog for the release flow. For whoever writes the release notes:

  • Codex: the plan-utilization history is no longer re-merged with itself on every refresh. The legacy-bucket migration also folded the canonical bucket into its own merge, so a quadratic merge ran on the main thread on every provider refresh and every menu open (~20 ms on a three-month-old history, growing with the square of its size); it now runs only when a legacy, opaque or unscoped bucket actually needs migrating, and the merge inserts in place with a binary search instead of copying the whole array per entry (Stop re-merging the Codex plan-utilization history with itself on every refresh #3141).

make check and the full make test (77/77 sharded groups, zero failures) pass on the pushed head.

@clawsweeper clawsweeper Bot added 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 status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Aug 22, 2026
@steipete
steipete merged commit 6ef5c5a into steipete:main Aug 23, 2026
9 checks passed
steipete added a commit that referenced this pull request Aug 23, 2026
steipete added a commit that referenced this pull request Aug 23, 2026
…easoning split, stale (#3120)

* Align Codex token parsing with tokscale stale snapshots

- skip lightly regressed cumulative snapshots before interleaved latching
- take the maximum of cached and cache-read fields in all parsers
- cover cache field selection and out-of-order snapshot accounting with focused tests

* Refresh Codex parser hash

* Parse bare usage rows in Codex rollouts

* Fix stale reasoning and fallback cache parity

* Fix Codex fallback test fixture line handling

* fix(antigravity): repair offline fallback proof and oauth fallback; fix(spend): limit concurrent dashboard fetches to 3

* fix(lint): break long lines in offline fallback proof tests

* fix(tests): update gatekeeper anchors for spend dashboard concurrency limit

* fix(tests): correct gatekeeper line anchors for concurrent dashboard fix

* docs: update appcast for 0.54.1

* chore: open 0.54.2 unreleased changelog section

* Stop re-merging the Codex plan-utilization history with itself on every refresh (#3141)

`materializeCodexPlanUtilizationHistoryIfNeeded` exists to fold legacy, opaque
and unscoped Codex plan-utilization buckets into the canonical account bucket.
Its scoped loop also appended the canonical bucket's own histories to
`historiesToMerge` — `matchesTargetContinuity` is true for
`rawKey == canonicalKey`, and only the removal of the old key was guarded — so
`guard !historiesToMerge.isEmpty` never fired once the canonical bucket had any
history, and the migration merge ran on every successful provider refresh and
every menu open, merging the history with itself.

That merge is quadratic: `updatedPlanUtilizationEntries` copied the whole entry
array per entry, scanned it linearly for the insertion point, and allocated the
same-hour slice. Measured with an optimized standalone reproduction over a real
three-month-old history (session 1909 entries, weekly 2239): 20.6 ms of MainActor
time per call, scaling ~3.9x per doubling. `planUtilizationMaxSamples` allows
17520 entries per series, so it would keep growing.

Two changes:

- Track whether a foreign source actually contributed and require that in the
  guard, so the canonical-only case returns without merging or rewriting
  anything. Every path where a legacy, opaque or unscoped bucket contributes is
  untouched; `legacyRawKeysToRemove` is populated only in branches that also set
  the flag, so no removal is skipped, and `providerBuckets.unscoped` is cleared
  only inside the branch that sets it.
- Make the merge itself near-linear: `updatedPlanUtilizationEntries` mutates the
  array in place and finds the insertion point with a binary search for the same
  strict upper bound (with a fast path for the common append), and
  `mergedPlanUtilizationHistories` accumulates per series and builds each history
  once.

The binary search assumes entries are sorted by `capturedAt`, which every
in-app producer guaranteed through `PlanUtilizationSeriesHistory`'s designated
initializer — except the synthesized `Codable` decoder, which assigned entries
verbatim from JSON. An explicit `init(from:)` now routes decoding through that
initializer, so an on-disk history written by an older build or edited by hand
cannot smuggle in an unsorted series.

The skipped self-merge also incidentally re-canonicalized per-hour peaks on
read; that repair belongs at load time, not on every refresh, and is not
reintroduced here. The visible effect is that at most one extra real observation
per affected hour is kept.

Tests: canonical-only history is returned untouched and enqueues no persistence
write (the history revision is unchanged); a genuine foreign merge matches an
explicit expected result across overlapping hours, out-of-order sources, distinct
series and retention trimming; the binary search's upper-bound contract is pinned
directly through a DEBUG shim over an array with a run of equal timestamps (a
lower bound would return a different index); and decoding a series whose JSON
entries are out of order yields a sorted series.

Implemented by grok-4.6 (xhigh) via implementation-loop; reviewed hunk by hunk
plus an independent deep review that confirmed both equivalences by differential
fuzzing (200k sorted cases with no mismatch) and found the decoder gap, fixed in
one iterate round. Gatekeeper line anchors for the touched file were re-verified
independently.

The DEBUG sortedness assertion is checked once per merged series rather than
once per inserted entry: a per-entry check is itself O(n) and reintroduced, in
debug builds, exactly the quadratic scan this insertion path removes (measured
over the real 4160-entry history: a legacy migration took ~1000 ms with the
per-entry assertion versus ~15 ms without it).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Fix Codex day cost blanked by trace-only priority turns (#3150)

Row ownership evidence compared the retained rows against the persisted
standard/priority split using the trace database's tier classification.
The persisted maps come from the rows' own pricingMode, so a turn the
trace reports as priority after its rows were persisted as standard read
as a row-ownership mismatch, the rows lost trust, and the day fell back
to the aggregate — which returns nil for long-context tiered models, so
the whole day's cost disappeared from the menu, the chart and the window
total.

Judge retention against both classifications and flag only a group that
matches neither. A wrongly retained row set still fails both, because the
persisted totals are canonical for the file and tier classification never
changes how many tokens the rows carry.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs: credit #3141 and #3150 changelog entries

* fix(qwen-cloud): restore Brave browser support in cookie import (#3148)

* fix(qwen-cloud): restore Brave browser support, narrowed to Chrome+Brave per AGENTS.md

Qwen Cloud's cookie import was restricted to [.chrome] only (commit
529cc6c 'Keep Qwen imports Chrome-only'). Brave users hit 'No Qwen
Cloud session cookies found in browsers' even when they had a valid
Qwen Cloud session in Brave, because their cookies were never probed.

This commit restores Brave in the import order, but follows
AGENTS.md L48 ('default Chrome-only when possible to avoid other
browser prompts; override via browser list when needed'). The override
is the minimum necessary: Chrome + Brave. The other Chromium browsers
(chromeBeta, edge, arc, firefox, safari) are deliberately omitted to
avoid unsolicited Keychain / browser-store access prompts on
automatic refreshes from browsers that don't carry a Qwen Cloud
session. Brave is kept because it shares the same Chromium Safe
Storage format as Chrome and is a common Qwen Cloud authentication
target.

Also adds docs/qwen-cloud-proof/README.md with the redacted end-to-end
proof captured against the live Qwen Cloud API from the user's Mac
after granting the modified binary access to 'Brave Safe Storage' in
macOS Keychain.

* fix(qwen-cloud): recovery message now names Brave alongside Chrome

ClawSweeper P2 follow-up on #3148: when the Brave cookie import
fails, QwenCloudSettingsError.missingCookie's recovery message
still told users to sign in to Chrome and grant access to Chrome
Safe Storage. Now that Brave is a supported source, the message
must name both browsers and their respective Safe Storage entries,
otherwise a Brave-only user would be told to use Chrome and never
find the working path.

Updates the error description to:
  'No Qwen Cloud session cookies found in browsers. Sign in to
   Qwen Cloud in Chrome or Brave, allow CodexBar to access the
   corresponding Safe Storage in Keychain Access (Chrome Safe
   Storage and/or Brave Safe Storage), or paste a manual Cookie
   header.'

Adds focused test coverage:
- missing cookie error mentions both supported browsers and their safe storage
- missing cookie error appends non-empty details
- missing cookie error omits empty details

35/35 Qwen Cloud tests pass (32 prior + 3 new).

* Fix OpenRouter completed-day activity query (#3138)

* Preserve unknown Grok period usage (#3159)

Co-authored-by: anupamchugh <8416306+anupamchugh@users.noreply.github.com>

* fix: report non-writable CLI path conflicts (#3153)

* fix: prefer successful CLI install status

* fix: keep CLI path conflicts visible

* fix: report non-writable CLI path conflicts

* docs: add CLI conflict behavior proof

* docs: add CLI install comparison screenshots

* Fix single-quota icon scaling (#3155)

* docs: credit #3138 #3148 #3153 #3155 #3159 changelog entries

* fix(spend): silent refresh and invalidation coverage (#3106)

* fix(spend): bucket calendar for all heatmap dates and full revision hash

- SpendActivityDateFormatting.mediumDateString now takes calendar/timeZone, monthMarkers uses series.calendar, tooltips and accessibility use bucket calendar.
- selectedDay renormalized on calendar change to keep toggle correct.
- snapshotRevision now hashes all project daily costs/tokens and session lastActivity/model breakdowns, not just counts.

Fixes ClawSweeper P2 for #3106.

* fix(gatekeeper): update anchors and add provider-specific design markers for spend dashboard

* Update provider gatekeeper anchors for v0.54 rebase

* fix(test): pin claude spend snapshot in observation test

* fix(test): seed pinned claude spend publication before first snapshot

* fix: resolve remaining conflict markers from gatekeeper rebase

* fix(lint): shorten Sakana test lines

* fix(spend): restore heatmap calendar property lost in rebase

* Extend spend publication test wait

* Restore spend gatekeeper anchors after rebase

* fix(spend): sync independent snapshot and bucket calendar normalization for 3106

- publishSpendDashboardTokenSnapshotState now calls synchronizeSharedSpendDashboardAfterTokenPublication
- heatmap calendar onChange no longer renormalizes selectedDay via stale controller
- SpendDashboardController.update now normalizes selectedDay atomically when bucketTimeZoneIdentifier changes
- update gatekeeper anchors for shifted lines (1620,1649,1666,1693)

* test(spend): cover independent snapshot sync for 3106

Exercise the direct independent publication path added at
UsageStore+SpendDashboardTokenCost.swift:181. The prior focused test
seeded Claude before observation and then used the regular Codex
publisher, which already syncs independently, so removing that line
would not fail. Add a post-start Claude snapshot via
_setSpendDashboardTokenSnapshotForTesting and assert the shared
dashboard debounced sync is scheduled and the publication inputs
update. Verified: swiftformat clean, swiftlint --strict clean,
swift test --filter SpendDashboardPublicationTests 18 tests passed.

* docs: add fresh-bundle proof for 3106

Add redacted menu-icon crop and dashboard snapshot from debug build
2798eec (swift build --target CodexBarCLI, .build/debug/CodexBarCLI
dashboard --pretty). The snapshot shows the shared spend controller
produces a dashboard with provider rows/windows, confirming the
independent-sync and calendar paths are live in the fresh binary.

* docs: add menu and Spend dashboard screenshots for 3106

Add redacted screenshots from fresh debug build 45ba984:
- 3106-menu-after-fix.png: menu bar extra open, showing provider rows
- 3106-settings-after-fix.png: Settings window (general)
- 3106-spend-dashboard-after-fix.png: Usage & Spend pane (usageSpend)
with heatmap and Overview, confirming the shared controller renders
in the fresh bundle.

* docs: remove screenshots for 3106 per request

Keep only the redacted CLI dashboard snapshot JSON as fresh-bundle
proof; screenshots are not needed.

* Improve Antigravity retrieval: retired Flash alias and offline fallback (#3119)

* fix(antigravity): allow OAuth errors to fallback to offline when local data exists

Fix P2 from Codex review on #3119: AntigravityOAuthFetchStrategy.shouldFallback
now checks hasOfflineData, so expired credentials do not block offline.

* fix(antigravity): unbind offline account, read app-data, bound scans + proof

- Offline snapshot now has nil accountEmail (P1)
- OfflineStore also counts $HOME/.gemini/antigravity and .../conversations (P2)
- SpendDashboardController bounds Codex scans to 3 concurrent (P2)
- Add AntigravityOfflineFallbackProofTests covering app-data and nil email

* fix: remove broken proof test, keep P1/P2 fixes and shell proof

* fix(gatekeeper): update SpendDashboardController anchors after bounding Codex scans

* fix: revert bounded Codex scans (keep offline P1/P2), restore gatekeeper

* feat(spend): add tokscale-compatible local readers for Cursor and Antigravity (#3113)

* feat(spend): add tokscale-compatible local readers for Cursor and Antigravity

- Cursor: read ~/.config/tokscale/cursor-cache/usage*.csv (v1/v2/v3) with
  tokstyle column handling, cacheWrite = with-without, noon UTC for date-only,
  and CostUsageDailyReport aggregation.
  (Sources/CodexBarCore/Providers/Cursor/CursorLocalCSVReader.swift:1)

- Antigravity: read ~/.config/tokscale/antigravity-cache/sessions/*.jsonl
  (tokscale JSONL) and stub for ~/.gemini/antigravity-cli/*.db direct SQLite
  (ProtoReader to follow). Handles session_meta fallback and dedup.
  (Sources/CodexBarCore/Providers/Antigravity/AntigravityLocalReader.swift:1)

- CostUsageFetcher: local fallback before remote for Cursor (offline) and
  primary for Antigravity (quota-only before), with Provider-specific by
  design comments for gatekeeper.
  (Sources/CodexBarCore/CostUsageFetcher.swift:440)

- Antigravity descriptor: enable supportsTokenSnapshot for spend dashboard.
  (Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift:51)

Reproduced from /tmp/opencodex/src/adapters/cursor/protobuf-events.ts:218
and /tmp/tokscale/crates/tokscale-core/src/sessions/{cursor,antigravity_cli}.rs
Phase 1 of opencodex/tokscale plan, offline-first, no auth.

* test(readers): cover cursor csv schemas and antigravity cache fallback

* fix(test): include antigravity in cost capable dashboard sources

* fix(spend): honor CSV total tokens and add Antigravity Linux capability

* fix(test): honor cursor CSV total tokens column in aggregation

* fix(spend): repair 3113 tokscale readers P1s

- catch remote Cursor errors before falling back to local CSV
- recompute summaries after window filtering for Cursor and Antigravity
- keep Antigravity costs nil (unpriced) and deduplicate by responseId
- parse date-only CSV rows with UTC calendar
- thread fallback calendar through loaders

* fix(lint): repair 3113 build and format

- calendar before now in makeDailyReport
- implicit optional init
- wrap long lines and andOperator

* style: swiftformat wrap for 3113

* Fix 3113 provider gatekeeper anchors

* fix(spend): address 3113 review findings -- freshness, calendar, date-only, fixture model

- Preserve cache freshness: return nil when filtered window is empty instead of publishing established zero with now timestamp
- Pass pinned calendar into tokenSnapshot for Cursor/Antigravity local snapshots
- Keep date-only Cursor CSV rows in configured calendar's noon, not UTC noon
- Use clearly fictitious test model test-model-antigravity-a

* style: fix line length for fixture model

* fix(test): update gatekeeper anchors for CostUsageFetcher line drift

Allowlist lines 1339->1335 and 1695->1691 after 075eac7 freshness/calendar fixes

* test: repair gatekeeper anchors and regenerate parser hash on merged tree

---------

Co-authored-by: Yuxin-Qiao <2242016570@qq.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Olddonkey <olddonkeyblog@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Umut Keltek <35880258+umutkeltek@users.noreply.github.com>
Co-authored-by: kiranmagic7 <kiranmagic@proton.me>
Co-authored-by: Anupam Chugh <anupam.chugh@gmail.com>
Co-authored-by: anupamchugh <8416306+anupamchugh@users.noreply.github.com>
Co-authored-by: yicone <yicone@gmail.com>
Co-authored-by: Akshay Prabhu <12824090+akshayprabhu200@users.noreply.github.com>
steipete added a commit that referenced this pull request Aug 24, 2026
…ay reloads (#3136)

* Price OpenCodex usage once per entry and stop per-entry catalog/overlay reloads

The OpenCodex spend source (`~/.opencodex/usage.jsonl` → `OpenCodexUsageFanOut`
→ `OpenCodexUsageAggregator.snapshot`) re-resolved pricing context per entry:
`listPriceUSD` called `CostUsagePricing.codexCostUSD` without a pre-resolved
models.dev catalog, so every call went through `ModelsDevCache.load` →
`FileManager.attributesOfItem` (a stat plus an extended-attribute read), and
without a pre-resolved custom-pricing overlay, so every call also re-read the
overlay file location. Each windowed entry was priced three times (day, session
and hour accumulators), and day keys / hour buckets were recomputed through
Calendar per entry. On a 35k-entry log (all inside the 30-day window) that is
~100k stat+xattr syscalls and ~70k Calendar interval computations per refresh —
in the running app this was the 25–35 s CPU spike on every adaptive refresh
(sampled: `snapshotsBySubscription` → `attributesOfItem` → `getxattr`/`listxattr`).

Changes (snapshot output is byte-identical; verified against a reference
implementation in tests and by diffing CLI JSON on frozen inputs):
- Resolve the models.dev catalog and the custom-pricing overlay once per
  fan-out / snapshot and pass them down; price each windowed entry once and
  reuse the value for the day/session/hour/model merges. A missing catalog is
  substituted with an empty catalog so the degraded path never falls back to
  per-call loads.
- Memoize the local-day key and hour-bucket start per calendar interval using
  the calendar's own `[start, end)` intervals (DST-correct; no 86400/3600
  arithmetic).
- `ModelsDevCache.load` reads (mtime, size) via POSIX `stat` instead of
  `attributesOfItem` (which also reads xattrs); memo/invalidation semantics
  unchanged. This helps every caller repo-wide.

CodexParserHash is regenerated because ModelsDevPricing.swift is in the hashed
set; the previous hash (3c984b655688593f) is added to
compatiblePredecessorParserHashes since parsing and the persisted row shape are
unchanged, so existing cost-usage.sqlite stores are adopted on upgrade instead
of rebuilt.

Measured (release CodexBarCLI, isolated cache root, real 41.7 MB / ~35k-entry
usage.jsonl, same machine, `cost --provider codex --days 30`), OpenCodex path
isolated with identical frozen inputs:
- OpenCodex path alone (empty codex home, identical frozen inputs, CLI JSON
  output identical apart from `updatedAt`):
  cold  14.3 s real / 9.1 s user / 4.9 s sys / 193 G instructions
      →  2.6 s      / 2.4 s      / 0.1 s     /  40 G
  warm (store cache hit)  13.8 s / 8.3 s / 5.3 s / 166 G
      →  1.2 s / 1.1 s / 0.04 s / 13 G
- Full `cost --provider codex` CLI run on live data, steady state after the log
  grew (the app's per-refresh case): ~11 s → ~3.5 s real (7.3–9.2 s → 3.2 s user);
  cold 26 s → 14 s. Peak footprint unchanged (~430 MB cold/grown, ~120–140 MB
  warm).
Peak memory is unchanged — the remaining transient is the append-only log
re-parse (`OpenCodexUsageStore` identity = path|size|mtime), left for a
follow-up.

Tests: equivalence against an independent reference implementation (mixed
providers, estimated/reported/unreported/unsupported, custom overlay, duplicate
request IDs, DST transitions in America/Los_Angeles and America/Santiago),
metadata-read counting proving one catalog load per snapshot (zero with an
injected catalog), day/hour memo boundary cases, and ModelsDevCache memo
invalidation on size/mtime change after the stat switch.

Implemented by grok-4.6 (xhigh) via implementation-loop; reviewed hunk by hunk
plus an independent deep review; one iterate round.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: add changelog entry for #3136

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: comment the OpenCodex price-once context and memo semantics

Explain why the models.dev catalog and the custom-pricing overlay are resolved
once per snapshot / fan-out, why a missing catalog is substituted with an empty
one (so the degraded path never falls back to per-call ModelsDevCache.load),
the two-level overlay precedence in listPriceUSD, why the day-key memo cannot
disagree with CostUsageLocalDay.key, and that the metadata-read recorder is
task-local test-only instrumentation. Comments only; CodexParserHash is
regenerated because ModelsDevPricing.swift is in the hashed set (no shipped
hash is affected; the predecessor list is unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update appcast for 0.54.1

* chore: open 0.54.2 unreleased changelog section

* Stop re-merging the Codex plan-utilization history with itself on every refresh (#3141)

`materializeCodexPlanUtilizationHistoryIfNeeded` exists to fold legacy, opaque
and unscoped Codex plan-utilization buckets into the canonical account bucket.
Its scoped loop also appended the canonical bucket's own histories to
`historiesToMerge` — `matchesTargetContinuity` is true for
`rawKey == canonicalKey`, and only the removal of the old key was guarded — so
`guard !historiesToMerge.isEmpty` never fired once the canonical bucket had any
history, and the migration merge ran on every successful provider refresh and
every menu open, merging the history with itself.

That merge is quadratic: `updatedPlanUtilizationEntries` copied the whole entry
array per entry, scanned it linearly for the insertion point, and allocated the
same-hour slice. Measured with an optimized standalone reproduction over a real
three-month-old history (session 1909 entries, weekly 2239): 20.6 ms of MainActor
time per call, scaling ~3.9x per doubling. `planUtilizationMaxSamples` allows
17520 entries per series, so it would keep growing.

Two changes:

- Track whether a foreign source actually contributed and require that in the
  guard, so the canonical-only case returns without merging or rewriting
  anything. Every path where a legacy, opaque or unscoped bucket contributes is
  untouched; `legacyRawKeysToRemove` is populated only in branches that also set
  the flag, so no removal is skipped, and `providerBuckets.unscoped` is cleared
  only inside the branch that sets it.
- Make the merge itself near-linear: `updatedPlanUtilizationEntries` mutates the
  array in place and finds the insertion point with a binary search for the same
  strict upper bound (with a fast path for the common append), and
  `mergedPlanUtilizationHistories` accumulates per series and builds each history
  once.

The binary search assumes entries are sorted by `capturedAt`, which every
in-app producer guaranteed through `PlanUtilizationSeriesHistory`'s designated
initializer — except the synthesized `Codable` decoder, which assigned entries
verbatim from JSON. An explicit `init(from:)` now routes decoding through that
initializer, so an on-disk history written by an older build or edited by hand
cannot smuggle in an unsorted series.

The skipped self-merge also incidentally re-canonicalized per-hour peaks on
read; that repair belongs at load time, not on every refresh, and is not
reintroduced here. The visible effect is that at most one extra real observation
per affected hour is kept.

Tests: canonical-only history is returned untouched and enqueues no persistence
write (the history revision is unchanged); a genuine foreign merge matches an
explicit expected result across overlapping hours, out-of-order sources, distinct
series and retention trimming; the binary search's upper-bound contract is pinned
directly through a DEBUG shim over an array with a run of equal timestamps (a
lower bound would return a different index); and decoding a series whose JSON
entries are out of order yields a sorted series.

Implemented by grok-4.6 (xhigh) via implementation-loop; reviewed hunk by hunk
plus an independent deep review that confirmed both equivalences by differential
fuzzing (200k sorted cases with no mismatch) and found the decoder gap, fixed in
one iterate round. Gatekeeper line anchors for the touched file were re-verified
independently.

The DEBUG sortedness assertion is checked once per merged series rather than
once per inserted entry: a per-entry check is itself O(n) and reintroduced, in
debug builds, exactly the quadratic scan this insertion path removes (measured
over the real 4160-entry history: a legacy migration took ~1000 ms with the
per-entry assertion versus ~15 ms without it).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Fix Codex day cost blanked by trace-only priority turns (#3150)

Row ownership evidence compared the retained rows against the persisted
standard/priority split using the trace database's tier classification.
The persisted maps come from the rows' own pricingMode, so a turn the
trace reports as priority after its rows were persisted as standard read
as a row-ownership mismatch, the rows lost trust, and the day fell back
to the aggregate — which returns nil for long-context tiered models, so
the whole day's cost disappeared from the menu, the chart and the window
total.

Judge retention against both classifications and flag only a group that
matches neither. A wrongly retained row set still fails both, because the
persisted totals are canonical for the file and tier classification never
changes how many tokens the rows carry.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs: credit #3141 and #3150 changelog entries

* fix(qwen-cloud): restore Brave browser support in cookie import (#3148)

* fix(qwen-cloud): restore Brave browser support, narrowed to Chrome+Brave per AGENTS.md

Qwen Cloud's cookie import was restricted to [.chrome] only (commit
529cc6c 'Keep Qwen imports Chrome-only'). Brave users hit 'No Qwen
Cloud session cookies found in browsers' even when they had a valid
Qwen Cloud session in Brave, because their cookies were never probed.

This commit restores Brave in the import order, but follows
AGENTS.md L48 ('default Chrome-only when possible to avoid other
browser prompts; override via browser list when needed'). The override
is the minimum necessary: Chrome + Brave. The other Chromium browsers
(chromeBeta, edge, arc, firefox, safari) are deliberately omitted to
avoid unsolicited Keychain / browser-store access prompts on
automatic refreshes from browsers that don't carry a Qwen Cloud
session. Brave is kept because it shares the same Chromium Safe
Storage format as Chrome and is a common Qwen Cloud authentication
target.

Also adds docs/qwen-cloud-proof/README.md with the redacted end-to-end
proof captured against the live Qwen Cloud API from the user's Mac
after granting the modified binary access to 'Brave Safe Storage' in
macOS Keychain.

* fix(qwen-cloud): recovery message now names Brave alongside Chrome

ClawSweeper P2 follow-up on #3148: when the Brave cookie import
fails, QwenCloudSettingsError.missingCookie's recovery message
still told users to sign in to Chrome and grant access to Chrome
Safe Storage. Now that Brave is a supported source, the message
must name both browsers and their respective Safe Storage entries,
otherwise a Brave-only user would be told to use Chrome and never
find the working path.

Updates the error description to:
  'No Qwen Cloud session cookies found in browsers. Sign in to
   Qwen Cloud in Chrome or Brave, allow CodexBar to access the
   corresponding Safe Storage in Keychain Access (Chrome Safe
   Storage and/or Brave Safe Storage), or paste a manual Cookie
   header.'

Adds focused test coverage:
- missing cookie error mentions both supported browsers and their safe storage
- missing cookie error appends non-empty details
- missing cookie error omits empty details

35/35 Qwen Cloud tests pass (32 prior + 3 new).

* Fix OpenRouter completed-day activity query (#3138)

* Preserve unknown Grok period usage (#3159)

Co-authored-by: anupamchugh <8416306+anupamchugh@users.noreply.github.com>

* fix: report non-writable CLI path conflicts (#3153)

* fix: prefer successful CLI install status

* fix: keep CLI path conflicts visible

* fix: report non-writable CLI path conflicts

* docs: add CLI conflict behavior proof

* docs: add CLI install comparison screenshots

* Fix single-quota icon scaling (#3155)

* docs: credit #3138 #3148 #3153 #3155 #3159 changelog entries

* fix(spend): silent refresh and invalidation coverage (#3106)

* fix(spend): bucket calendar for all heatmap dates and full revision hash

- SpendActivityDateFormatting.mediumDateString now takes calendar/timeZone, monthMarkers uses series.calendar, tooltips and accessibility use bucket calendar.
- selectedDay renormalized on calendar change to keep toggle correct.
- snapshotRevision now hashes all project daily costs/tokens and session lastActivity/model breakdowns, not just counts.

Fixes ClawSweeper P2 for #3106.

* fix(gatekeeper): update anchors and add provider-specific design markers for spend dashboard

* Update provider gatekeeper anchors for v0.54 rebase

* fix(test): pin claude spend snapshot in observation test

* fix(test): seed pinned claude spend publication before first snapshot

* fix: resolve remaining conflict markers from gatekeeper rebase

* fix(lint): shorten Sakana test lines

* fix(spend): restore heatmap calendar property lost in rebase

* Extend spend publication test wait

* Restore spend gatekeeper anchors after rebase

* fix(spend): sync independent snapshot and bucket calendar normalization for 3106

- publishSpendDashboardTokenSnapshotState now calls synchronizeSharedSpendDashboardAfterTokenPublication
- heatmap calendar onChange no longer renormalizes selectedDay via stale controller
- SpendDashboardController.update now normalizes selectedDay atomically when bucketTimeZoneIdentifier changes
- update gatekeeper anchors for shifted lines (1620,1649,1666,1693)

* test(spend): cover independent snapshot sync for 3106

Exercise the direct independent publication path added at
UsageStore+SpendDashboardTokenCost.swift:181. The prior focused test
seeded Claude before observation and then used the regular Codex
publisher, which already syncs independently, so removing that line
would not fail. Add a post-start Claude snapshot via
_setSpendDashboardTokenSnapshotForTesting and assert the shared
dashboard debounced sync is scheduled and the publication inputs
update. Verified: swiftformat clean, swiftlint --strict clean,
swift test --filter SpendDashboardPublicationTests 18 tests passed.

* docs: add fresh-bundle proof for 3106

Add redacted menu-icon crop and dashboard snapshot from debug build
2798eec (swift build --target CodexBarCLI, .build/debug/CodexBarCLI
dashboard --pretty). The snapshot shows the shared spend controller
produces a dashboard with provider rows/windows, confirming the
independent-sync and calendar paths are live in the fresh binary.

* docs: add menu and Spend dashboard screenshots for 3106

Add redacted screenshots from fresh debug build 45ba984:
- 3106-menu-after-fix.png: menu bar extra open, showing provider rows
- 3106-settings-after-fix.png: Settings window (general)
- 3106-spend-dashboard-after-fix.png: Usage & Spend pane (usageSpend)
with heatmap and Overview, confirming the shared controller renders
in the fresh bundle.

* docs: remove screenshots for 3106 per request

Keep only the redacted CLI dashboard snapshot JSON as fresh-bundle
proof; screenshots are not needed.

* Improve Antigravity retrieval: retired Flash alias and offline fallback (#3119)

* fix(antigravity): allow OAuth errors to fallback to offline when local data exists

Fix P2 from Codex review on #3119: AntigravityOAuthFetchStrategy.shouldFallback
now checks hasOfflineData, so expired credentials do not block offline.

* fix(antigravity): unbind offline account, read app-data, bound scans + proof

- Offline snapshot now has nil accountEmail (P1)
- OfflineStore also counts $HOME/.gemini/antigravity and .../conversations (P2)
- SpendDashboardController bounds Codex scans to 3 concurrent (P2)
- Add AntigravityOfflineFallbackProofTests covering app-data and nil email

* fix: remove broken proof test, keep P1/P2 fixes and shell proof

* fix(gatekeeper): update SpendDashboardController anchors after bounding Codex scans

* fix: revert bounded Codex scans (keep offline P1/P2), restore gatekeeper

* feat(spend): add tokscale-compatible local readers for Cursor and Antigravity (#3113)

* feat(spend): add tokscale-compatible local readers for Cursor and Antigravity

- Cursor: read ~/.config/tokscale/cursor-cache/usage*.csv (v1/v2/v3) with
  tokstyle column handling, cacheWrite = with-without, noon UTC for date-only,
  and CostUsageDailyReport aggregation.
  (Sources/CodexBarCore/Providers/Cursor/CursorLocalCSVReader.swift:1)

- Antigravity: read ~/.config/tokscale/antigravity-cache/sessions/*.jsonl
  (tokscale JSONL) and stub for ~/.gemini/antigravity-cli/*.db direct SQLite
  (ProtoReader to follow). Handles session_meta fallback and dedup.
  (Sources/CodexBarCore/Providers/Antigravity/AntigravityLocalReader.swift:1)

- CostUsageFetcher: local fallback before remote for Cursor (offline) and
  primary for Antigravity (quota-only before), with Provider-specific by
  design comments for gatekeeper.
  (Sources/CodexBarCore/CostUsageFetcher.swift:440)

- Antigravity descriptor: enable supportsTokenSnapshot for spend dashboard.
  (Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift:51)

Reproduced from /tmp/opencodex/src/adapters/cursor/protobuf-events.ts:218
and /tmp/tokscale/crates/tokscale-core/src/sessions/{cursor,antigravity_cli}.rs
Phase 1 of opencodex/tokscale plan, offline-first, no auth.

* test(readers): cover cursor csv schemas and antigravity cache fallback

* fix(test): include antigravity in cost capable dashboard sources

* fix(spend): honor CSV total tokens and add Antigravity Linux capability

* fix(test): honor cursor CSV total tokens column in aggregation

* fix(spend): repair 3113 tokscale readers P1s

- catch remote Cursor errors before falling back to local CSV
- recompute summaries after window filtering for Cursor and Antigravity
- keep Antigravity costs nil (unpriced) and deduplicate by responseId
- parse date-only CSV rows with UTC calendar
- thread fallback calendar through loaders

* fix(lint): repair 3113 build and format

- calendar before now in makeDailyReport
- implicit optional init
- wrap long lines and andOperator

* style: swiftformat wrap for 3113

* Fix 3113 provider gatekeeper anchors

* fix(spend): address 3113 review findings -- freshness, calendar, date-only, fixture model

- Preserve cache freshness: return nil when filtered window is empty instead of publishing established zero with now timestamp
- Pass pinned calendar into tokenSnapshot for Cursor/Antigravity local snapshots
- Keep date-only Cursor CSV rows in configured calendar's noon, not UTC noon
- Use clearly fictitious test model test-model-antigravity-a

* style: fix line length for fixture model

* fix(test): update gatekeeper anchors for CostUsageFetcher line drift

Allowlist lines 1339->1335 and 1695->1691 after 075eac7 freshness/calendar fixes

* docs: credit #3106 #3113 #3119 changelog entries

* feat: add CHF display currency (#3149)

* test: fix currency fixtures after CHF became supported

* fix(codex): tokscale parity for token counts - max cached, clamped, reasoning split, stale (#3120)

* Align Codex token parsing with tokscale stale snapshots

- skip lightly regressed cumulative snapshots before interleaved latching
- take the maximum of cached and cache-read fields in all parsers
- cover cache field selection and out-of-order snapshot accounting with focused tests

* Refresh Codex parser hash

* Parse bare usage rows in Codex rollouts

* Fix stale reasoning and fallback cache parity

* Fix Codex fallback test fixture line handling

* fix(antigravity): repair offline fallback proof and oauth fallback; fix(spend): limit concurrent dashboard fetches to 3

* fix(lint): break long lines in offline fallback proof tests

* fix(tests): update gatekeeper anchors for spend dashboard concurrency limit

* fix(tests): correct gatekeeper line anchors for concurrent dashboard fix

* docs: update appcast for 0.54.1

* chore: open 0.54.2 unreleased changelog section

* Stop re-merging the Codex plan-utilization history with itself on every refresh (#3141)

`materializeCodexPlanUtilizationHistoryIfNeeded` exists to fold legacy, opaque
and unscoped Codex plan-utilization buckets into the canonical account bucket.
Its scoped loop also appended the canonical bucket's own histories to
`historiesToMerge` — `matchesTargetContinuity` is true for
`rawKey == canonicalKey`, and only the removal of the old key was guarded — so
`guard !historiesToMerge.isEmpty` never fired once the canonical bucket had any
history, and the migration merge ran on every successful provider refresh and
every menu open, merging the history with itself.

That merge is quadratic: `updatedPlanUtilizationEntries` copied the whole entry
array per entry, scanned it linearly for the insertion point, and allocated the
same-hour slice. Measured with an optimized standalone reproduction over a real
three-month-old history (session 1909 entries, weekly 2239): 20.6 ms of MainActor
time per call, scaling ~3.9x per doubling. `planUtilizationMaxSamples` allows
17520 entries per series, so it would keep growing.

Two changes:

- Track whether a foreign source actually contributed and require that in the
  guard, so the canonical-only case returns without merging or rewriting
  anything. Every path where a legacy, opaque or unscoped bucket contributes is
  untouched; `legacyRawKeysToRemove` is populated only in branches that also set
  the flag, so no removal is skipped, and `providerBuckets.unscoped` is cleared
  only inside the branch that sets it.
- Make the merge itself near-linear: `updatedPlanUtilizationEntries` mutates the
  array in place and finds the insertion point with a binary search for the same
  strict upper bound (with a fast path for the common append), and
  `mergedPlanUtilizationHistories` accumulates per series and builds each history
  once.

The binary search assumes entries are sorted by `capturedAt`, which every
in-app producer guaranteed through `PlanUtilizationSeriesHistory`'s designated
initializer — except the synthesized `Codable` decoder, which assigned entries
verbatim from JSON. An explicit `init(from:)` now routes decoding through that
initializer, so an on-disk history written by an older build or edited by hand
cannot smuggle in an unsorted series.

The skipped self-merge also incidentally re-canonicalized per-hour peaks on
read; that repair belongs at load time, not on every refresh, and is not
reintroduced here. The visible effect is that at most one extra real observation
per affected hour is kept.

Tests: canonical-only history is returned untouched and enqueues no persistence
write (the history revision is unchanged); a genuine foreign merge matches an
explicit expected result across overlapping hours, out-of-order sources, distinct
series and retention trimming; the binary search's upper-bound contract is pinned
directly through a DEBUG shim over an array with a run of equal timestamps (a
lower bound would return a different index); and decoding a series whose JSON
entries are out of order yields a sorted series.

Implemented by grok-4.6 (xhigh) via implementation-loop; reviewed hunk by hunk
plus an independent deep review that confirmed both equivalences by differential
fuzzing (200k sorted cases with no mismatch) and found the decoder gap, fixed in
one iterate round. Gatekeeper line anchors for the touched file were re-verified
independently.

The DEBUG sortedness assertion is checked once per merged series rather than
once per inserted entry: a per-entry check is itself O(n) and reintroduced, in
debug builds, exactly the quadratic scan this insertion path removes (measured
over the real 4160-entry history: a legacy migration took ~1000 ms with the
per-entry assertion versus ~15 ms without it).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Fix Codex day cost blanked by trace-only priority turns (#3150)

Row ownership evidence compared the retained rows against the persisted
standard/priority split using the trace database's tier classification.
The persisted maps come from the rows' own pricingMode, so a turn the
trace reports as priority after its rows were persisted as standard read
as a row-ownership mismatch, the rows lost trust, and the day fell back
to the aggregate — which returns nil for long-context tiered models, so
the whole day's cost disappeared from the menu, the chart and the window
total.

Judge retention against both classifications and flag only a group that
matches neither. A wrongly retained row set still fails both, because the
persisted totals are canonical for the file and tier classification never
changes how many tokens the rows carry.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs: credit #3141 and #3150 changelog entries

* fix(qwen-cloud): restore Brave browser support in cookie import (#3148)

* fix(qwen-cloud): restore Brave browser support, narrowed to Chrome+Brave per AGENTS.md

Qwen Cloud's cookie import was restricted to [.chrome] only (commit
529cc6c 'Keep Qwen imports Chrome-only'). Brave users hit 'No Qwen
Cloud session cookies found in browsers' even when they had a valid
Qwen Cloud session in Brave, because their cookies were never probed.

This commit restores Brave in the import order, but follows
AGENTS.md L48 ('default Chrome-only when possible to avoid other
browser prompts; override via browser list when needed'). The override
is the minimum necessary: Chrome + Brave. The other Chromium browsers
(chromeBeta, edge, arc, firefox, safari) are deliberately omitted to
avoid unsolicited Keychain / browser-store access prompts on
automatic refreshes from browsers that don't carry a Qwen Cloud
session. Brave is kept because it shares the same Chromium Safe
Storage format as Chrome and is a common Qwen Cloud authentication
target.

Also adds docs/qwen-cloud-proof/README.md with the redacted end-to-end
proof captured against the live Qwen Cloud API from the user's Mac
after granting the modified binary access to 'Brave Safe Storage' in
macOS Keychain.

* fix(qwen-cloud): recovery message now names Brave alongside Chrome

ClawSweeper P2 follow-up on #3148: when the Brave cookie import
fails, QwenCloudSettingsError.missingCookie's recovery message
still told users to sign in to Chrome and grant access to Chrome
Safe Storage. Now that Brave is a supported source, the message
must name both browsers and their respective Safe Storage entries,
otherwise a Brave-only user would be told to use Chrome and never
find the working path.

Updates the error description to:
  'No Qwen Cloud session cookies found in browsers. Sign in to
   Qwen Cloud in Chrome or Brave, allow CodexBar to access the
   corresponding Safe Storage in Keychain Access (Chrome Safe
   Storage and/or Brave Safe Storage), or paste a manual Cookie
   header.'

Adds focused test coverage:
- missing cookie error mentions both supported browsers and their safe storage
- missing cookie error appends non-empty details
- missing cookie error omits empty details

35/35 Qwen Cloud tests pass (32 prior + 3 new).

* Fix OpenRouter completed-day activity query (#3138)

* Preserve unknown Grok period usage (#3159)

Co-authored-by: anupamchugh <8416306+anupamchugh@users.noreply.github.com>

* fix: report non-writable CLI path conflicts (#3153)

* fix: prefer successful CLI install status

* fix: keep CLI path conflicts visible

* fix: report non-writable CLI path conflicts

* docs: add CLI conflict behavior proof

* docs: add CLI install comparison screenshots

* Fix single-quota icon scaling (#3155)

* docs: credit #3138 #3148 #3153 #3155 #3159 changelog entries

* fix(spend): silent refresh and invalidation coverage (#3106)

* fix(spend): bucket calendar for all heatmap dates and full revision hash

- SpendActivityDateFormatting.mediumDateString now takes calendar/timeZone, monthMarkers uses series.calendar, tooltips and accessibility use bucket calendar.
- selectedDay renormalized on calendar change to keep toggle correct.
- snapshotRevision now hashes all project daily costs/tokens and session lastActivity/model breakdowns, not just counts.

Fixes ClawSweeper P2 for #3106.

* fix(gatekeeper): update anchors and add provider-specific design markers for spend dashboard

* Update provider gatekeeper anchors for v0.54 rebase

* fix(test): pin claude spend snapshot in observation test

* fix(test): seed pinned claude spend publication before first snapshot

* fix: resolve remaining conflict markers from gatekeeper rebase

* fix(lint): shorten Sakana test lines

* fix(spend): restore heatmap calendar property lost in rebase

* Extend spend publication test wait

* Restore spend gatekeeper anchors after rebase

* fix(spend): sync independent snapshot and bucket calendar normalization for 3106

- publishSpendDashboardTokenSnapshotState now calls synchronizeSharedSpendDashboardAfterTokenPublication
- heatmap calendar onChange no longer renormalizes selectedDay via stale controller
- SpendDashboardController.update now normalizes selectedDay atomically when bucketTimeZoneIdentifier changes
- update gatekeeper anchors for shifted lines (1620,1649,1666,1693)

* test(spend): cover independent snapshot sync for 3106

Exercise the direct independent publication path added at
UsageStore+SpendDashboardTokenCost.swift:181. The prior focused test
seeded Claude before observation and then used the regular Codex
publisher, which already syncs independently, so removing that line
would not fail. Add a post-start Claude snapshot via
_setSpendDashboardTokenSnapshotForTesting and assert the shared
dashboard debounced sync is scheduled and the publication inputs
update. Verified: swiftformat clean, swiftlint --strict clean,
swift test --filter SpendDashboardPublicationTests 18 tests passed.

* docs: add fresh-bundle proof for 3106

Add redacted menu-icon crop and dashboard snapshot from debug build
2798eec (swift build --target CodexBarCLI, .build/debug/CodexBarCLI
dashboard --pretty). The snapshot shows the shared spend controller
produces a dashboard with provider rows/windows, confirming the
independent-sync and calendar paths are live in the fresh binary.

* docs: add menu and Spend dashboard screenshots for 3106

Add redacted screenshots from fresh debug build 45ba984:
- 3106-menu-after-fix.png: menu bar extra open, showing provider rows
- 3106-settings-after-fix.png: Settings window (general)
- 3106-spend-dashboard-after-fix.png: Usage & Spend pane (usageSpend)
with heatmap and Overview, confirming the shared controller renders
in the fresh bundle.

* docs: remove screenshots for 3106 per request

Keep only the redacted CLI dashboard snapshot JSON as fresh-bundle
proof; screenshots are not needed.

* Improve Antigravity retrieval: retired Flash alias and offline fallback (#3119)

* fix(antigravity): allow OAuth errors to fallback to offline when local data exists

Fix P2 from Codex review on #3119: AntigravityOAuthFetchStrategy.shouldFallback
now checks hasOfflineData, so expired credentials do not block offline.

* fix(antigravity): unbind offline account, read app-data, bound scans + proof

- Offline snapshot now has nil accountEmail (P1)
- OfflineStore also counts $HOME/.gemini/antigravity and .../conversations (P2)
- SpendDashboardController bounds Codex scans to 3 concurrent (P2)
- Add AntigravityOfflineFallbackProofTests covering app-data and nil email

* fix: remove broken proof test, keep P1/P2 fixes and shell proof

* fix(gatekeeper): update SpendDashboardController anchors after bounding Codex scans

* fix: revert bounded Codex scans (keep offline P1/P2), restore gatekeeper

* feat(spend): add tokscale-compatible local readers for Cursor and Antigravity (#3113)

* feat(spend): add tokscale-compatible local readers for Cursor and Antigravity

- Cursor: read ~/.config/tokscale/cursor-cache/usage*.csv (v1/v2/v3) with
  tokstyle column handling, cacheWrite = with-without, noon UTC for date-only,
  and CostUsageDailyReport aggregation.
  (Sources/CodexBarCore/Providers/Cursor/CursorLocalCSVReader.swift:1)

- Antigravity: read ~/.config/tokscale/antigravity-cache/sessions/*.jsonl
  (tokscale JSONL) and stub for ~/.gemini/antigravity-cli/*.db direct SQLite
  (ProtoReader to follow). Handles session_meta fallback and dedup.
  (Sources/CodexBarCore/Providers/Antigravity/AntigravityLocalReader.swift:1)

- CostUsageFetcher: local fallback before remote for Cursor (offline) and
  primary for Antigravity (quota-only before), with Provider-specific by
  design comments for gatekeeper.
  (Sources/CodexBarCore/CostUsageFetcher.swift:440)

- Antigravity descriptor: enable supportsTokenSnapshot for spend dashboard.
  (Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift:51)

Reproduced from /tmp/opencodex/src/adapters/cursor/protobuf-events.ts:218
and /tmp/tokscale/crates/tokscale-core/src/sessions/{cursor,antigravity_cli}.rs
Phase 1 of opencodex/tokscale plan, offline-first, no auth.

* test(readers): cover cursor csv schemas and antigravity cache fallback

* fix(test): include antigravity in cost capable dashboard sources

* fix(spend): honor CSV total tokens and add Antigravity Linux capability

* fix(test): honor cursor CSV total tokens column in aggregation

* fix(spend): repair 3113 tokscale readers P1s

- catch remote Cursor errors before falling back to local CSV
- recompute summaries after window filtering for Cursor and Antigravity
- keep Antigravity costs nil (unpriced) and deduplicate by responseId
- parse date-only CSV rows with UTC calendar
- thread fallback calendar through loaders

* fix(lint): repair 3113 build and format

- calendar before now in makeDailyReport
- implicit optional init
- wrap long lines and andOperator

* style: swiftformat wrap for 3113

* Fix 3113 provider gatekeeper anchors

* fix(spend): address 3113 review findings -- freshness, calendar, date-only, fixture model

- Preserve cache freshness: return nil when filtered window is empty instead of publishing established zero with now timestamp
- Pass pinned calendar into tokenSnapshot for Cursor/Antigravity local snapshots
- Keep date-only Cursor CSV rows in configured calendar's noon, not UTC noon
- Use clearly fictitious test model test-model-antigravity-a

* style: fix line length for fixture model

* fix(test): update gatekeeper anchors for CostUsageFetcher line drift

Allowlist lines 1339->1335 and 1695->1691 after 075eac7 freshness/calendar fixes

* test: repair gatekeeper anchors and regenerate parser hash on merged tree

---------

Co-authored-by: Yuxin-Qiao <2242016570@qq.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Olddonkey <olddonkeyblog@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Umut Keltek <35880258+umutkeltek@users.noreply.github.com>
Co-authored-by: kiranmagic7 <kiranmagic@proton.me>
Co-authored-by: Anupam Chugh <anupam.chugh@gmail.com>
Co-authored-by: anupamchugh <8416306+anupamchugh@users.noreply.github.com>
Co-authored-by: yicone <yicone@gmail.com>
Co-authored-by: Akshay Prabhu <12824090+akshayprabhu200@users.noreply.github.com>

* docs: credit #3120 changelog entry

* test: fix remaining CHF unconvertible fixtures after #3149

* fix: detect ChatGPT-hosted Codex activity (#3163)

* fix(antigravity): reuse signed-in agy for quota refresh (#3161)

* docs: credit #3161 and #3163 changelog entries

* Fix Claude web cookie refresh (#3162)

* docs: credit #3162 changelog entry

* chore: regenerate parser hash on merged tree

* test: include 0.54.2 parity hash in predecessor list

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Umut Keltek <35880258+umutkeltek@users.noreply.github.com>
Co-authored-by: kiranmagic7 <kiranmagic@proton.me>
Co-authored-by: Anupam Chugh <anupam.chugh@gmail.com>
Co-authored-by: anupamchugh <8416306+anupamchugh@users.noreply.github.com>
Co-authored-by: yicone <yicone@gmail.com>
Co-authored-by: Akshay Prabhu <12824090+akshayprabhu200@users.noreply.github.com>
Co-authored-by: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>
Co-authored-by: Yuxin-Qiao <2242016570@qq.com>
Co-authored-by: Zihao Qi <35388022+Zihao-Qi@users.noreply.github.com>
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.

2 participants