Skip to content

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

Merged
steipete merged 12 commits into
steipete:mainfrom
Yuxin-Qiao:feat/spend-tokscale-antigravity-cursor
Aug 23, 2026
Merged

feat(spend): add tokscale-compatible local readers for Cursor and Antigravity#3113
steipete merged 12 commits into
steipete:mainfrom
Yuxin-Qiao:feat/spend-tokscale-antigravity-cursor

Conversation

@Yuxin-Qiao

@Yuxin-Qiao Yuxin-Qiao commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Phase 1 of the opencodex/tokscale plan, offline-first, no auth.

Cursor

  • Reads ~/.config/tokscale/cursor-cache/usage*.csv (v1/v2/v3) exactly as tokscale/crates/tokscale-core/src/sessions/cursor.rs:182 – column indices, cacheWrite = with - without:252, parseCost:132, noon UTC for date-only 330, quote-aware parseCSVLine:278.
  • CursorLocalCSVReader.swift:1 aggregates to CostUsageDailyReport and CostUsageFetcher.loadCursorLocalSnapshot is tried after the remote dashboard API, so offline or cookie-expired still shows spend.

Antigravity

  • Reads ~/.config/tokscale/antigravity-cache/sessions/*.jsonl (tokscale antigravity sync output) as type: usage {modelId, input, output, cacheRead, cacheWrite, timestamp, responseId} sessions/antigravity.rs:48, with session_meta fallback.
  • AntigravityLocalReader.swift:1 also stubs direct SQLite ~/.gemini/antigravity-cli/conversations/*.db via ProtoReader field numbers antigravity_cli.rs:21 for Phase 2.
  • AntigravityProviderDescriptor:51 now supportsTokenSnapshot:true so the spend dashboard includes it.

Fetcher

  • CostUsageFetcher.swift:440 tries local cache before falling through to the scanner, with // Provider-specific by design for gatekeeper. Mirrors opencodex's OcxUsage canonical and tokscale's offline-first.

Reproduced

  • git clone --depth1 to /tmp/opencodex and /tmp/tokscale, inspected src/adapters/cursor/protobuf-events.ts:218, sessions/{cursor,antigravity_cli}.rs, pricing/aliases.rs:43.

Verified: swift build --target CodexBarCore ok, swiftformat done, gatekeeper for new provider == .cursor/.antigravity clusters to be added in follow-up.


Branch rebuilt from main (heads 1d298810362341aca1)

The previously pushed head accidentally deleted the application tree (only Scripts/ survived, ~769k deletions). The branch is now rebuilt off current main with the full feature commit (cherry-picked intact from local history, applies clean) plus:

  • test(readers): CursorAntigravityLocalReaderTests covering v1/v2/v3 CSV schemas (column indices, cacheWrite = with − without, date-only noon), day aggregation + summary, and the Antigravity JSONL cache with session_meta model fallback (5 tests).
  • Gatekeeper: re-pointed drifted CostUsageFetcher anchors and added .antigravity to the cost-capable/snapshot provider sets.

Real behavior proof

$ swift test --filter CursorAntigravityLocalReaderTests
✔ Test run with 5 tests in 1 suite passed
$ swift test --filter ProviderArchitectureGatekeeperTests
✔ Test run with 38 tests in 1 suite passed
$ swift test --filter CostUsageFetcherTests
✔ Test run with 17 tests in 1 suite passed
$ swift test --filter CursorUsageEventsFetcherTests
✔ Test run with 29 tests in 1 suite passed
$ swift test --filter SpendDashboardModelTests
✔ Test run with 45 tests in 1 suite passed
$ swiftformat Sources Tests && swiftlint --strict
Done linting! Found 0 violations, 0 serious in 1975 files.
$ git ls-tree --name-only HEAD | wc -l
31

Real behavior proof (after aa464e6)

Remote fallback (P1)

$ swift test --filter CursorAntigravityLocalReaderTests
✔ Test "catches remote Cursor error and falls back to local CSV" passed

Expired cursor cookie now catches loadRemoteTokenSnapshot throw and returns loadCursorLocalSnapshot if present; antigravity offline path also covered.

Window filtering (P1)

$ swift test --filter CursorAntigravityLocalReaderTests
✔ Test "recomputes summary after window filtering" passed

30-day view now recomputes summary from filtered (was full.summary with all-time totals).

Unpriced Antigravity (P1)

$ swift test --filter CursorAntigravityLocalReaderTests
✔ Test "keeps unpriced Antigravity costs nil" passed

AntigravityLocalReader now leaves costUSD/totalCostUSD nil and costProvenance .unknown (was 0 + listPriceEstimate → $0).

Dedup / UTC / Calendar (P1/P2)

$ swift test --filter CursorAntigravityLocalReaderTests
✔ Test "deduplicates by responseId" passed
✔ Test "parses date-only CSV in UTC" passed
✔ Test "uses configured calendar for local snapshots" passed

Real behavior proof — PR #3113 (terminal only, no screenshots)

Synthetic TOKSCALE_CONFIG_DIR caches were created locally (no real cookies, no secrets) and read through the verbatim CursorLocalCSVReader / AntigravityLocalReader at 902c7401a (files Sources/CodexBarCore/Providers/Cursor/CursorLocalCSVReader.swift and Sources/CodexBarCore/Providers/Antigravity/AntigravityLocalReader.swift).

Covers:

  • Cursor CSV v1/v2/v3 schemas (column indices, cacheWrite = with - without, noon UTC for date-only, quote-aware parseCSVLine)
  • Total Tokens column honored as authoritative
  • Antigravity JSONL type: usage with session_meta model fallback, responseId dedup, and unpriced (costUSD nil) handling
$ TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof swift run proof (via swiftc-compiled verbatim readers)
# Synthetic tokscale caches created under /tmp/tokscale-proof (no secrets, no real cookies)
antigravity-cache
cursor-cache

/tmp/tokscale-proof/antigravity-cache:
sessions

/tmp/tokscale-proof/antigravity-cache/sessions:
demo.jsonl

/tmp/tokscale-proof/cursor-cache:
usage.csv
usage_2.csv
usage_3.csv
--- cursor-cache/usage.csv (v1) ---
Date,Model,Input (w/ Cache Write),Input (w/o Cache Write),Cache Read,Output Tokens,Total Tokens,Cost,Cost to you
2026-08-20T10:00:00.000Z,cursor-test-model-a,1200,800,400,300,1500,0.012,0
2026-08-20T12:00:00.000Z,cursor-test-model-a,600,500,100,100,700,0.004,0
2026-08-21T09:00:00.000Z,cursor-test-model-b,0,0,0,40,40,0.001,0
--- cursor-cache/usage_2.csv (v2 with Kind) ---
Date,Kind,Model,Max Mode,Input (w/ Cache Write),Input (w/o Cache Write),Cache Read,Output Tokens,Total Tokens,Cost
2026-08-20T11:00:00Z,chat,cursor-test-model-c,on,500,500,0,50,550,0.001
--- cursor-cache/usage_3.csv (v3 with Cloud Agent ID + date-only noon) ---
Date,Cloud Agent ID,Automation ID,Kind,Model,Max Mode,Input (w/ Cache Write),Input (w/o Cache Write),Cache Read,Output Tokens,Total Tokens,Cost
2026-08-22,agent-123,,agent,cursor-test-model-d,off,900,700,200,120,1020,0.05
--- antigravity-cache/sessions/demo.jsonl (session_meta fallback + dedup) ---
{"type":"session_meta","modelId":"test-gemini-model"}
{"type": "usage", "modelId": "test-gemini-model", "input": 100, "output": 30, "cacheRead": 50, "cacheWrite": 10, "timestamp": 1787457600000, "responseId": "rid-001"}
{"type": "usage", "input": 40, "output": 10, "cacheRead": 0, "cacheWrite": 0, "timestamp": 1787461200000, "responseId": "rid-002"}
{"type": "usage", "modelId": "test-gemini-model", "input": 100, "output": 30, "cacheRead": 50, "cacheWrite": 10, "timestamp": 1787457600000, "responseId": "rid-001"}
--- compile verbatim readers (CursorLocalCSVReader.swift + AntigravityLocalReader.swift @ 902c7401a) ---
/tmp/proof-3113-main.swift:90:17: warning: variable 'e' was never mutated; consider changing to 'let' constant [#VariableNeverMutated]
 88 |             let key = CostUsageLocalDay.key(from: row.date, calendar: calendar)
 89 |             let existing = byDay[key]
 90 |             var e = existing ?? CostUsageDailyReport.Entry(date: key, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, totalTokens: 0, requestCount: 0, costUSD: 0, modelsUsed: nil, modelBreakdowns: [])
    |                 `- warning: variable 'e' was never mutated; consider changing to 'let' constant [#VariableNeverMutated]
 91 |             let tot = row.totalTokens ?? (row.input + row.output + row.cacheRead + row.cacheWrite)
 92 |             var bds = e.modelBreakdowns ?? []

/tmp/proof-3113-main.swift:171:20: warning: variable 'ex' was never mutated; consider changing to 'let' constant [#VariableNeverMutated]
169 |         var merged: [String: CostUsageDailyReport.Entry] = [:]
170 |         for e in self.parseJSONLCache(calendar: calendar) {
171 |             if var ex = merged[e.date] {
    |                    `- warning: variable 'ex' was never mutated; consider changing to 'let' constant [#VariableNeverMutated]
172 |                 let mergedCost: Double? = { if ex.costUSD == nil, e.costUSD == nil { return nil }; return (ex.costUSD ?? 0) + (e.costUSD ?? 0) }()
173 |                 let ne = CostUsageDailyReport.Entry(date: ex.date, inputTokens: (ex.inputTokens ?? 0) + (e.inputTokens ?? 0), outputTokens: (ex.outputTokens ?? 0) + (e.outputTokens ?? 0), cacheReadTokens: (ex.cacheReadTokens ?? 0) + (e.cacheReadTokens ?? 0), cacheCreationTokens: (ex.cacheCreationTokens ?? 0) + (e.cacheCreationTokens ?? 0), reasoningTokens: (ex.reasoningTokens ?? 0) + (e.reasoningTokens ?? 0), totalTokens: (ex.totalTokens ?? 0) + (e.totalTokens ?? 0), requestCount: (ex.requestCount ?? 0) + (e.requestCount ?? 0), costUSD: mergedCost, modelsUsed: nil, modelBreakdowns: self.mergeBreakdowns(ex.modelBreakdowns, e.modelBreakdowns))

[#VariableNeverMutated]: <https://docs.swift.org/compiler/documentation/diagnostics/variable-never-mutated>
--- run with TOKSCALE_CONFIG_DIR ---
TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof
--- Cursor CSV ---
found cursor CSVs: ["usage.csv", "usage_2.csv", "usage_3.csv"]
  usage.csv: 3 rows
    2026-08-20 10:00:00 +0000 model=cursor-test-model-a input=800 read=400 write=400 out=300 totalTokens=Optional(1500) cost=0.012
    2026-08-20 12:00:00 +0000 model=cursor-test-model-a input=500 read=100 write=100 out=100 totalTokens=Optional(700) cost=0.004
    2026-08-21 09:00:00 +0000 model=cursor-test-model-b input=0 read=0 write=0 out=40 totalTokens=Optional(40) cost=0.001
  usage_2.csv: 1 rows
    2026-08-20 11:00:00 +0000 model=cursor-test-model-c input=500 read=0 write=0 out=50 totalTokens=Optional(550) cost=0.001
  usage_3.csv: 1 rows
    2026-08-22 12:00:00 +0000 model=cursor-test-model-d input=700 read=200 write=200 out=120 totalTokens=Optional(1020) cost=0.05
cursor daily entries: 3
  date=2026-08-20 input=1800 read=500 write=500 out=450 total=2750 req=3 cost=0.017
    breakdown model=cursor-test-model-a cost=0.016 tokens=2200 req=2
    breakdown model=cursor-test-model-c cost=0.001 tokens=550 req=1
  date=2026-08-21 input=0 read=0 write=0 out=40 total=40 req=1 cost=0.001
    breakdown model=cursor-test-model-b cost=0.001 tokens=40 req=1
  date=2026-08-22 input=700 read=200 write=200 out=120 total=1020 req=1 cost=0.05
    breakdown model=cursor-test-model-d cost=0.05 tokens=1020 req=1
cursor summary totalTokens=3810 totalCostUSD=0.068
--- Antigravity JSONL ---
found antigravity jsonl: ["demo.jsonl"]
antigravity entries (deduped by responseId): 1
  date=2026-08-23 input=140 out=40 read=50 write=10 total=240 req=2 cost=nil
    breakdown model=test-gemini-model tokens=240 req=2
antigravity daily report entries: 1
  date=2026-08-23 total=240 req=2
antigravity summary totalTokens=240 totalCostUSD=nil (expected nil because unpriced)
--- Assertions ---
All assertions passed — readers are tokscale-compatible and correctly aggregate with dedup, cacheWrite derivation, and date-only noon handling.

Prepared from: TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof with synthetic CSV/JSONL, compiled via swiftc -module-cache-path /tmp/clang-cache /tmp/proof-3113-main.swift -o /tmp/proof-3113-bin and executed as TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof /tmp/proof-3113-bin.


Fix 075eac7 — address 4 review blockers + production fetcher proof

Fixes pushed at 075eac7:

  • CostUsageFetcher.swift:1190 Preserve freshness: loadCursorLocalSnapshot / loadAntigravityLocalSnapshot now guard !filtered.isEmpty else { return nil } instead of publishing an established zero with now when the window has no rows (stale cache no longer looks fresh)
  • CostUsageFetcher.swift:1210 Pass pinned calendar: tokenSnapshot(..., calendar: cal, ...) so the current-day session lookup uses the configured calendar, not Calendar.current
  • CursorLocalCSVReader.swift:184 Keep date-only rows in configured day: parse yyyy-MM-dd as year/month/day in UTC, then reconstruct at 12:00 in the passed calendar (UTC+13/14 no longer shifts to next local day)
  • CursorAntigravityLocalReaderTests.swift:111 Use fictitious fixture test-model-antigravity-a instead of gemini-3.7-flash

Production fetcher proof — CostUsageFetcher.loadCursorLocalSnapshot / loadAntigravityLocalSnapshot (fixed 075eac7, synthetic TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof):

=== CostUsageFetcher production path (fixed 075eac7) ===
TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof
now=2026-08-23 04:00:00 +0000 calendar=gregorian tz=Asia/Shanghai
CURSOR_SNAPSHOT via CostUsageFetcher.loadCursorLocalSnapshot
  daily.count=3 historyDays=30 last30DaysTokens=3810 last30DaysCostUSD=Optional(0.068) historyCoverageIsEstablished=true updatedAt=2026-08-23 04:00:00 +0000
  date=2026-08-20 total=2750 req=3 cost=Optional(0.017)
  date=2026-08-21 total=40 req=1 cost=Optional(0.001)
  date=2026-08-22 total=1020 req=1 cost=Optional(0.05)
ANTIGRAVITY_SNAPSHOT via CostUsageFetcher.loadAntigravityLocalSnapshot
  daily.count=1 last30DaysTokens=240 cost=nil
  date=2026-08-23 total=240 req=2
loadCursorLocalSnapshot: filtered empty -> nil (freshness fix, not stale zero)
STALE_CACHE test (now=2026-09-30, 7d window, no rows in window): nil (correct, not stale zero)
CALENDAR test UTC+14: daily=["2026-08-21", "2026-08-22"] (date-only 2026-08-22 should stay on 2026-08-22, not 2026-08-23)
All production fetcher assertions passed

Fix 60fdee7 — lint

  • Shorten fixture to test-model-a to fix line_length violation at CursorAntigravityLocalReaderTests.swift:112 (was 126 chars, now <120). swiftlint --strict now passes (0 violations). Previous proofs' test-model-a / test-gemini-model are both clearly fictitious and acceptable; updated synthetic cache now uses test-model-a consistently, see combined proof above (reader + fetcher).

Head is now 60fdee7, CI re-running.

@clawsweeper

clawsweeper Bot commented Aug 21, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

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

@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: 9da2dec42f

ℹ️ 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".

Comment on lines +439 to +443
// Provider-specific by design: Cursor local CSV is an offline fallback for the remote dashboard API.
if provider == .cursor, let local = await self.loadCursorLocalSnapshot(
now: now, historyDays: clampedHistoryDays)
{
return local

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fall back to the Cursor cache when the remote fetch throws

On macOS, an offline request, expired session, or missing Cursor cookie makes the preceding try await loadRemoteTokenSnapshot throw rather than return nil, so execution never reaches this advertised local fallback even when valid tokscale CSV files exist. Catch the remote Cursor failure, try the local snapshot, and rethrow only if no usable local data is available.

Useful? React with 👍 / 👎.

Comment on lines +1150 to +1151
let filtered = full.data.filter { $0.date >= sinceKey && $0.date <= nowKey }
let daily = CostUsageDailyReport(data: filtered, summary: full.summary)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recompute totals after filtering local history

When the local cache contains rows older than the requested window, filtered correctly limits daily.data, but retaining full.summary makes tokenSnapshot prefer the all-time token and cost totals over those filtered entries. Consequently a 30-day snapshot can report months of spend while exposing only 30 days of rows; recompute the summary from filtered here and in the mirrored Antigravity loader.

Useful? React with 👍 / 👎.

Comment on lines +84 to +86
costUSD: 0, modelsUsed: nil,
modelBreakdowns: [CostUsageDailyReport.ModelBreakdown(
modelName: modelId, costUSD: 0, totalTokens: total, requestCount: 1)])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep unknown Antigravity costs nil until they are priced

For every nonempty Antigravity cache, each usage entry and model breakdown receives an explicit costUSD: 0, the summary therefore totals zero, and the fetcher labels the result as a list-price estimate. Users with real token usage will consequently see $0 spend rather than an unknown value or an actual model-price estimate; apply the pricing catalog before publishing the snapshot, or leave costs nil until pricing is available.

Useful? React with 👍 / 👎.

Comment on lines +52 to +54
supportsTokenCost: true,
noDataMessage: { "Antigravity cost summary is not supported." },
supportsTokenSnapshot: true),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include Antigravity in the capability registry test

Enabling supportsTokenSnapshot here makes ProviderArchitectureGatekeeperTests.small provider capabilities preserve legacy registries fail on both macOS and Linux because its expected provider sets still omit .antigravity. Update that registry assertion alongside this capability change so the test suite can pass and the new behavior is covered.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

Comment on lines +177 to +179
var c = calendar.dateComponents([.year, .month, .day], from: d)
c.hour = 12; c.minute = 0; c.second = 0; c.timeZone = TimeZone(secondsFromGMT: 0)
return calendar.date(from: c) ?? d

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 date-only CSV days west of UTC

For a date-only value such as 2026-08-21, the formatter first creates midnight UTC, but extracting components with a negative-offset calendar produces August 20 before the code moves it to noon UTC. Cursor rows are therefore assigned to the previous day for users west of UTC; extract the year/month/day using a UTC calendar or parse the components directly from the date string.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. 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. labels Aug 21, 2026
@clawsweeper

clawsweeper Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codex review: needs changes before merge. Reviewed August 23, 2026, 9:18 AM ET / 13:18 UTC.

ClawSweeper review

What this changes

Adds local tokscale CSV and JSONL readers for Cursor and Antigravity and feeds their usage into CodexBar’s spend dashboard.

Merge readiness

⚠️ Needs maintainer review before merge - 3 items remain

Keep open: the current head still assigns date-only Cursor cache rows using the process calendar rather than CodexBar’s configured reporting calendar, so spend can appear on the wrong day.

Priority: P2
Reviewed head: 54a99a3c1fdd9f7953741d4ebf287f2e48c58f3f

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) The local-cache proof is strong, but the confirmed reporting-calendar defect should be repaired before merge.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The PR body provides redacted terminal output from synthetic tokscale caches through the readers and production fetcher; it is sufficient for the demonstrated parser paths, though it does not cover the remaining configured-calendar defect.
Patch quality 🦐 gold shrimp (3/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The PR body provides redacted terminal output from synthetic tokscale caches through the readers and production fetcher; it is sufficient for the demonstrated parser paths, though it does not cover the remaining configured-calendar defect.
Evidence reviewed 5 items Calendar propagation defect: The loader computes and passes fallbackCalendar, but reads each CSV through parseFile(at:) without that calendar; date-only parsing therefore uses its default Calendar.current before reporting groups with the configured calendar.
Date-only parser contract: CursorLocalCSVReader.parseFile defaults its calendar to Calendar.current, and its date-only branch constructs noon in the supplied calendar; the loader must supply the configured calendar for the intended behavior.
Current main does not already provide this feature: Current main still returns only the remote snapshot before scanner fallback and its Antigravity descriptor has token-cost support disabled, so the central local-reader feature remains necessary.
Findings 1 actionable finding [P2] Pass the pinned calendar to Cursor CSV parsing
Security None None.

Live Verification

Command: swift test --filter CursorAntigravityLocalReaderTests

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’s cost-usage fetcher obtains provider usage from remote APIs and local files, normalizes it into daily reports, and supplies those reports to the spend dashboard. This PR adds tokscale cache files as local sources for Cursor and Antigravity.

flowchart LR
A[Cursor CSV cache] --> C[Local usage readers]
B[Antigravity JSONL cache] --> C
D[Remote provider fetch] --> E[Cost usage fetcher]
C --> E
E --> F[Daily usage report]
F --> G[Spend dashboard]
Loading

Before merge

  • Pass the pinned calendar to Cursor CSV parsing (P2) - fallbackCalendar is used for filtering and snapshot lookup, but this call uses parseFile’s Calendar.current default. Date-only rows are constructed in that parser calendar, so a configured calendar in another time zone can move a row to a neighboring day; pass calendar here and add a cross-time-zone regression test.
  • Resolve merge risk (P1) - Existing users with a configured reporting time zone different from the process time zone can see date-only Cursor records shifted into the wrong daily bucket or outside the selected history window.
  • Complete next step (P2) - A one-line calendar propagation repair plus a focused regression test can resolve the remaining merge blocker without changing feature direction.

Findings

  • [P2] Pass the pinned calendar to Cursor CSV parsing — Sources/CodexBarCore/CostUsageFetcher.swift:1162
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production versus test LOC production +519/-8, tests +154/-13 The feature adds two parsers and fetcher integration; focused regression coverage should protect their reporting semantics.

Merge-risk options

Maintainer options:

  1. Preserve configured-day grouping (recommended)
    Pass the configured calendar into Cursor CSV parsing and prove date-only rows remain on their intended day in a non-current time zone.
Copy recommended automerge instruction
@clawsweeper automerge

Special instructions:
Pass the supplied calendar to CursorLocalCSVReader.parseFile in loadCursorLocalSnapshot and add a non-current-time-zone regression test for a date-only CSV row.

Technical review

Best possible solution:

Pass the resolved reporting calendar into Cursor CSV parsing and add a cross-time-zone date-only regression test before merging.

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

Yes—source reproducible: invoke the package-level token-snapshot path with a configured calendar different from Calendar.current and a date-only Cursor CSV row; the loader omits the configured calendar at parsing.

Is this the best way to solve the issue?

No—the current implementation is not the best final form because it preserves configured-calendar grouping only after parsing; parsing must receive that same calendar.

Full review comments:

  • [P2] Pass the pinned calendar to Cursor CSV parsing — Sources/CodexBarCore/CostUsageFetcher.swift:1162
    fallbackCalendar is used for filtering and snapshot lookup, but this call uses parseFile’s Calendar.current default. Date-only rows are constructed in that parser calendar, so a configured calendar in another time zone can move a row to a neighboring day; pass calendar here and add a cross-time-zone regression test.
    Confidence: 0.99

Overall correctness: patch is incorrect
Overall confidence: 0.99

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 4b14ed9c57d3.

Labels

Label justifications:

  • P2: The remaining defect affects daily spend attribution but has a narrow, well-defined repair.
  • merge-risk: 🚨 compatibility: Merging as-is can change existing users’ date-only Cursor usage totals and selected-window results when configured and process calendars differ.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (terminal): The PR body provides redacted terminal output from synthetic tokscale caches through the readers and production fetcher; it is sufficient for the demonstrated parser paths, though it does not cover the remaining configured-calendar defect.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides redacted terminal output from synthetic tokscale caches through the readers and production fetcher; it is sufficient for the demonstrated parser paths, though it does not cover the remaining configured-calendar defect.

Evidence

Acceptance criteria:

  • [P1] swift test --filter CursorAntigravityLocalReaderTests.
  • [P1] swift test --filter CostUsageFetcherTests.
  • [P1] make check.

What I checked:

Likely related people:

  • Peter Steinberger: Introduced the current spend-reporting series and performed the descriptor and provider-architecture refactors that this change extends. (role: spend-reporting and provider-architecture contributor; confidence: high; commits: bbb5cd73af04, 973ec704de4d, fb66e63e8719; files: Sources/CodexBarCore/CostUsageFetcher.swift, Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift)
  • Yuxin Qiao: Recent main-history commits maintain Antigravity readiness and spend timezone/provenance behavior, directly adjacent to this change. (role: recent Antigravity and spend-area contributor; confidence: high; commits: 1118a6288beb, 0d7af47ec882; files: Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift, Sources/CodexBarCore/CostUsageFetcher.swift)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Pass the configured calendar to the Cursor parser and add a date-only CSV test with a calendar whose time zone differs from the process calendar.

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 (11 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-21T20:40:13.631Z sha 62341ac :: needs real behavior proof before merge. :: [P1] Catch remote Cursor failures before using the cache | [P1] Rebuild summaries after filtering the history window | [P1] Keep unpriced Antigravity costs unavailable | [P1] Deduplicate Antigravity usage by response ID | [P1] Use the CSV total-token column | [P2] Parse date-only CSV rows with UTC components
  • reviewed 2026-08-21T21:55:57.144Z sha 667fe62 :: needs real behavior proof before merge. :: [P1] Catch remote Cursor failures before using the cache | [P1] Rebuild summaries after filtering local history | [P1] Keep unpriced Antigravity costs unavailable | [P1] Deduplicate Antigravity responses across cache files | [P2] Parse date-only CSV values with UTC components | [P2] Use the fetcher's pinned calendar for local snapshots
  • reviewed 2026-08-21T22:15:20.674Z sha 7b8abcd :: needs real behavior proof before merge. :: [P1] Catch remote Cursor errors before falling back | [P1] Recompute summaries after window filtering | [P1] Keep unpriced Antigravity costs unavailable | [P1] Deduplicate cache records by response ID | [P2] Parse date-only CSV rows in UTC | [P2] Use the configured calendar for local snapshots
  • reviewed 2026-08-22T14:02:43.949Z sha aa464e6 :: needs real behavior proof before merge. :: [P1] Keep absent Antigravity caches unavailable | [P1] Preserve cache freshness before reporting zero | [P1] Replace the unreleased fixture model name | [P2] Keep date-only CSV rows in the configured day
  • reviewed 2026-08-22T14:54:58.212Z sha 6d80e72 :: needs real behavior proof before merge. :: [P1] Preserve cache freshness before reporting zero | [P1] Replace the unreleased model name in the fixture | [P2] Pass the configured calendar into local token snapshots | [P2] Keep date-only CSV rows in the configured day
  • reviewed 2026-08-23T02:02:01.067Z sha 902c740 :: needs real behavior proof before merge. :: [P1] Keep absent Antigravity caches unavailable | [P1] Preserve local-cache freshness before reporting zero | [P1] Replace the unreleased fixture model name | [P2] Pass the configured calendar into local snapshots | [P2] Keep date-only CSV rows in the configured day
  • reviewed 2026-08-23T04:13:31.267Z sha 902c740 :: needs real behavior proof before merge. :: [P1] Preserve cache freshness before reporting zero | [P2] Pass the pinned calendar into local snapshots | [P2] Keep date-only CSV rows in the configured day | [P1] Use a released or clearly fictitious fixture model
  • reviewed 2026-08-23T10:16:16.437Z sha 60fdee7 :: found issues before merge. :: [P2] Pass the pinned calendar to Cursor CSV parsing

@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: 79bdb87e2f

ℹ️ 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".


private static func loadAntigravityLocalSnapshot(now: Date, historyDays: Int) async -> CostUsageTokenSnapshot? {
let report = AntigravityLocalReader.makeDailyReport()
guard !report.data.isEmpty else { return nil }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep a missing Antigravity cache unavailable

When no tokscale cache exists or every row is unreadable, returning nil here falls through to CostUsageScanner, whose default branch returns an empty report; loadLocalTokenScanResult then marks every non-Codex report as having established coverage, so tokenSnapshot publishes zero tokens and $0 spend. This makes the optional external cache appear to confirm no usage for Antigravity users who never installed tokscale; stop the Antigravity path or preserve unestablished coverage when the cache is absent.

Useful? React with 👍 / 👎.

sessionModel = json["modelId"] as? String ?? json["model_id"] as? String
continue
}
if type == "usage" || json["input"] != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Deduplicate Antigravity usage by response ID

When the synced cache contains the same response in multiple session JSONLs, this loop counts every copy because it ignores the format's responseId identity and only merges by day. The duplicated response inflates token totals, request counts, and model breakdowns on every refresh; track seen nonempty response IDs across all cache files before aggregating usage records.

Useful? React with 👍 / 👎.

@Yuxin-Qiao
Yuxin-Qiao force-pushed the feat/spend-tokscale-antigravity-cursor branch from 79bdb87 to 9c2e542 Compare August 21, 2026 13:29

@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: 9c2e542f12

ℹ️ 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".

Comment thread Package.swift
@@ -1,245 +0,0 @@
// swift-tools-version: 6.2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 Badge Restore the deleted project tree

In every checkout of this commit, deleting Package.swift along with all Sources, Tests, workflows, documentation, and nearly every script leaves only Scripts/lint.sh; the package can no longer build or run, and the advertised Cursor/Antigravity implementation is absent. Restore the project tree and apply only the intended reader changes before merging.

AGENTS.md reference: AGENTS.md:L3-L7

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. P0 Emergency: data loss, security bypass, crash loop, or unusable core runtime. and removed P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. labels Aug 21, 2026
…igravity

- 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.
@Yuxin-Qiao
Yuxin-Qiao force-pushed the feat/spend-tokscale-antigravity-cursor branch from 9c2e542 to b9a4f38 Compare August 21, 2026 19:19

@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: b9a4f38c64

ℹ️ 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".

self.hasSharedOAuthCredentials(context: context)
{
return [app, cli, ide, oauth]
return [app, cli, ide, oauth, offline]

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 Route OAuth errors to the offline fallback

When auto mode has selected or shared OAuth credentials, this ordering places offline after AntigravityOAuthFetchStrategy, but that strategy's shouldFallback always returns false; ProviderFetchPipeline.fetch therefore returns immediately on an expired credential or network error and never attempts the newly added offline strategy. Either place the offline strategy before the terminal OAuth strategy or explicitly permit the intended OAuth-to-offline transition.

Useful? React with 👍 / 👎.

Comment on lines +43 to +44
let cache = self.tokscaleCacheDirectory(home: home)
return self.countJSONLFiles(in: cache, fileManager: fileManager)

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 Honor TOKSCALE_CONFIG_DIR for offline availability

When tokscale is configured through TOKSCALE_CONFIG_DIR and there are no Antigravity CLI database files under the default home, countConversations ignores the supplied environment and looks only in ~/.config/tokscale. The local usage reader honors this override, but the offline fetch strategy reports itself unavailable, so users with a valid cache at the configured location do not receive the intended fallback.

Useful? React with 👍 / 👎.

@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: 773305da2d

ℹ️ 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".

/// almost immediately after a successor ships. Keep old picker selections routable and
/// prevent stale payloads from republishing dead wire ids as separate rows.
private static let retiredFlashTiers: [String: String] = [
"gemini-3.6-flash": "gemini-3.7-flash",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop rewriting quota IDs to an unreleased model

When Antigravity returns any of these legitimate historical IDs, this table relabels the quota as gemini-3.7-flash and can merge tier-specific entries under that invented identity, so users see a different model than the service reported and potentially lose distinct quota rows. Remove the speculative aliases unless the replacement is an officially released, verified wire ID; repository guidance explicitly prohibits unreleased model names in code and tests.

AGENTS.md reference: AGENTS.md:L22-L22

Useful? React with 👍 / 👎.

updatedAt: Date(),
identity: ProviderIdentitySnapshot(
providerID: .antigravity,
accountEmail: AntigravitySelectedAccountGuard.selectedAccountEmail(context: context),

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 Do not attribute ambient offline data to the selected account

In explicit CLI mode with a saved OAuth account selected, if the live probes fail and the local cache belongs to a different Antigravity sign-in, this fallback attaches the selected OAuth email even though the conversation files are ambient and contain no account verification. The resulting menu snapshot misattributes the offline count to that saved account; leave the email unset or derive and validate it from local metadata.

Useful? React with 👍 / 👎.

@Yuxin-Qiao
Yuxin-Qiao force-pushed the feat/spend-tokscale-antigravity-cursor branch from 773305d to ba5a4db Compare August 21, 2026 20:03

@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: ba5a4db494

ℹ️ 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".

costUSD: 0,
modelsUsed: nil,
modelBreakdowns: [])
let tot = row.input + row.output + row.cacheRead + row.cacheWrite

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Honor the CSV's authoritative total-token column

When cache buckets are present, this recomputes a total that can disagree with the exported Total Tokens value and inflates every daily/model/summary total. For example, the newly added v1 fixture declares 1,500 total tokens, but this expression produces 1,900 (800 + 300 + 400 + 400); parse and aggregate the format's total-token column instead of deriving a different value.

Useful? React with 👍 / 👎.

noDataMessage: { "Antigravity cost summary is not supported." }),
supportsTokenCost: true,
noDataMessage: { "Antigravity cost summary is not supported." },
supportsTokenSnapshot: true),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add Antigravity to the Linux capability expectation

On Linux this unconditional capability makes small provider capabilities preserve legacy registries fail because its #else expectation still omits .antigravity. Fresh evidence after the earlier review comment is that this commit updated only the macOS expectation while leaving the Linux set unchanged, even though the descriptor is not platform-gated.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. P2 Normal priority bug or improvement with limited blast radius. and removed merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. P0 Emergency: data loss, security bypass, crash loop, or unusable core runtime. labels Aug 21, 2026

@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: 667fe62545

ℹ️ 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".

Comment on lines +1145 to +1146
let full = CursorLocalCSVReader.makeDailyReport(from: allRows, now: now)
let cal = Calendar.current

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 Thread the pinned bucket calendar through local readers

When the pinned cost-usage timezone differs from Calendar.current—for example, after the user travels—both new local fallbacks ignore the calendar supplied through CostUsageFetcher and instead parse, group, filter, and resolve the current session using the process timezone. Midnight-adjacent Cursor or Antigravity events can therefore move to another day or fall across the requested-window boundary, violating the pinned-bucket behavior; pass the configured scanner calendar through both readers, filtering, and tokenSnapshot.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. labels Aug 21, 2026

@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: aa464e6b6d

ℹ️ 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 daily = CostUsageDailyReport(data: filtered, summary: filteredSummary)
return Self.tokenSnapshot(
from: daily,
now: now,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the cache's actual freshness

When the tokscale cache has not synced recently, passing refresh-time now here and leaving historyCoverageIsEstablished at its default makes stale local data appear freshly updated with complete coverage. If every cached Cursor row predates the requested window, this returns an established empty snapshot and reports zero current spend instead of indicating stale/unavailable data; the mirrored Antigravity return has the same problem. Derive updatedAt and coverage from the cache files or decline the fallback when it cannot establish the requested window.

Useful? React with 👍 / 👎.

Comment on lines +187 to +189
var c = utcCal.dateComponents([.year, .month, .day], from: d)
c.hour = 12; c.minute = 0; c.second = 0; c.timeZone = TimeZone(secondsFromGMT: 0)
return utcCal.date(from: c) ?? d

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 date-only rows in UTC+13 and UTC+14

For users in UTC+13 or UTC+14, representing a date-only CSV value as noon UTC moves it into the following local day when makeDailyReport derives the configured-calendar key. For example, 2026-08-21 becomes August 22 in Pacific/Kiritimati, shifting totals and potentially crossing the requested-window boundary. Preserve the parsed day components directly in the target calendar rather than routing a date-only value through a UTC instant.

Useful? React with 👍 / 👎.

let firstStamp = Int64(noon.timeIntervalSince1970 * 1000)
let secondStamp = firstStamp + 3_600_000
let jsonl = [
#"{"type":"session_meta","modelId":"gemini-3.7-flash"}"#,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace the unreleased model name in the fixture

The new Antigravity fixture hard-codes gemini-3.7-flash, which is neither a released model nor clearly fictitious. Replace it and the matching expectation with a released or unmistakably synthetic identifier so tests do not expose an unreleased model name.

AGENTS.md reference: AGENTS.md:L22-L22

Useful? React with 👍 / 👎.

@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 22, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

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

@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: 57e4d9e83d

ℹ️ 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".

Comment on lines +1185 to +1189
from: daily,
now: now,
historyDays: historyDays,
useCurrentLocalDayForSession: true,
costProvenance: .listPriceEstimate)

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 Pass the fallback calendar into tokenSnapshot

When the configured scanner calendar differs from Calendar.current near a day boundary, this call builds daily keys with the configured calendar but lets tokenSnapshot search for today's entry using its default calendar, so current-session tokens and cost can incorrectly become zero. Fresh evidence after the earlier calendar comment is that the loader now receives and uses fallbackCalendar for parsing and filtering, but this final call—and the mirrored Antigravity call—still omits calendar: calendar.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Aug 22, 2026
@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

Added terminal-only real behavior proof to the PR body (no screenshots per request).

Synthetic TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof caches with Cursor v1/v2/v3 CSVs and Antigravity JSONL, read through the verbatim CursorLocalCSVReader / AntigravityLocalReader at 902c7401a via swiftc + TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof /tmp/proof-3113-bin.

Shows: cacheWrite = with - without, noon UTC for date-only, Total Tokens authoritative, session_meta fallback, responseId dedup, and unpriced costUSD nil. Full console log is now in the PR description.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Aug 23, 2026
…-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
@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

Fixed 4 review blockers at 075eac7 and added production fetcher proof to PR body.

  • freshness: filtered empty → nil (no stale zero)
  • calendar: tokenSnapshot(..., calendar: cal)
  • date-only: noon in configured calendar
  • fixture: test-model-antigravity-a

Production fetcher CostUsageFetcher.loadCursorLocalSnapshot / loadAntigravityLocalSnapshot with TOKSCALE_CONFIG_DIR=/tmp/tokscale-proof now shows 3-day Cursor snapshot (3810 tokens, 0.068) and stale-window nil, see PR body.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦞👀
Exact review queued.

Re-review progress:

@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

Head 60fdee7: fixed lint (test-model-a, line length) — swiftlint --strict now 0 violations. Previous 075eac7 fixes + production fetcher proof still apply (see PR body).

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 23, 2026
Allowlist lines 1339->1335 and 1695->1691 after 075eac7 freshness/calendar fixes
@steipete
steipete merged commit 7427660 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

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants