Skip to content

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

Merged
steipete merged 25 commits into
steipete:mainfrom
Yuxin-Qiao:fix/codex-tokscale-parity-p0
Aug 23, 2026
Merged

fix(codex): tokscale parity for token counts - max cached, clamped, reasoning split, stale#3120
steipete merged 25 commits into
steipete:mainfrom
Yuxin-Qiao:fix/codex-tokscale-parity-p0

Conversation

@Yuxin-Qiao

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

Copy link
Copy Markdown
Contributor

Summary

  • rebuild the parity branch on current main; replace the prior corrupted remote history
  • derive cached usage as max(cached_input_tokens, cache_read_input_tokens), matching tokscale before its clamp to input
  • add field-level stale-regression detection for out-of-order token_count events and run it before watermark latching so stale snapshots cannot enter interleaved mode
  • keep reasoning additive metadata; do not subtract it from output because tokscale treats reasoning as a display subset of output, while CodexBar stores both fields alongside output
  • compare reasoning in stale detection only when both snapshots provide it; an omitted reasoning field is unknown, not zero (ClawSweeper P2)
  • apply the maximum cache-field selection in the JSONSerialization fallback path, not just the fast byte parser (ClawSweeper P2)
  • parse bare usage rows from non-event rollout lines (one-shot codex exec / headless output): canonical usage envelopes, OpenAI and completion-style aliases, cached-input subtraction, timestamp fallback to the last accepted row
  • regenerate the Codex parser hash after scanner changes
  • fix codex json fallback applies maximum cache selection fixture: Swift multiline literal had inserted newlines inside the JSON (invalid JSONL) and the huge integer alone no longer forced the fallback; now built via joined string parts with an escaped cache-read key so the fast parser declines and the fallback path is exercised

Evidence

  • Head: fix/codex-tokscale-parity-p0 @ ec305bc
  • swift test --filter CostUsageScannerTests: 22/22 passed, including codex json fallback applies maximum cache selection
  • make check: clean (SwiftFormat, SwiftLint strict 0 violations, parser hash current, locale/docs/package gates)
  • CI: https://github.com/steipete/CodexBar/actions/runs/32625155825 — success (lint, changes, build-linux-cli x64/arm64, build-linux-musl-cli, swift-test-macos 0/2 and 1/2, lint-build-test)

Reference: tokscale crates/tokscale-core/src/sessions/codex.rs

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

@clawsweeper clawsweeper Bot added merge-risk: 🚨 automation 🚨 Merging this PR could break CI, automerge, proof capture, label sync, or automation. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P1 Urgent regression or broken agent/channel workflow affecting real users now. 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 real behavior proof before merge. Reviewed August 23, 2026, 5:54 PM ET / 21:54 UTC.

ClawSweeper review

What this changes

The branch adjusts Codex rollout token parsing for cache aliases, stale snapshots, and bare responses, while also including unrelated provider, dashboard, UI, release, and proof changes.

Merge readiness

Blocked until real behavior proof from a real setup is added - 16 items remain

Keep open: the PR is incorrect because its new dashboard throttle drops completed account results, and several bare-rollout parser defects remain. Likely related people: Yuxin-Qiao (parser work), olddonkey (recent CostUsage work), and steipete (current-head repair).

Priority: P1
Reviewed head: 90a1e2637734eb8f70350b55e32810193a5b5921

Review scores

Measure Result What it means
Overall readiness 🧂 unranked krab (1/6) Concrete correctness defects, broad unrelated scope, and missing real behavior proof leave the patch unready to merge.
Proof confidence 🦪 silver shellfish (2/6) Needs real behavior proof before merge: No redacted after-fix run through a real Codex rollout is present; CI and synthetic tests are supplemental. Add redacted terminal output or runtime logs, then update the PR body for a fresh review or ask a maintainer to comment @clawsweeper re-review.
Patch quality 🧂 unranked krab (1/6) 9 actionable review findings remain.

Verification

Check Result Evidence
Real behavior Needs proof Needs real behavior proof before merge: No redacted after-fix run through a real Codex rollout is present; CI and synthetic tests are supplemental. Add redacted terminal output or runtime logs, then update the PR body for a fresh review or ask a maintainer to comment @clawsweeper re-review.
Evidence reviewed 4 items Throttle result is discarded: When a fourth account is queued, group.next() removes a completed tuple but its value is discarded before the later collection loop, omitting that account from dashboard results.
Bare parser stores exclusive prompt input: The bare-response path subtracts cached tokens before storing input, unlike the downstream contract that expects total prompt input plus a cached subset.
Mixed branch includes release artifacts: The merge diff changes 57 files (+2,813/-175), including appcast, changelog, providers, dashboard behavior, UI, and proof assets; repository guidance says generated root appcast artifacts should be avoided except during releases.
Findings 9 actionable findings [P1] Retain results drained by the throttle
[P2] Store bare input as the total prompt count
[P2] Take the maximum across bare cache aliases
Security None None.

Live Verification

Command: swift test --filter CostUsageScannerTests

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.23.0.tgz

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

Assertions:

  • FAIL expect_output: Test Suite

How this fits together

CodexBar scans local Codex rollout logs into token and cost rows that feed Usage & Spend. The PR also changes the concurrent Codex-account loading path used by the Spend dashboard.

flowchart LR
A[Codex rollout logs] --> B[Token scanner]
B --> C[Usage rows]
C --> D[Cost report]
D --> E[Spend dashboard]
F[Codex accounts] --> G[Concurrent loaders]
G --> E
Loading

Before merge

  • Add real behavior proof - Needs real behavior proof before merge: No redacted after-fix run through a real Codex rollout is present; CI and synthetic tests are supplemental. Add redacted terminal output or runtime logs, then update the PR body for a fresh review or ask a maintainer to comment @clawsweeper re-review.
  • Retain results drained by the throttle (P1) - group.next() removes a completed child before adding the fourth account, but its tuple is discarded. With four accounts, the first completion never reaches results, so that account silently vanishes from inputs or failedSourceIDs; append it before launching the next task.
  • Store bare input as the total prompt count (P2) - This removes cached tokens before storing input, but the cost path expects input to be the total prompt count and subtracts the cached subset itself. A row with 120 input and 20 cached becomes 100/20 and is undercounted; store 120 plus the separate cache subset.
  • Take the maximum across bare cache aliases (P2) - The helper returns the first present alias, so cached_input_tokens: 0 wins over cache_read_input_tokens: 25. This disagrees with the token-count path and records cached usage as uncached; select the maximum reported alias.
  • Read models from nested bare envelopes (P2) - The parser accepts result.usage and response.usage, but model lookup only checks the root and data. A standalone nested response is stored as unknown, losing model-specific attribution and pricing; inspect result and response too.
  • Use the scan calendar for bare usage days (P2) - Bare rows derive their day with the process calendar, unlike typed rows that pass range.calendar. Near a configured bucket-timezone midnight, this groups the row on the wrong day or outside the requested range; forward range.calendar here.
  • Persist and seed the bare timestamp fallback (P2) - lastAcceptedTokenTimestamp is local to one parse call and only updates after a bare row. It is neither carried in CodexParseResult nor seeded by regular token events, so timestamp-less bare rows are dropped on resumed scans and after typed records; preserve and update it through resume state.
  • Continue typed parsing when bare extraction declines (P2) - Any line containing "usage" enters this block, but a typed event makes obj["type"] != nil, so the guard returns and the unconditional return skips the normal typed parser. A token-count event with ancillary usage data therefore loses its delta; return early only after a bare row was handled.
  • Make stale-ratio arithmetic overflow-safe (P2) - A malformed but Int-representable counter above Int.max / 100 reaches these products and traps during stale detection. Use reporting-overflow or division-based comparison so a bad local rollout line fails soft instead of aborting the scan.
  • Remove release artifacts from this parser PR (P2) - appcast.xml is a generated root release artifact and repository guidance says to avoid changing it except during releases. It is unrelated to the stated parser repair; remove it together with the unrelated release and product changes.
  • Resolve merge risk (P1) - The 57-file mixed branch combines independent provider, dashboard, UI, and release changes with a parsing repair, making regressions difficult to attribute or roll back.
  • Resolve merge risk (P1) - No redacted after-fix real Codex rollout run demonstrates the intended behavior; CI and synthetic tests are supplemental.
  • Complete next step (P2) - The identified code defects have narrow mechanical repairs, though real rollout proof remains required before merge.
  • Improve patch quality - Retain every group.next() result and add a four-account regression.
  • Improve patch quality - Repair the bare-rollout contracts and add focused regressions for aliases, nested models, time zones, and resumed scans.
  • Improve patch quality - Post redacted after-fix Codex rollout output after rebuilding the focused branch.

Findings

  • [P1] Retain results drained by the throttle — Sources/CodexBar/SpendDashboardController.swift:474-475
  • [P2] Store bare input as the total prompt count — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:3728-3741
  • [P2] Take the maximum across bare cache aliases — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:3728-3729
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Changed scope 57 files; production +1,076/-105, tests +1,406/-46, docs/release/proof +331/-24 The merge diff greatly exceeds the stated Codex parser repair and includes unrelated release and product surfaces.

Merge-risk options

Maintainer options:

  1. Rebuild and repair the focused patch (recommended)
    Keep only the Codex scanner repair and focused tests, fix the dashboard and parser defects, and add redacted after-fix rollout proof before merge.
  2. Pause the mixed branch
    Pause or close this branch if the parser repair cannot be extracted without its unrelated product and release changes.

Technical review

Best possible solution:

Rebuild the change as a focused Codex scanner PR, preserve total prompt tokens with cached tokens as a subset, retain all throttled dashboard results, and add redacted real-rollout proof.

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

Yes, from source: queue four Codex accounts and the result drained by the three-task throttle is discarded; focused parser fixtures can likewise reproduce the bare-row errors.

Is this the best way to solve the issue?

No: the parser repair should be a narrow scanner change with focused coverage, not a combined 57-file product and release branch.

Full review comments:

  • [P1] Retain results drained by the throttle — Sources/CodexBar/SpendDashboardController.swift:474-475
    group.next() removes a completed child before adding the fourth account, but its tuple is discarded. With four accounts, the first completion never reaches results, so that account silently vanishes from inputs or failedSourceIDs; append it before launching the next task.
    Confidence: 0.99
  • [P2] Store bare input as the total prompt count — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:3728-3741
    This removes cached tokens before storing input, but the cost path expects input to be the total prompt count and subtracts the cached subset itself. A row with 120 input and 20 cached becomes 100/20 and is undercounted; store 120 plus the separate cache subset.
    Confidence: 0.99
  • [P2] Take the maximum across bare cache aliases — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:3728-3729
    The helper returns the first present alias, so cached_input_tokens: 0 wins over cache_read_input_tokens: 25. This disagrees with the token-count path and records cached usage as uncached; select the maximum reported alias.
    Confidence: 0.99
  • [P2] Read models from nested bare envelopes — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:3744
    The parser accepts result.usage and response.usage, but model lookup only checks the root and data. A standalone nested response is stored as unknown, losing model-specific attribution and pricing; inspect result and response too.
    Confidence: 0.98
  • [P2] Use the scan calendar for bare usage days — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:4187-4189
    Bare rows derive their day with the process calendar, unlike typed rows that pass range.calendar. Near a configured bucket-timezone midnight, this groups the row on the wrong day or outside the requested range; forward range.calendar here.
    Confidence: 0.98
  • [P2] Persist and seed the bare timestamp fallback — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:4213-4215
    lastAcceptedTokenTimestamp is local to one parse call and only updates after a bare row. It is neither carried in CodexParseResult nor seeded by regular token events, so timestamp-less bare rows are dropped on resumed scans and after typed records; preserve and update it through resume state.
    Confidence: 0.98
  • [P2] Continue typed parsing when bare extraction declines — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:4680-4691
    Any line containing "usage" enters this block, but a typed event makes obj["type"] != nil, so the guard returns and the unconditional return skips the normal typed parser. A token-count event with ancillary usage data therefore loses its delta; return early only after a bare row was handled.
    Confidence: 0.99
  • [P2] Make stale-ratio arithmetic overflow-safe — Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift:434-439
    A malformed but Int-representable counter above Int.max / 100 reaches these products and traps during stale detection. Use reporting-overflow or division-based comparison so a bad local rollout line fails soft instead of aborting the scan.
    Confidence: 0.97
  • [P2] Remove release artifacts from this parser PR — appcast.xml:5-52
    appcast.xml is a generated root release artifact and repository guidance says to avoid changing it except during releases. It is unrelated to the stated parser repair; remove it together with the unrelated release and product changes.
    Confidence: 0.98

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 7427660af27d.

Labels

Label justifications:

  • P1: With four or more Codex accounts, the new dashboard throttle can silently omit a completed account result.
  • merge-risk: 🚨 compatibility: The branch changes provider behavior, UI policy, local readers, and release metadata beyond the parser repair.
  • merge-risk: 🚨 availability: The dashboard loading loop now discards completed account results while enforcing its new concurrency limit.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: No redacted after-fix run through a real Codex rollout is present; CI and synthetic tests are supplemental. Add redacted terminal output or runtime logs, then update the PR body for a fresh review or ask a maintainer to comment @clawsweeper re-review.

Evidence

Acceptance criteria:

  • [P1] swift test --filter CostUsageScannerTests.
  • [P1] swift test --filter SpendDashboard.
  • [P1] make check.

What I checked:

Likely related people:

  • Yuxin-Qiao: The supplied PR history attributes the rollout parser changes to Yuxin-Qiao. (role: recent parser contributor; confidence: medium; commits: 72e3aa292174, 66a0d8aaaff7; files: Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift)
  • olddonkey: Recent CostUsage reconciliation work in the supplied history touches the same reporting subsystem. (role: recent CostUsage area contributor; confidence: medium; commits: 07bd85a5e6e9; files: Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+ReportReconciliation.swift)
  • steipete: The current reviewed head and available blame are attributed to steipete after the merged-tree repair. (role: current-head committer; confidence: medium; commits: 90a1e2637734; files: Sources/CodexBar/SpendDashboardController.swift, Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift)

Rating scale

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

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

Workflow

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

History

Review history (15 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-22T12:31:50.020Z sha f3aaf19 :: needs real behavior proof before merge. :: [P1] Include reasoning in reported token totals | [P2] Keep bare input as the total prompt count | [P2] Take the maximum across bare cache aliases | [P2] Read models from nested bare-usage envelopes | [P2] Use the requested calendar for bare usage days | [P2] Persist the bare-row timestamp fallback across scans
  • reviewed 2026-08-22T14:17:22.335Z sha da4b1d8 :: needs real behavior proof before merge. :: [P1] Include reasoning in report totals and fallback pricing | [P2] Make stale-ratio arithmetic overflow-safe | [P2] Store bare input as the full prompt count | [P2] Take the maximum across bare cache aliases | [P2] Read models from nested bare-usage envelopes | [P2] Use the scan calendar for bare-usage days
  • reviewed 2026-08-22T15:24:32.039Z sha 66a0d8a :: needs real behavior proof before merge. :: [P2] Store total prompt tokens for bare usage | [P2] Take the maximum cache alias in bare responses | [P2] Read model evidence from every accepted bare envelope | [P2] Use the requested calendar for bare usage | [P2] Seed bare-row timestamp fallback from normal events | [P2] Route bare usage through subagent ownership filtering
  • reviewed 2026-08-23T04:55:49.035Z sha c69ac50 :: needs real behavior proof before merge. :: [P2] Store full prompt tokens for bare usage | [P2] Take the maximum across bare cache aliases | [P2] Read nested models for accepted bare envelopes | [P2] Use the requested calendar for bare rows | [P2] Seed bare timestamp fallback from typed records | [P2] Keep bare rows inside subagent ownership filtering
  • reviewed 2026-08-23T05:37:55.592Z sha c69ac50 :: needs real behavior proof before merge. :: [P2] Store total prompt tokens for bare usage | [P2] Take the maximum across bare cache aliases | [P2] Read model evidence from nested bare envelopes | [P2] Use the requested calendar for bare usage days | [P2] Seed bare timestamp fallback from typed records | [P2] Route bare usage through subagent ownership filtering
  • reviewed 2026-08-23T06:50:02.797Z sha 990e742 :: needs real behavior proof before merge. :: [P1] Retain results consumed by the throttle | [P2] Store total prompt tokens for bare usage | [P2] Take the maximum across bare cache aliases | [P2] Read nested model evidence for bare envelopes | [P2] Use the requested calendar for bare rows | [P2] Seed bare timestamp fallback from typed records
  • reviewed 2026-08-23T07:21:00.716Z sha ec305bc :: needs real behavior proof before merge. :: [P1] Retain results consumed by the throttle | [P2] Store total prompt tokens for bare usage | [P2] Take the maximum across bare cache aliases | [P2] Read nested model evidence for bare envelopes | [P2] Use the requested calendar for bare rows | [P2] Seed bare timestamp fallback from typed records
  • reviewed 2026-08-23T10:06:14.983Z sha ec305bc :: needs real behavior proof before merge. :: [P1] Retain results drained by the throttle | [P2] Keep bare input as the total prompt count | [P2] Take the maximum across bare cache aliases | [P2] Read model evidence from nested bare envelopes | [P2] Use the requested calendar for bare usage | [P2] Persist the bare-row timestamp fallback

@Yuxin-Qiao
Yuxin-Qiao force-pushed the fix/codex-tokscale-parity-p0 branch from a58c4b4 to 1aec80d Compare August 21, 2026 18:47
@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

Refreshed the branch after regenerating the parser hash. New head: e1ae18f. Local evidence updated in the PR body: build passed, CostUsageScannerTests 18/18 passed, changed-file format/lint clean.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 21, 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 P2 Normal priority bug or improvement with limited blast radius. and removed P1 Urgent regression or broken agent/channel workflow affecting real users now. merge-risk: 🚨 automation 🚨 Merging this PR could break CI, automerge, proof capture, label sync, or automation. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. labels Aug 21, 2026
@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

Additional local evidence for head e1ae18f:

  • Full macOS test shard run locally: 6 of 7 groups fully green; all non-environmental suites pass
  • The one failing group (AlibabaTokenPlanWebStrategyTests) fails identically from a clean environment in this sandbox: cookie-cache file locks under Application Support cannot be created (Operation not permitted), a pre-existing environmental limitation unrelated to the parser changes
  • CI lint-build-test failure is the intended Draft gate: macOS Swift tests are deferred until ready-for-review per repo design

@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 21, 2026
@Yuxin-Qiao
Yuxin-Qiao marked this pull request as ready for review August 21, 2026 20:11

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

ℹ️ 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 +422 to +427
let currentReasoning = current.reasoning ?? 0
let previousReasoning = previous.reasoning ?? 0
guard current.input < previous.input
|| current.cached < previous.cached
|| current.output < previous.output
|| currentReasoning < previousReasoning

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 events when optional reasoning metadata disappears

When a cumulative event follows one containing reasoning_output_tokens but omits that optional field—even while its input/output counters advance—coalescing nil to zero manufactures a reasoning regression. The aggregate will usually satisfy the stale threshold, causing both parsing paths to return early and omit the event's token and cost delta. Compare reasoning only when both snapshots supply it, rather than interpreting absence as zero.

Useful? React with 👍 / 👎.

Comment on lines +4282 to +4286
let staleBaseline = tracker.watermark ?? rawTotalsBaseline
if let previousTotal = staleBaseline,
!hasUnresolvedForkBaseline,
Self.codexLooksLikeStaleRegression(
current: adjustedTotal,

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 Stop stale suppression after interleaved mode is latched

Once an Ultra/multi-lineage stream has already set sawInterleavedTotals, this check still discards every close aggregate with any regressed component. For example, with a 100/100 input/output watermark, a later lineage snapshot of 90/110 is classified stale and returned here, although the existing post-latch containment would safely count the 10 new output tokens. Limit this pre-latch heuristic to non-interleaved state so legitimate component growth continues through the containment path.

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: 1bc79b0d20

ℹ️ 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 cachedTokens = Self.codexBareUsageInt(
usage, keys: ["cached_input_tokens", "cache_read_input_tokens", "cached_tokens"]) ?? 0
let billedInput = max(0, inputTokens - cachedTokens)

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 Keep bare input as the total prompt count

When a bare usage envelope includes cached tokens, subtracting them here violates CodexBar's token contract: CostUsagePricing.codexCostUSD treats inputTokens as the total prompt and subtracts cachedInputTokens itself. A response with 120 prompt and 20 cached tokens is therefore stored as 100/20 and priced as 80 uncached plus 20 cached rather than 100 uncached plus 20 cached, underreporting both tokens and cost. Store the total input count and the cached subset separately.

Useful? React with 👍 / 👎.

var days: [String: [String: [Int]]] = [:]
var rows: [CodexUsageRow] = []
var tokenSnapshots: [CostUsageCodexTokenSnapshot] = []
var lastAcceptedTokenTimestamp: String?

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 Persist the timestamp fallback across incremental scans

When a rollout is resumed with a nonzero startOffset—either after an append or a bounded partial scan—this fallback is reset to nil, and CodexParseResult does not carry it forward. If a timestamped bare usage row was processed in the prior chunk and a timestamp-less response is processed in the next, handleBareUsage cannot resolve a day and silently drops the new usage; preserve the last accepted timestamp in the resumable parser state.

Useful? React with 👍 / 👎.

@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 21, 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.

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

ℹ️ 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 +3726 to +3727
let cachedTokens = Self.codexBareUsageInt(
usage, keys: ["cached_input_tokens", "cache_read_input_tokens", "cached_tokens"]) ?? 0

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 Take the maximum across bare cache aliases

When a bare usage envelope contains both cached_input_tokens and cache_read_input_tokens, this selects the first present key rather than the maximum. For example, a stale cached_input_tokens: 0 alongside cache_read_input_tokens: 25 records all prompt tokens as uncached and none as cached, even though the token-count paths in this change explicitly handle this format by taking the maximum; apply the same alias selection here.

Useful? React with 👍 / 👎.

cached: cachedTokens,
output: outputTokens,
reasoning: nil),
modelEvidence(obj) ?? (obj["data"] as? [String: Any]).flatMap(modelEvidence))

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 Read model evidence from every accepted envelope

When one of the newly accepted result or response envelopes carries its model next to its nested usage object, the usage is parsed but the model lookup only examines the root and data. A one-shot row without an earlier turn context is consequently stored as unknown, losing model attribution and potentially preventing model-specific pricing; inspect result and response for model evidence as well.

Useful? React with 👍 / 👎.

Comment on lines +4184 to +4187
let resolvedTimestamp = timestamp ?? lastAcceptedTokenTimestamp
guard let dayKey = resolvedTimestamp.flatMap({
Self.dayKeyFromTimestamp($0) ?? Self.dayKeyFromParsedISO($0)
}) else { return }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the configured calendar for bare usage days

When the configured cost bucket timezone differs from the process timezone—CLICostCommand.swift constructs the fetcher from tokenCostUsageBucketTimeZone—a bare usage event near midnight is grouped using Calendar.current here instead of range.calendar. It can therefore land on the wrong day or outside the requested scan window, unlike normal token-count rows, which pass range.calendar explicitly.

Useful? React with 👍 / 👎.

Comment on lines +4684 to +4688
handleBareUsage(
totals: bare.totals,
modelEvidence: bare.model,
timestamp: obj["timestamp"] as? String)
}

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 Buffer bare usage before subagent ownership filtering

When a subagent rollout contains a copied-prefix bare usage row, this calls handleBareUsage immediately even though typed records are being accumulated in pendingSubagentLines for owned-suffix filtering. Because bare rows never reach the replay filter around pendingSubagentLines, usage copied from the parent is counted in the child as well; bare usage needs to participate in the same buffering and ownership decision before being added.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. 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: 8701db710f

ℹ️ 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 +3513 to +3514
let exclusiveOutput = max(0, output - (reasoning ?? 0))
return CostUsageCodexTotals(input: input, cached: cached, output: exclusiveOutput, reasoning: reasoning)

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 Migrate consumers before storing exclusive output

For every fast-path token-count event with positive reasoning_output_tokens, this removes those tokens from output, but the downstream codexResolvedCostUSD and makeCodexBilledDayEntry paths still price and total only row.output/packed output without re-adding reasoning. This therefore understates token totals and cost by the reasoning count; additionally, CodexUsageRow.init caps reasoning to the now-exclusive output, truncating reasoning whenever it exceeds the non-reasoning remainder, while the Foundation fallback still stores inclusive output. Preserve inclusive output or migrate all parser paths, row invariants, reporting, and pricing consumers together.

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: 82515c02fc

ℹ️ 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 +3729 to +3732
// A usage object that exists but fails numeric extraction (overflowing counters)
// must decline the fast line so the Foundation fallback re-parses it.
if (totalObject != nil && total == nil) || (lastObject != nil && last == nil) {
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.

P2 Badge Avoid the lossy fallback for overflowing counters

When a usage field exceeds Int, this sends the line to the Foundation path, but that path converts the resulting NSDecimalNumber with unchecked intValue; for the 100-digit input used by the new fixture this can yield zero or a truncated value. The test only asserts cached/output, so the overflowed input is still silently lost rather than recovered; explicitly range-check and clamp/reject the decimal instead of treating Foundation reparsing as sufficient.

Useful? React with 👍 / 👎.

Comment on lines +438 to +439
return currentTotal * 100 >= previousTotal * 98
|| currentTotal + lastTotal * 2 >= previousTotal

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 Make stale-ratio arithmetic overflow-safe

When a corrupt but still Int-representable counter exceeds roughly Int.max / 100 and a later snapshot regresses, these ratio products overflow under Swift's checked arithmetic and trap instead of failing the scan softly. The new overflow-decline path does not catch such values because each individual field parses successfully; compare using division or reporting-overflow operations so malformed rollout data cannot crash the scanner.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. and removed P2 Normal priority bug or improvement with limited blast radius. labels Aug 22, 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: d0349b7c86

ℹ️ 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 +4683 to +4684
obj["type"] == nil,
let bare = Self.codexBareUsage(from: obj)

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 Accept typed completion envelopes in bare-usage parsing

When a non-event response carries root usage plus a discriminator, such as {"type":"turn.completed","usage":...}, this guard rejects the otherwise-supported usage solely because type is present. This is separate from the previously reported event_msg fallthrough problem: even if that unconditional return is fixed, parseCodexFastLine has no turn.completed case, so the completion's tokens remain uncounted; allow recognized completion response types through the bare extractor.

Useful? React with 👍 / 👎.

Yuxin-Qiao and others added 18 commits August 23, 2026 14:47
…ry refresh (steipete#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>
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>
…pete#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 steipete#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).
Co-authored-by: anupamchugh <8416306+anupamchugh@users.noreply.github.com>
* 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(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 steipete#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.
…ck (steipete#3119)

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

Fix P2 from Codex review on steipete#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
…igravity (steipete#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
# Conflicts:
#	Sources/CodexBarCore/Generated/CodexParserHash.generated.swift
#	Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift
@steipete
steipete merged commit d927836 into steipete:main Aug 23, 2026
9 checks passed
steipete added a commit to Yuxin-Qiao/CodexBar that referenced this pull request Aug 23, 2026
steipete added a commit that referenced this pull request Aug 23, 2026
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>
Yuxin-Qiao added a commit to Yuxin-Qiao/CodexBar that referenced this pull request Aug 25, 2026
…tness

- Rebase onto latest main (steipete#3120 parity scanner) preserving cache-first hydration
- Record successful dashboard fetch completion time for 5m TTL expiry
- Fix aggregate SQL timestamp placeholder bindings (18/17 not 20/19)
- Restore exact cutover parity assertion (remove 50k tolerance)
- Restore main scannerUnits semantics in fixture oracle and manifest
- Realign gatekeeper line anchors after scanner rebase
- Remove obsolete pre-steipete#3120 tokscale parity test
Yuxin-Qiao added a commit to Yuxin-Qiao/CodexBar that referenced this pull request Aug 25, 2026
…tness

- Rebase onto latest main (steipete#3120 parity scanner) preserving cache-first hydration
- Record successful dashboard fetch completion time for 5m TTL expiry
- Fix aggregate SQL timestamp placeholder bindings (18/17 not 20/19)
- Restore exact cutover parity assertion (remove 50k tolerance)
- Restore main scannerUnits semantics in fixture oracle and manifest
- Realign gatekeeper line anchors after scanner rebase
- Remove obsolete pre-steipete#3120 tokscale parity test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P1 Urgent regression or broken agent/channel workflow affecting real users now. 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants