Skip to content

Add Notion AI provider - #2552

Merged
steipete merged 4 commits into
steipete:mainfrom
n0ah37:feat/notion-ai-provider
Aug 3, 2026
Merged

Add Notion AI provider#2552
steipete merged 4 commits into
steipete:mainfrom
n0ah37:feat/notion-ai-provider

Conversation

@n0ah37

@n0ah37 n0ah37 commented Aug 1, 2026

Copy link
Copy Markdown

Summary

Adds a Notion AI provider that tracks the two usage-allowance windows Notion shows in Settings → Notion AI → Usage:

  • Rolling — the 6-hour window, with its reset countdown
  • Monthly — the billing-period window, with its period end

Both bars carry an expected-usage estimate (n% in deficit / n% in reserve), on the card and in the CLI.

Notion starts enforcing this allowance on August 3, 2026. Before that date the same endpoint already returns real usage numbers with "enforcement": "preview", so the gauges are accurate either way — which is why it seemed worth having in the menu bar now rather than after people start hitting the cap.

No existing issue or PR covers Notion, so this is a fresh integration rather than a follow-on.

Rebased onto current main after #2606 ("derive provider registrations from descriptors"). Notion now registers through ProviderManifest and ProviderImplementationManifest, its icon style is .init(provider: .notion), and the "not yet supported in widgets" behavior that used to live in a hand-written switch is expressed as widgetSelectable: false on the descriptor.

Data source

Two cookie-authenticated POSTs to app.notion.com per refresh, both endpoints the Notion web app itself calls:

Endpoint Purpose
/api/v3/getSpaces Resolves the signed-in account (email, name) and its workspaces, including plan_type and subscription_tier. Drives automatic workspace selection and the identity line.
/api/v3/getCreditRateLimitStatus The allowance itself, for one spaceId.
{
  "status": "within_limit",
  "window": { "creditType": "basic_ai_credits", "scope": "per_user", "window": "6h", "used": 42.5, "limit": 100 },
  "resetsInSeconds": 12600,
  "billingPeriodWindow": {
    "creditType": "basic_ai_credits", "scope": "per_user", "cadence": "billing_period",
    "used": 18.0, "limit": 100, "periodEndMs": 1788000000000
  },
  "enforcement": "preview"
}

(Illustrative values — the shape is what was observed.)

windowprimary (window length parsed from the 6h token, reset from resetsInSeconds), billingPeriodWindowsecondary (reset from periodEndMs). Usage is scaled against the returned limit rather than assumed to be a percentage, so a future non-100 limit keeps working.

Behaviour notes

  • Plan gating. Only Business and Enterprise workspaces carry an allowance. Anything else answers {"status":"not_applicable"}, which surfaces as a clear provider error naming the workspace instead of a 0% gauge.
  • No window is better than a fabricated one. Every field in the response is optional, so an unrelated 200 body decodes cleanly into an all-nil status. The parser rejects a payload carrying neither window, and a window with no usable limit is omitted rather than reported as 0% — a false "plenty of headroom" is the worst thing this provider could show.
  • Multi-workspace accounts. Defaults to the first workspace on a plan that has an allowance. A specific one can be pinned via the Workspace ID settings field or the existing workspaceID provider config field (no config-schema change) — both dashed and undashed UUID forms are accepted. An id the account cannot see falls back to auto-selection rather than being queried for an opaque 403.
  • The imported header is persisted through CookieHeaderCache, which is what makes background refreshes work at all. BrowserCookieAccessGate.shouldAttempt refuses Chromium cookie reads unless the refresh is user-initiated, so a timer tick can never reach the browser store; without a cached header the provider failed on every automatic refresh and told the user to log in while their session was perfectly valid. A rejected cached header is cleared and re-imported, following the Abacus/Manus shape.
  • A deferred import no longer reads as "logged out". When the gate suppresses every candidate browser but profile data exists on disk, the provider reports that cookies can only be read during a manual refresh, rather than telling the user to log in to an account they are already logged in to.
  • Cookie import requires token_v2, and de-duplicates by cookie name across the Notion domains — a stale token_v2 left on the legacy notion.so alongside the live one would otherwise put two of them in a single header. This is not hypothetical: it reproduced on the machine this was developed on, which held token_v2 on both .app.notion.com and .www.notion.so. Imports are also cached for 5s like the other cookie providers, so a refresh tick does not re-read Safe Storage every time.
  • cURL captures do not forward x-notion-space-id. A capture taken in one workspace would pin that space while the request body asks for the configured one, and the mismatch would surface as another workspace's usage rather than an error.
  • Not read: Notion credits (Custom Agents, Workers) are a separate meter on getAIUsageEligibilityV2. Out of scope here.
  • Follows Perplexity's shape throughout — descriptor + single web strategy, fetcher, snapshot mapping, settings store, cookie-source picker with Off, manual Cookie header / cURL capture path, and a login action. Declares widgetSelectable: false, so it stays out of the widget picker.

Pace

Notion reports only periodEndMs for the billing period, so the snapshot carries the shared monthly sentinel — which is what makes the descriptor's .calendarMonthResetWindow match — and pace resolution substitutes the real calendar month ending at the reset. A flat 30 days would misstate expected usage in February and in every 31-day cycle.

Three paths scored these windows without resolving first, so fixing only the descriptor would not have been enough:

  • UsageStore.weeklyPace — feeds the menu-bar pace token, the "runs out" text, and predictive pace warnings.
  • resetWindowPaceDetail — preferred a caller-supplied pace measured against the raw window, silently undoing the resolution computed one line above it.

Both now resolve, which also fixes the same latent bug for the eight other providers carrying that sentinel (Amp, MiMo, StepFun, Doubao, Alibaba ×2, OpenCode Go): before this, a 31-day cycle exceeded the 30-day sentinel outright and dropped its pace token for the first day of every long month.

Two smaller pieces come with it:

  • The rolling window is paced as a session window in the CLI too. Its 6-hour length sat outside the CLI's 300-minute session ceiling, so the card and codexbar usage disagreed about whether that bar had an estimate at all.
  • codexbar now honors the Notion settings snapshot. Workspace ID, the manual cookie header, and the off source previously had no effect on the CLI, which always auto-selected a workspace.
  • A rolling length that parses to exactly the monthly sentinel (30d, 720h, 43200m) is dropped rather than reported, since pace matching keys on that number and would otherwise resolve the rolling bar as a calendar cycle ending hours away.

One change outside the provider

UsageStore.debugLogText gained a .notion case (the debug pane previously said "not yet implemented" while a working probe existed). That tipped the enclosing function to cyclomatic complexity 21, so .openai and .azureopenai — which already call the same apiKeyDebugLine helper — are folded into one case. Happy to drop the debug-log wiring instead if you'd rather that switch stayed untouched.

Provider count

Bumped 66 → 67 in the places Scripts/check-site-locales.mjs enforces: README.md, docs/providers.md, docs/social.html, docs/llms.txt, docs/index.html, and the three counted keys across all 23 locales in docs/site-locales.mjs.

CodexParserHash.generated.swift is not touched. It was, until #2606 replaced the exhaustive provider switch in CostUsageScanner.swift with a default: arm — the rebase dropped that regeneration along with the case.

Docs

  • docs/notion.md — setup, data source, mapping table, troubleshooting.
  • docs/providers.md — strategy-table row and provider section.
  • docs/configuration.md — notes Notion as a workspaceID consumer.

Commands run

swift build
make check                                    # SwiftFormat + SwiftLint, 0 violations
make test                                     # full sharded suite

Verification

The endpoints, payload shape, and mapping were verified against a live Business workspace — the API's rolling and billing figures matched what the Notion settings pane rendered, including the reset countdown and period end. getSpaces plan gating was confirmed by calling the endpoint for both a Business and a free personal space; the latter returns not_applicable, and an unknown space id returns 403.

31 unit tests cover parsing (both the singly- and doubly-wrapped record shapes), window mapping, workspace selection and id normalization, session-file permissions and round-trip, the resolved calendar-cycle length against known 28- and 31-day resets, the sentinel collision on the rolling token, and — through a stubbed ProviderHTTPTransport — the 401, non-200, and not_applicable paths. NotionMenuCardModelTests drives real menu-card model construction and asserts the monthly bar's estimate is unchanged whether or not a precomputed pace is supplied; without the resolution fix it reports 10% in deficit instead of 15%.

Redacted live proof from this head is in a comment below — manual import, then a background refresh that shows Cookie cache miss yet still succeeds from the persisted token_v2, which is the CredentialFileWriter fallback this PR adds rather than the pre-existing cookie cache. The session file is -rw-------. The Monthly pace line in that output also shows the calendar-cycle fix end to end.

No menu screenshots are included.

Known gap — the Notion desktop app

This reads a browser session only. Notion.app is Electron and does keep a Chromium cookie store, but SweetCookieKit's Browser is a closed enum and its decryption keys come from a per-browser catalogue of Safe Storage labels; Notion.app's key ("Notion Safe Storage") is not in it, so a store pointed at that directory would be found and not decrypted. Supporting it is a SweetCookieKit change — happy to open that separately if you'd take it.

@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: 8c59645cfe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/CodexBarCore/Providers/Notion/NotionProviderDescriptor.swift Outdated
Comment thread Sources/CodexBarCore/Providers/Notion/NotionUsageFetcher.swift Outdated
@clawsweeper clawsweeper Bot added 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. P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. labels Aug 1, 2026
@clawsweeper

clawsweeper Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 3, 2026, 6:37 AM ET / 10:37 UTC.

ClawSweeper review

What this changes

Adds a default-disabled Notion AI provider that imports or accepts a Notion session, selects an eligible workspace, shows rolling and billing-period allowance usage in the menu and CLI, and documents the setup.

Merge readiness

⚠️ Ready for maintainer review - 4 items remain

Keep open for maintainer merge review. The branch adds a coherent default-disabled provider, fixes the two prior cookie-handling findings, and includes redacted after-fix terminal proof; the remaining judgment is whether CodexBar should accept the unsupported Notion cookie/API boundary after one owner-run comparison against a Business workspace.

Priority: P2
Reviewed head: dfac3f71826235190de3464bd29ab9ce98a95b9c
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A substantial, coherent provider addition with strong live terminal proof and focused coverage; the remaining merge gate is owner acceptance of the external cookie/API boundary.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The current-head comment provides redacted terminal proof of a real Notion Business workspace: manual browser import, successful later background refresh from the persisted session after a cache miss, live allowance output, and 0600 file permissions.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The current-head comment provides redacted terminal proof of a real Notion Business workspace: manual browser import, successful later background refresh from the persisted session after a cache miss, live allowance output, and 0600 file permissions.
Evidence reviewed 6 items Provider registration follows current architecture: The Notion descriptor and implementation are added to the manifest-based registration points introduced on current main, rather than restoring the former parallel switches.
Cookie boundary is deliberately constrained: The proposed descriptor sets Chrome-only automatic import, and the fetcher sends the imported or manual session only to the fixed app.notion.com base URL; its captured-header allowlist intentionally excludes the workspace-pinning header.
Persisted fallback is owner-only and atomic: The proposed session store persists only token_v2 through the existing credential writer; that writer creates a 0600 staged file before writing, syncs it, and atomically renames it into place. The PR adds permission and round-trip tests.
Findings None None.
Security None None.

How this fits together

CodexBar provider descriptors register the supported usage sources, while provider implementations turn settings and credentials into usage snapshots for the menu bar and CLI. This change adds Notion's browser-session web flow, maps its workspace allowance responses into the shared rate-window model, and feeds the existing pace and display surfaces.

flowchart LR
    A[Notion browser session] --> B[Chrome-only cookie import or manual header]
    B --> C[Notion provider fetcher]
    C --> D[Workspace and allowance endpoints]
    D --> E[Usage snapshot]
    E --> F[Pace calculation]
    F --> G[Menu bar and CLI output]
Loading

Decision needed

Question Recommendation
After a live Business-workspace comparison, should CodexBar accept the supported scope of a Chrome-only Notion session import plus an owner-only persisted token for background refreshes despite Notion’s unsupported internal API contract? Verify and accept the boundary: Perform the stated owner-run comparison and merge if the values and background session reuse match, accepting that endpoint changes may require future maintenance.

Why: The implementation is coherent and proof-positive, but accepting a third-party browser-session credential and unsupported endpoint as a supported provider is a product and credential-boundary choice that cannot be resolved from tests alone.

Before merge

  • Resolve merge risk (P1) - This adds a new cookie-authenticated integration against unsupported internal Notion endpoints, so a Notion-side contract or session-policy change can stop refreshes without a CodexBar code change.
  • Resolve merge risk (P1) - The feature persists a reusable Notion session token in an owner-only local credential file to support non-interactive refreshes; maintainers must explicitly accept that credential-storage boundary before release.
  • Complete next step (P2) - No mechanical repair remains; the next action is the owner-run live verification and explicit acceptance of the Notion session-storage and unsupported-endpoint boundary before merge.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Change surface 37 files affected; +2,120 / -91 lines The new provider spans core parsing, credential flow, app settings, CLI rendering, documentation, and focused tests, so the owner-run verification should cover the complete user path.

Merge-risk options

Maintainer options:

  1. Verify the live integration, then accept the scoped risk (recommended)
    Compare both allowances with Notion’s Business-workspace UI and confirm a non-user-initiated refresh reuses the stored session before merging this default-disabled integration.
  2. Pause for a different credential policy
    Do not merge if owner-only file storage for a reusable Notion session is outside the repository’s acceptable credential boundary.

Technical review

Best possible solution:

Land the provider only after an owner verifies that a Business workspace's Rolling and Monthly values match Notion’s usage page and confirms a later background refresh succeeds without another browser import, while retaining the Chrome-only and owner-only storage constraints.

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

Not applicable as a bug report. The contributor supplied redacted after-fix terminal output from a real Business workspace, including manual import, non-interactive reuse after a cookie-cache miss, and owner-only session-file permissions.

Is this the best way to solve the issue?

Yes, conditionally. The descriptor/manifest integration follows current main and preserves existing cookie-source conventions, but a maintainer must still approve the unsupported Notion API and persisted-session boundary after a live comparison.

AGENTS.md: found and applied where relevant.

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

Labels

Label justifications:

  • P2: This is a bounded new provider integration with real user value, but it is default-disabled and has no demonstrated urgent regression.
  • merge-risk: 🚨 security-boundary: Merging expands CodexBar’s credential boundary to import and persist a reusable Notion browser-session token for an unsupported web API.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The current-head comment provides redacted terminal proof of a real Notion Business workspace: manual browser import, successful later background refresh from the persisted session after a cache miss, live allowance output, and 0600 file permissions.
  • proof: sufficient: Contributor real behavior proof is sufficient. The current-head comment provides redacted terminal proof of a real Notion Business workspace: manual browser import, successful later background refresh from the persisted session after a cache miss, live allowance output, and 0600 file permissions.

Evidence

What I checked:

  • Provider registration follows current architecture: The Notion descriptor and implementation are added to the manifest-based registration points introduced on current main, rather than restoring the former parallel switches. (Sources/CodexBarCore/Providers/ProviderManifest.swift:74, a557d0229f90)
  • Cookie boundary is deliberately constrained: The proposed descriptor sets Chrome-only automatic import, and the fetcher sends the imported or manual session only to the fixed app.notion.com base URL; its captured-header allowlist intentionally excludes the workspace-pinning header. (Sources/CodexBarCore/Providers/Notion/NotionProviderDescriptor.swift:29, dfac3f718262)
  • Persisted fallback is owner-only and atomic: The proposed session store persists only token_v2 through the existing credential writer; that writer creates a 0600 staged file before writing, syncs it, and atomically renames it into place. The PR adds permission and round-trip tests. (Sources/CodexBarCore/Providers/Notion/NotionSessionStore.swift:43, dfac3f718262)
  • Prior review findings are fixed at the reviewed head: The earlier Chrome-only import and bare-token normalization findings are addressed by the current descriptor/import path and request-context normalization; no remaining line-level defect was identified in this re-review. (Sources/CodexBarCore/Providers/Notion/NotionUsageFetcher.swift:170, dfac3f718262)
  • After-fix behavior proof is present: The contributor's August 3, 2026 terminal transcript shows a user-initiated cookie import followed by a background CLI refresh that succeeds after a shared cookie-cache miss using the persisted session, plus a 0600 session-file mode check. (dfac3f718262)
  • Current release does not contain this feature: Current main is at eddf0b4 and CHANGELOG.md identifies 0.46.1 as unreleased and v0.46.0 as the latest shipped release; this open branch is not a current-main implementation to close as already released. (CHANGELOG.md:3, eddf0b4a1809)

Likely related people:

  • Peter Steinberger: Introduced the manifest-based provider registration architecture on current main and directly engaged on this PR's rebase and final live-verification gate. (role: recent provider-platform contributor; confidence: high; commits: a557d0229f90; files: Sources/CodexBarCore/Providers/ProviderManifest.swift, Sources/CodexBar/Providers/Shared/ProviderImplementationManifest.swift)
  • Petr Kratochvíl: Recent history connects this contributor to menu-bar pace behavior that the PR extends and corrects for calendar-backed allowance windows. (role: recent pace-surface contributor; confidence: medium; commits: f93e3ed460ad, 26ce38d416ff; files: Sources/CodexBar/UsageStore+HistoricalPace.swift, Sources/CodexBar/MenuCardView+ModelHelpers.swift, Sources/CodexBarCLI/CLIRenderer.swift)

Rank-up moves

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

  • Have an owner compare Rolling and Monthly values against Notion’s usage page and verify a later background refresh on the freshly built bundle.

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 (6 earlier review cycles)
  • reviewed 2026-08-01T17:05:08.824Z sha 8c59645 :: needs real behavior proof before merge. :: [P1] Restrict automatic Notion cookie imports to Chrome | [P2] Prefix a pasted token with its cookie name
  • reviewed 2026-08-01T20:12:39.680Z sha 8c59645 :: needs real behavior proof before merge. :: [P1] Restrict automatic Notion cookie imports to Chrome | [P2] Prefix a pasted token with its cookie name
  • reviewed 2026-08-03T01:59:36.710Z sha 138f053 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-03T02:10:25.727Z sha a7dc716 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-03T10:22:43.401Z sha 6a7e61e :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-03T10:29:40.647Z sha dfac3f7 :: needs maintainer review before merge. :: none

@steipete
steipete force-pushed the feat/notion-ai-provider branch from 8c59645 to 138f053 Compare August 3, 2026 01:56
@steipete

steipete commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Maintainer update pushed; this PR is not merged.

The branch is rebased onto current main (fd7857a065) and now includes:

  • Chrome-only automatic cookie import by default, while preserving an explicit browser-order override in the importer.
  • Correct normalization of a pasted bare token_v2 value to token_v2=<value>.
  • A validated Notion session fallback that persists only token_v2 through CredentialFileWriter (atomic owner-only 0600 file), alongside the shared cookie cache.
  • Explicit documentation that this relies on unsupported internal Notion /api/v3 endpoints that may change or break without notice.
  • Fixture-backed parsing for getSpaces and getCreditRateLimitStatus, plus session-file permission/round-trip tests.
  • The requested ### Added changelog entry thanking @n0ah37.

Proof on the pushed head a7dc7163f0:

  • make check — passed (SwiftFormat, SwiftLint, docs/locales, generated parser hash, packaging checks).
  • swift test --filter Notion — 24 tests passed across the Notion usage and session-store suites.
  • make test — all 790 selections passed in 66/66 groups; zero retries and zero timeouts.
  • Final branch autoreview against origin/main — clean, no accepted/actionable findings (Codex Sol, high reasoning, 0.99 confidence).

No live Notion request was made during this repair. Maintainer live verification is the final gate: Peter should enable Notion AI, explicitly refresh/import the Chrome session, confirm Rolling and Monthly values match Notion’s Settings → Notion AI → Usage page, then confirm a later background refresh reuses the persisted session without another browser import. Once that is checked, the provider is ready for maintainer merge review.

@steipete
steipete force-pushed the feat/notion-ai-provider branch from 138f053 to a7dc716 Compare August 3, 2026 02:06
n0ah37 and others added 4 commits August 3, 2026 17:21
Tracks the two usage-allowance windows Notion shows in Settings > Notion AI >
Usage: the rolling 6-hour window and the billing-period window. Notion begins
enforcing the allowance on 2026-08-03; before then the same endpoint already
returns real numbers with "enforcement": "preview".

Reads two cookie-authenticated endpoints on app.notion.com that the Notion web
app itself calls: getSpaces for account identity and workspace plans, and
getCreditRateLimitStatus for the allowance. Only Business and Enterprise
workspaces carry one; anything else reports not_applicable as a clear error
rather than an empty gauge.

The imported cookie header is persisted through CookieHeaderCache. Chromium
cookie reads are gated to user-initiated refreshes to avoid a Keychain prompt,
so without a cached header the provider fails on every background refresh and
reports "no cookies found" while the session is valid. Cookies are also
de-duplicated by name across the Notion domains, since a stale token_v2 left on
the legacy notion.so alongside the live one would otherwise send both.

Every field of the rate-limit response is optional, so an unrelated 200 body
decodes cleanly into an all-nil status. The parser rejects a payload carrying
neither window, and a window with no usable limit is omitted rather than
reported as 0% used.

Adding the .notion debug-log case tipped UsageStore.debugLogText past the
cyclomatic-complexity cap, so .openai and .azureopenai are folded into one case;
both already call the same apiKeyDebugLine helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The billing-period window is scored against the real calendar month ending
at its reset rather than a flat 30 days, and the rolling window gets the
session-pace treatment on both the card and in `codexbar usage`.

Notion reports only `periodEndMs` for the billing period, so the snapshot
carries the shared monthly sentinel and the descriptor declares
`.calendarMonthResetWindow`. Resolution then substitutes the true cycle
length. Three paths scored these windows without resolving first --
`UsageStore.weeklyPace` (menu-bar pace token, "runs out" text, predictive
pace warnings) and `resetWindowPaceDetail` when handed a precomputed pace
-- so a February cycle read 6% expected at 0% used and a 31-day cycle lost
its pace token for a day. Both now resolve, which also fixes the eight
other providers that carry the same sentinel.

The CLI ignored the Notion settings snapshot entirely, so Workspace ID, a
manual cookie header, and the `off` source had no effect on `codexbar`.

Also drops a rolling window length that parses to exactly the monthly
sentinel (`30d`, `720h`, `43200m`), which would otherwise be resolved as a
calendar cycle ending hours from now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@n0ah37
n0ah37 force-pushed the feat/notion-ai-provider branch from a7dc716 to 6a7e61e Compare August 3, 2026 10:18
@n0ah37

n0ah37 commented Aug 3, 2026

Copy link
Copy Markdown
Author

Rebased onto current main (a557d022) and added the pace/CLI work that belongs with this feature.

Rebase. #2606 ("derive provider registrations from descriptors") landed in between and rewrote the machinery this provider hooks into. Notion is now registered through ProviderManifest and ProviderImplementationManifest, its icon style is .init(provider: .notion), and it declares widgetSelectable: false to preserve the "not yet supported in widgets" behavior the old hand-written switch expressed. Nothing else about the provider changed.

New in this push. Both allowance bars now carry an expected-usage estimate:

  • The billing-period window is scored against the real calendar month ending at its reset, not a flat 30 days. Notion reports only periodEndMs, so the snapshot carries the shared monthly sentinel and the descriptor declares .calendarMonthResetWindow; resolution substitutes the true cycle length. Three paths scored these windows without resolving first — UsageStore.weeklyPace (menu-bar pace token, "runs out" text, predictive pace warnings) and resetWindowPaceDetail when handed a precomputed pace — so a February cycle read 6% expected at 0% used, and a 31-day cycle lost its pace token for a day. Both now resolve, which fixes the same latent bug for the eight other providers carrying that sentinel (Amp, MiMo, StepFun, Doubao, Alibaba ×2, OpenCode Go).
  • The rolling window is paced as a session window on the card and in the CLI; its 6-hour length previously fell outside the CLI's 300-minute session ceiling, so the two surfaces disagreed.
  • codexbar now honors the Notion settings snapshot at all — Workspace ID, manual cookie header, and the off source had no effect on the CLI before.
  • A rolling length that parses to exactly the monthly sentinel (30d, 720h, 43200m) is dropped, since it would otherwise be resolved as a calendar cycle ending hours from now.

Head: dfac3f71 · make check clean (0 violations, 1731 files) · make test 798 selections, 67/67 groups, 0 failures, 0 retries, 0 timeouts.

Real behavior proof

Captured on this head against a real Notion Business workspace. Account, workspace name, space id, and home path redacted; the allowance figures are live and matched Settings → Notion AI → Usage.

1. Manual import — user-initiated, the only context allowed to read a Chromium cookie store:

$ CodexBarCLI cookie refresh --provider notion --allow-keychain-prompt
notion: ✅ Browser cookie refreshed.

2. Later background refreshcodexbar usage runs outside .userInitiated, so it cannot reach the browser store at all:

$ CodexBarCLI usage --provider notion --verbose --log-level verbose
debug com.steipete.codexbar.cookie-cache: provider=notion   Cookie cache miss
debug com.steipete.codexbar.notion:      [notion] Using stored session from <browser> Default
debug com.steipete.codexbar.notion:      [notion] Cookie names: token_v2
debug com.steipete.codexbar.notion:      [notion] Using workspace <redacted> (<redacted>)
[notion] fetch strategies:
  - notion.web (web) available
== Notion AI (web) ==
Rolling: 62% left [=======-----]
Pace: 23% in deficit | Expected 16% used | Projected empty in 1h 31m
Resets in 5h 4m
Monthly: 60% left [=======-----]
Pace: On pace | Expected 41% used | Lasts until reset
Resets in 18d 9h
Account: <redacted>
Plan: Business

The first two log lines are the specific claim: the shared cookie cache missed, and the fetch still succeeded from the token_v2 persisted by NotionSessionStore — so this exercises the CredentialFileWriter fallback added in this PR rather than the pre-existing cookie cache. That branch is gated on ProviderInteractionContext.current != .userInitiated, so it is reachable only from a non-user-initiated refresh.

The Monthly pace line also demonstrates the calendar-cycle fix end to end: with the reset 18d 9h away in a 31-day billing cycle, Expected 41% used is 12.6/31. Scoring the same window against a flat 30 days would report 39%.

3. Persisted session is owner-only:

$ stat -f "%Sp  %z bytes  %N" ~/Library/Application\ Support/CodexBar/notion-session.json
-rw-------  603 bytes  /Users/<redacted>/Library/Application Support/CodexBar/notion-session.json

Both earlier Codex findings are present on this head: browserCookieOrder: ProviderBrowserCookieDefaults.chromeOnlyImportOrder (P1) and the bare-token_v2 normalization in requestContext(from:) (P2).

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed 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 3, 2026
@n0ah37

n0ah37 commented Aug 3, 2026

Copy link
Copy Markdown
Author

@clawsweeper re-review

Proof from the current head dfac3f71 is in the comment above, and the PR body now points at it. The branch is also rebased onto current main (past #2606), so the earlier conflict is gone.

@clawsweeper

clawsweeper Bot commented Aug 3, 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 repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@steipete
steipete merged commit 8393fc8 into steipete:main Aug 3, 2026
1 check passed
Finesssee added a commit to nesszer/Win-CodexBar that referenced this pull request Aug 4, 2026
* Port upstream 0.47.0: Low Power Mode (steipete#2518)

* Port upstream 0.47.0: Notion AI provider (steipete#2552)

* Port upstream 0.47.0: codexbar hooks watch (steipete#2536)

* Port upstream 0.47.0: Cursor optional on-demand usage (steipete#2338)

* Port upstream 0.47.0: Command Code persist browser sessions (steipete#2564)

* Port upstream 0.47.0: OpenCode Go idle WAL read (steipete#2544)

* Port upstream 0.47.0: real-calendar monthly pace (steipete#2552)

* Port upstream 0.47.0: XAI provider

* Port upstream 0.47.0: verified z.ai/Kimi/Grok window durations (steipete#2431)

* Document upstream port procedure

* Simplify port helpers: drop bespoke trait, unused param, duplicated aggregation
proxynico added a commit to proxynico/CodexBar that referenced this pull request Aug 14, 2026
* fix: use in-process libproc instead of ps/lsof scans (#2599)

Replace repeated macOS process-inspection subprocesses with scoped libproc and sysctl calls while preserving the Linux ps, lsof, and /proc paths. Add defensive PROCARGS2 parsing and self-process integration coverage to harden #2267.

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* fix: size provider menus to their content (#2602)

* build: refresh widget dependency lockfile (#2603)

* build: sync widget Package.resolved with root (SweetCookieKit 0.5.1) (#2604)

Root Package.resolved moved ahead of the widget workspace copy, so
Scripts/package_app.sh release failed at the widget step with an
out-of-date resolved file error. The script's failure handling was
verified correct (non-zero exit, no stale bundle produced).

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* refactor: derive provider registrations from descriptors (#2606)

* refactor: consolidate provider bootstrap manifests

* refactor: derive provider icon styles from descriptors

* refactor: derive provider ancillary registrations

* refactor: derive widget provider metadata

* test: enforce provider widget metadata sync

* docs: simplify provider authoring workflow

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* fix: make iCloud sync survive first contact with a real fleet (#2608)

Four fixes from live multi-Mac testing:
- CKRecord rebase crash: allKeys() includes encrypted field names; routing
  them through the plain subscript threw NSInvalidArgumentException and
  crash-looped every receiver applying fetched records (regression test).
- Fetch-before-push on first sync: a fresh device now applies fleet state
  before composing pushes, so its editCount-1 records can't win conflict
  ties and clobber the fleet.
- Persist remote applies: applyExternalConfig skips disk writes by design
  (reload path); sync applies now schedulePersistConfig so config.json,
  the CLI, and the next launch see the merged result.
- Dirty-set push gating: startup no longer wholesale-uploads local config
  (a relaunched stale Mac degraded the richest Mac's config twice in
  testing). Providers push only when locally edited (persisted dirty set,
  cleared on save success), with an empty-fleet bootstrap seeding path.

Verified live: three-Mac convergence (golden profile incl. E2E-encrypted
secrets propagated to two receivers; source unchanged; fleet devices and
snapshots visible everywhere; no-account Mac degrades gracefully).

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(alibaba-token-plan): personal requests rejected with Workspace.NotAuthorised (hardcoded switchAgent) (#2533)

* fix(alibaba-token-plan): personal requests rejected with Workspace.NotAuthorised

The Personal/Solo path hardcoded `switchAgent: 1_233_135` in the
`cornerstoneParam` request body. The gateway binds that value to a specific
account's workspace, so for any other account the call is rejected with
`BailianGateway.Workspace.NotAuthorised` even though the outer envelope
claims `code: "200"`. Omitting the field lets the gateway resolve the
session's default workspace.

Also:
- Resolve `sec_token` best-effort for personal requests (the browser always
  sends it; some accounts are rejected without it) and append it to the body
  when available, mirroring the Teams path.
- `throwIfErrorPayload` now reads `errorCode`/`errorMsg` from the nested
  frame that carries `success: false`, so the real gateway error is surfaced
  instead of a misleading "API error: 200". Authorization-family errors map
  to `invalidCredentials` so the UI prompts for re-authentication.

Fixes #2500. Verified live against the mainland Personal/Solo API and covered
by new regression tests.

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

* fix: preserve Alibaba personal sessions

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* Add Notion AI provider (#2552)

* Add Notion AI provider

Tracks the two usage-allowance windows Notion shows in Settings > Notion AI >
Usage: the rolling 6-hour window and the billing-period window. Notion begins
enforcing the allowance on 2026-08-03; before then the same endpoint already
returns real numbers with "enforcement": "preview".

Reads two cookie-authenticated endpoints on app.notion.com that the Notion web
app itself calls: getSpaces for account identity and workspace plans, and
getCreditRateLimitStatus for the allowance. Only Business and Enterprise
workspaces carry one; anything else reports not_applicable as a clear error
rather than an empty gauge.

The imported cookie header is persisted through CookieHeaderCache. Chromium
cookie reads are gated to user-initiated refreshes to avoid a Keychain prompt,
so without a cached header the provider fails on every background refresh and
reports "no cookies found" while the session is valid. Cookies are also
de-duplicated by name across the Notion domains, since a stale token_v2 left on
the legacy notion.so alongside the live one would otherwise send both.

Every field of the rate-limit response is optional, so an unrelated 200 body
decodes cleanly into an all-nil status. The parser rejects a payload carrying
neither window, and a window with no usable limit is omitted rather than
reported as 0% used.

Adding the .notion debug-log case tipped UsageStore.debugLogText past the
cyclomatic-complexity cap, so .openai and .azureopenai are folded into one case;
both already call the same apiKeyDebugLine helper.

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

* fix(notion): harden cookie session handling

* test: isolate widget snapshot persistence

* feat(notion): pace both allowance bars and honor CLI provider settings

The billing-period window is scored against the real calendar month ending
at its reset rather than a flat 30 days, and the rolling window gets the
session-pace treatment on both the card and in `codexbar usage`.

Notion reports only `periodEndMs` for the billing period, so the snapshot
carries the shared monthly sentinel and the descriptor declares
`.calendarMonthResetWindow`. Resolution then substitutes the true cycle
length. Three paths scored these windows without resolving first --
`UsageStore.weeklyPace` (menu-bar pace token, "runs out" text, predictive
pace warnings) and `resetWindowPaceDetail` when handed a precomputed pace
-- so a February cycle read 6% expected at 0% used and a 31-day cycle lost
its pace token for a day. Both now resolve, which also fixes the eight
other providers that carry the same sentinel.

The CLI ignored the Notion settings snapshot entirely, so Workspace ID, a
manual cookie header, and the `off` source had no effect on `codexbar`.

Also drops a rolling window length that parses to exactly the monthly
sentinel (`30d`, `720h`, `43200m`), which would otherwise be resolved as a
calendar cycle ending hours from now.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* release: 0.47.0

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

* fix: make widget ProviderChoice display metadata a static literal

The AppIntents metadata processor in current Xcode rejects computed
caseDisplayRepresentations (must be a literal, exhaustive dictionary),
which broke the 0.47.0 release build. Titles are pinned to the
descriptor registry by WidgetProviderChoiceTests so providers cannot
drift silently.

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

* docs: update appcast for 0.47.0

* docs: open 0.47.1 Unreleased

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

* Add scoped-weekly (Fable) percentage as a menu-bar layout token (#2440)

* Add scoped-weekly (Fable) percentage as a menu-bar layout token

Claude's model-scoped weekly carve-outs (the "Fable only" window from #1851)
already show in the dropdown and in quota notifications, but the always-visible
menu bar could only show session, weekly, or automatic. This adds a scoped-weekly
token so the promo-window limit can sit in the bar.

- New PercentWindow.scopedWeekly, shown with an "F" prefix, selectable in the
  layout editor (palette, label, live and representative previews).
- scopedWeeklyWindow resolver matches the claude-weekly-scoped-* id prefix rather
  than a model name; when several are active it shows the most constrained one.
- The scoped-weekly percent is added to the icon-observation signature so a
  scoped-only change refreshes the title, mirroring weekly= and avoiding the
  stale-title problem from #2300 and #2299.
- Absent window renders the standard "–" placeholder; editor label localized.

Addresses #2360.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Address review: label scoped-weekly token by active model

Codex flagged that the token hard-coded "F" / "Fable only" while the resolver
accepts any claude-weekly-scoped-* window and picks the most constrained, so a
non-Fable window could be shown under the wrong model label.

- scopedWeeklyNamedWindow now returns the NamedRateWindow.
- The .scopedWeekly token derives its prefix from the active window's title
  (first letter) and its accessibility text from the full title, instead of a
  fixed "F" / "Fable only".
- Carry the title through MenuBarLayoutRenderData and include it in the
  icon-observation signature, so a model change refreshes the title.
- Resolver test now asserts a non-Fable most-constrained window carries its title.

The editor palette label stays "Fable %" (a proper noun that reads the same across
locales); a generic name is left as a naming choice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add menu bar pace layout tokens

The 0.45 layout editor replaced the old Percent/Pace/Both display modes,
but no token exposes the signed pace delta the Both mode used to render.
`Runs out` is not a substitute: it answers when a window ends, always
estimating from the weekly (or automatic) lane, while pace answers how far
off the sustainable rate usage currently runs.

Add Session/Weekly/Auto pace tokens that mirror the percent tokens' window
selection and reuse the existing `MenuBarDisplayText.paceText` formatting
(`+11%` ahead of the rate, `-8%` behind, `0%` on pace). Each token resolves
pace for its own window, so Weekly pace never borrows the session delta.

Pace needs the store's historical dataset and work-day setting, so it is
resolved upstream like `runsOut` through a shared `menuBarLayoutPaceText`
helper that both the status item and the editor preview call. The existing
3% expected-usage floor in `weeklyPace` still applies, so a token renders
the en-dash placeholder early in a window while its siblings stay visible.

Refs #2534

* Include layout pace tokens in the icon observation signature

Pace values change with the historical dataset, the work-day setting,
and the clock, none of which move the percent fields already hashed by
providerStoreIconObservationSignature. A historicalPaceRevision bump
therefore woke the icon observer but produced an unchanged signature,
so updateIcons() was skipped and a custom pace token kept its stale
value until an unrelated icon change forced a redraw.

Contribute the active layout's pace values to the signature the same
way cost and account tokens already do, gated on the layout actually
containing a pace token. The regression test renders two snapshots with
identical used percents but different resets: without this fix both
signatures were identical.

Refs #2534

* docs: credit menu bar pace contributor

* fix: localize scoped weekly label

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Petr Kratochvíl <krato@krato.cz>
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* Add one-shot dashboard snapshot command (#2499)

* Add one-shot dashboard snapshot command

* Fix dashboard pricing refresh policy

* Honor Cursor source policy in dashboard

* test: dedupe cliExecutableURL helper after branch update

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

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat: JS provider plugin runtime prototype (behind debug flag) (#2617)

* feat: add provider plugin runtime

* feat: convert synthetic venice and crof to JS plugins

* test: prove JS provider parity

* docs: document provider plugin prototype

* fix: satisfy plugin runtime lint checks

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* fix: sync CLI config edits via iCloud (#2618)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* test: make widget snapshot storage hermetic (#2619)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* feat(menu): compact usage detail rows (#2620)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Shun Min Chang <ji394m6y7@gmail.com>

* feat: add China Kimi and GLM quota routing (#2621)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: haoli <haoli@local.dev>

* feat: enrich Kimi monthly usage from desktop (#2622)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: haoli <haoli@local.dev>

* feat: declarative provider detail sections + JS detail providers (#2624)

* feat: add declarative provider detail model

* feat: render declarative provider details

* feat: bridge provider details from JavaScript

* feat: convert detail providers to JavaScript

* style: satisfy provider detail test lint

* fix: align z.ai plugin quota lanes

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* Unify pi and OMP agent sessions (#2626)

* Add live OhMyPi agent session discovery

Detect running OhMyPi harness processes, correlate them with bounded session metadata, and expose normalized sessions through the existing CLI and menu paths.

* Address OhMyPi review correlation gaps

Resolve OhMyPi metadata per live process environment, reject stale records, and isolate directory scan budget so existing provider discovery remains intact. Add focused regression coverage and document the fail-closed behavior.

* Preserve existing session metadata during OhMyPi scans

Give OhMyPi its own bounded directory budget so Codex and Claude correlation remain available, with regression coverage for the shared-provider behavior.

* Preserve legacy session JSON compatibility

Add an explicit v2 session payload for OhMyPi while keeping legacy remote clients decodable, and document and test the negotiated fallback.

* Expose OhMyPi in local session JSON

Keep the documented local --json output complete while preserving the remote v2-first compatibility fallback for older installations. Update CLI help, focused protocol coverage, and session documentation to distinguish current local output from legacy remote responses.

* fix(sessions): preserve legacy JSON compatibility

* Correct session JSON protocol overview

* feat(sessions): unify Pi-family discovery

Co-authored-by: William Mitchell <wdmitchell.uk@gmail.com>

---------

Co-authored-by: William Mitchell <wdmitchell.uk@gmail.com>
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* fix(zoommate): preserve browser cookie scope (#2627)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* Keep cached spend visible while Codex refreshes (#2628)

* fix: show cached spend while refreshing

Co-authored-by: hhh2210 <hzy2210@gmail.com>

* test: add cached spend refresh proof

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: hhh2210 <hzy2210@gmail.com>

* feat: route GLM credentials by region (#2623)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: haoli <haoli@local.dev>

* Define Codex Fast cost as API Fast USD (#2632)

* Centralize Codex priority pricing

* fix: define Codex Fast USD pricing

* test: split pricing expectation math for CI type-checker

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

---------

Co-authored-by: Bryan Font <bfont@me.com>
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix: parse Command Code usage windows (#2630)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Derek Zeng <zengzhuoxi@gmail.com>

* feat: plugin cookie capability and host API extensions

* feat: extend plugin host and convert providers

* fix: align z.ai plugin credential routing

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: introduce ProviderInstanceID identity seam (#2636)

* test: characterize provider instance identity

* feat: add provider instance identity seam

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* feat: user-installed provider plugins with TypeScript support (#2642)

* feat: add user-installed provider plugins

* fix: broker owns plugin HTTP representation headers

Apply broker-owned Accept, Accept-Encoding, and Content-Type after
plugin-supplied headers so a plugin cannot relax the user-plugin
response boundary. Test transport gains configurable response headers.

* build: add reproducible sucrase bundle verification script

Regenerates and verifies Sources/CodexBarCore/Resources/Plugins/
sucrase-3.35.1.min.js from the official npm artifact (sucrase@3.35.1,
esbuild@0.25.8 pinned, IIFE browser bundle). Expected SHA-256
4d997e15b72cbc9ccf6e743c30c6eb48bf4533f6709852367b40766be5eba70b was
independently reproduced by the coordinator from the npm registry;
'check' mode fails closed on any mismatch.

* fix: gate user-plugin registry lookup for non-JavaScriptCore platforms

Linux CLI builds compile CodexBarConfig without the plugin runtime;
unknown plugin config entries are dropped with the existing warning,
matching the documented macOS-only plugin boundary.

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* test: deflake Kimi grace budget timing (#2657)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* test: keep widget-snapshot container I/O out of test runs (#2656)

* test: keep widget-snapshot container I/O out of test runs

AdaptiveRefreshHeuristicsTests (and any suite reaching persistWidgetSnapshot without a save override) could hang forever on macOS 26 hosts: WidgetSnapshotStore.load() opens the real app-group container file and the open() can block indefinitely behind app-data (TCC) gating. Gate persistWidgetSnapshot so test runs only persist when a test explicitly installs _test_widgetSnapshotSaveOverride, and pin the behavior with regression tests.

* test: open widget-snapshot gate for injected snapshot URLs

Main's plugin series added a widgetSnapshotURL injection seam that promotion tests rely on; the gate now admits it alongside the save override and replaces the duplicate inline DEBUG guard.

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* feat: add shared-cache token activity heatmap (#2625)

* feat: add standalone token activity heatmap

* fix: preserve token activity coverage

* fix: complete activity heatmap navigation

* fix: expose daily activity accessibility

* fix: publish daily accessibility semantics

* fix: expose dates in activity accessibility

* fix: expose weekly activity accessibility

* feat: derive token activity from shared cache

---------

Co-authored-by: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* Expose Codex scan completeness in cost JSON (#2520)

* Expose provider-native cost scans

* feat(cli): expose Codex cost scan completeness

Co-authored-by: NickGuAI <yu.gu.columbia@gmail.com>

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: refresh Plugins pane visuals to match sibling settings panes (#2662)

* feat: resizable settings sidebar with persisted width (#2660)

* feat: resizable settings sidebar with persisted width

Replace the fixed 260pt settings sidebar with a drag-resizable one
(200-380pt, persisted via AppStorage). The divider stays the original
SwiftUI hairline; an input-only AppKit strip overlaid on the detail
pane's leading edge handles dragging, so the HStack layout is untouched.
Hover cursor via .pointerStyle(.columnResize) on macOS 15+ plus an
explicit-rect tracking area (visibleRect spans the whole window under
NSHostingView, so .inVisibleRect never fired).

* fix: hold the sidebar resize cursor with a cursor rect

The hover cursor was set imperatively on mouse enter/move, which SwiftUI
undid on its next render — the settings panes re-render continuously, so
the resize cursor only flashed. Cursor rects are declarative: AppKit
re-asks the view on every invalidation, so the cursor holds.

Measured on a probe harness hosting this view under a 20Hz re-render
loop, sampling NSCursor.currentSystem at 25Hz while an external driver
moved the pointer: imperative-only gave 0% resize on hover and 40%
during drag; with cursor rects, two clean transitions (arrow -> resize
on entry, back on exit) and 100% held across hover and drag.

* style: fix SwiftFormat violations in SidebarResizeHandle

Mechanical ./Scripts/lint.sh format output; #2660 landed with docComments
and wrap-body violations that broke CI on main.

* Wait for OpenCode Go Zen balance in CLI usage reads (#2583)

* Wait for OpenCode Go Zen balance in CLI usage reads

* fix: bound OpenCode Go CLI balance lookup

* Keep OpenCode Go Zen wait scoped to usage reads

* Measure OpenCode Go Zen wait from task start

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* OpenCode Go: per-model cost breakdown by day (#2649)

* feat: per-model cost breakdown for OpenCode Go's daily cost history

Extract each local assistant message's modelID from opencode.db (the
real model behind the constant opencode-go Zen-proxy providerID) and
group cost/request counts by (day, model) instead of just by day, so
the shared Cost history chart shows a per-model breakdown for
OpenCode Go the same way it already does for Claude/Codex. Rows with
no modelID fall back to an "unknown" bucket instead of being dropped.

* fix: use lowercase docs/claude.md link to pass case-sensitive CI lint

macOS resolves docs/CLAUDE.md to the same file as docs/claude.md, but
the Linux lint runner's case-sensitive filesystem doesn't, failing the
documentation-link check.

* fix: normalize the trimmed model id when grouping OpenCode Go costs

The grouping key used the untrimmed modelID even though emptiness was
checked against the trimmed value, so a model id with incidental
leading/trailing whitespace would form its own bucket instead of
merging with the clean value for the same model. Group by the trimmed
value consistently, and add regression tests for whitespace-only and
whitespace-padded model ids.

* Fix ETXTBSY race in claude-swap CLI card test (#2644)

* Fix ETXTBSY race in claude-swap CLI card test

The test wrote a shell script, set mode 0755, and executed it immediately.
Under swift test --parallel a concurrent fork inherits the still-open write
descriptor, so execve fails with ETXTBSY and the launch reports Cocoa 256.

Write the script body as data and execute a checked-in trampoline that reads
it, so execve only ever touches a file no test process has written.

Measured in swift:6.3.3-noble on arm64: 112 failures in 600 attempts with the
old shape, 0 in 600 with the trampoline.

* Apply ETXTBSY fix to the Linux test target

The first commit only touched Tests/CodexBarTests, which Package.swift builds
on macOS alone. The Linux job compiles CodexBarLinuxTests from TestsLinux, so
the race remained in the target that actually flaked.

Convert all three TestsLinux sites through a shared FakeExecutable helper, and
keep the macOS mirror in step.

Verified on Linux arm64 in swift:6.3.3-noble: 355 tests in 53 suites pass.

* Pin Perplexity promo expiry formatter to en_US_POSIX (#2651)

* Pin Perplexity promo expiry formatter to en_US_POSIX

The formatter set dateFormat but not locale, so MMM rendered in the user's
language inside an otherwise English string and diverged from the JS plugin
projection. Matches the en_US_POSIX pinning already used by every other
provider formatter.

Refs #868

* Assert the Perplexity promo expiry locale pin in tests

Parity coverage only trips on a non-English host, so an English runner
passes both before and after the regression. Add assertions that hold on
every host: the formatter keeps its en_US_POSIX locale, and the rendered
month stays ASCII English.

* Move the promo expiry fixture off the UTC month boundary

The formatter keeps the host time zone, so a midnight-UTC January instant
renders as Dec 31 west of UTC and failed the month assertion there even
with the locale pin correct.

* Decode Copilot credits_used for token-billed seats (#2593) (#2613)

* Decode Copilot token-billing credits used

* Cover credits used counter in token-billing card regression

* Keep Copilot credits counter accessible through snapshots

* Preserve Copilot credits counter across quota fallback

* Expose Copilot credits counter in diagnostics

* Keep credits counter on zero entitlement quota fallback

* fix: keep Codex fork catch-up progress across appends (#2648)

* fix: keep Codex fork catch-up progress across appends

* Keep subagent fork appends on full rescan

* Preserve zero-work subagent retries

* Report unreadable Claude OAuth refresh as terminal (#2650)

* Report unreadable Claude OAuth refresh as terminal

When the delegated Claude CLI touch completes but the Keychain fingerprint
does not move, the attempt reported a retryable failure. With foreign
Keychain reads disabled in release and no credentials file for the profile,
that state cannot be retried out of: the refreshed credential is
unreadable no matter how often the touch runs.

Add Outcome.unreadableAfterRefresh for that case, back off on the long
cooldown instead of the short one, and drop the 'run claude login, then
retry' advice that cannot help. The touch still runs first, so older Claude
Code versions that write the credentials file keep recovering.

Refs #2634

* Keep the delegated refresh Outcome contract public-stable

CodexBarCore exports Outcome through its library product, so the added
case would break downstream exhaustive switches on source update. Move the
unreadable verdict onto an internal AttemptResult alongside the outcome and
restore the original public cases; the fetcher reads the detail through
attemptDetailed.

* Cover retryable touch failure in unreadable configuration

The touch-error path staying retryable (short cooldown, no terminal
verdict) is deliberate: the error may be transient, and on older Claude
Code a retried touch can still create the credentials file. Document
that at the decision site and lock it in with a regression test so the
SessionError variant from #2634 has explicit coverage.

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* test: stabilize PTY overflow proof (#2664)

* Classify Codex rate windows by duration (5h/weekly/30-day) (#2600)

* Classify Codex rate windows by duration

* Keep monthly Codex history visible in the chart

* Suppress session pace for non-session Codex windows

* Preserve monthly Codex windows in reset backfill and keep fallback session pace

- Reset backfill rebuilt raw slots only from the session/weekly lanes, so a
  43,200-minute primary (now classified .monthly) was dropped or overwritten
  by a stale cached session window whenever a trusted backfill baseline
  existed. Preserve monthly-classified slot windows in place and backfill
  their reset from a matching cached monthly window.
- The session-pace guard rejected every non-300-minute Codex window, removing
  existing pace for fallback session durations (e.g. 540 minutes or nil).
  Suppress pace only for windows that classify into the weekly/monthly lanes,
  and let the central guard drive the card instead of exact-duration gates.
- Move the Codex reset-backfill helpers into their own file (file-length lint).
- Regression tests for both paths; they fail without the fixes.

* Retain monthly reset baselines in codex backfill merge

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* Retain priced Codex models when Auto Review is unpriced (#2643)

* Retain partial Codex model rows

* Use safe Codex dashboard test fixture

* Fail closed for malformed Codex model costs

* Add regression test for fully unpriced Codex routing history

* Extract model breakdown helpers to satisfy type body length

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>

* Bound Codex cost cache persistence size (#2637) (#2646)

* Bound Codex cost cache persistence size

* Preserve resumable and fork-dependent cache entries

* Prune cache against requested window

* Prune candidate artifacts over byte budget

* Scope cache guards and preserve active sessions

* Keep cache loadable under byte budget

* Mark trimmed caches for catch-up and prune discovery

* Bound sole oversized entries and reset discovery cursors

* Mark stripped entries incomplete

* Preserve full report and live fork protection during trim

* Compact protected fork parents over budget

* Enforce byte cap after underestimated encodes

* Exclude lineage-only parents from fork protection

* Re-encode after forced prune and bound lookback state

* Use report window for catch-up and exclude lineage parents

* Compact parents required by kept trim survivors

* Prune orphaned discovery mappings under budget

* Preserve lookback work and align report bounds

* fix: lower cost-cache load cap and never persist a refusable artifact

The 1 GiB load cap still let a 256 MiB-1 GiB legacy artifact be decoded
in one shot, which is exactly the MALLOC_LARGE multi-GiB spike #2637
traced. Since save now bounds Codex artifacts to 256 MiB, anything
meaningfully above the budget is legacy and cheaper to rebuild bounded;
drop the cap to 320 MiB (budget + enforcement slack).

Also make save uphold the loader's contract: when budget enforcement
cannot shrink the payload below the load cap (unstrippable resume or
buffered state), remove the artifact instead of writing one that every
launch would decode just to refuse.

* Preserve recursive lookback roots and share ID capacity

* Keep legacy lookback roots in the scan queue

* Preserve complete report across repeated trims

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* docs: changelog for triage merge batch (#2649, #2613, #2600, #2646, #2648, #2650, #2643, #2651)

* Use OpenRouter server remaining for key limit meter (#2612)

* Use OpenRouter server remaining for key limit meter

* Treat negative OpenRouter remaining as exhausted quota

* Align JS OpenRouter meter with Swift quota path

* Clamp JS OpenRouter server remaining to key limit

Match the Swift quota path's inclusive [0, keyLimit] clamp so a server
remaining above the configured limit renders 0% used instead of
suppressing the meter. Adds an above-limit Swift/JS parity fixture.

* Validate selected OpenRouter fallback quota value

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>

* docs: changelog for OpenRouter key-limit fix (#2612)

* build: sync widget-extension Package.resolved to SweetCookieKit 0.5.2

The root package moved to SweetCookieKit 0.5.2 but the widget project's
pinned resolved file lagged at 0.5.1, breaking Scripts/package_app.sh
(xcodebuild runs with automatic resolution disabled). CI does not build
the widget xcodeproj, so this only surfaced in local packaging.

* feat: localize Plugins pane (#2663)

* feat: localize Plugins pane

* docs: note Plugins pane localizations

* refactor: migrate provider payloads onto declarative details model (#2658)

* refactor: migrate provider details batch one

* refactor: migrate provider details batch two

* refactor: migrate provider details batch three

* fix: preserve provider detail rendering parity

* fix: migrate Copilot credits into provider details

* fix: pin Poe details to UTC

Poe timestamps are UTC and the daily buckets already used it, but the
recent-activity labels and Today bucket used the local zone in both the
Swift and JS paths, flaking the golden on non-Pacific runners. Golden
proven invariant under TZ=UTC, America/Los_Angeles, Asia/Tokyo.

* fix: normalize OpenRouter reset detail

* test: pin Poe menu fixture to UTC

* fix: preserve OpenRouter key details

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: decompose provider settings snapshot into provider-owned sections (#2659)

* refactor: decompose provider settings snapshots

* fix: restore empty provider settings factory

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: namespace provider-specific config fields into provider folders (#2661)

* test: characterize provider config JSON bytes

* refactor: namespace provider config fields

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* test: deflake refresh coalescing cleanup (#2672)

* fix: bound widget-snapshot file I/O with timeout and circuit breaker (#2673)

On macOS 26, opening the app-group snapshot can wedge indefinitely behind container or TCC I/O. Run snapshot reads and writes on bounded detached threads, then stop further process-local container access after the first timeout.

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* fix: stop infinite Overview flicker from agent-session rescan feedback loop (#2652) (#2674)

Hovering between provider chart submenus on the Overview tab with Agent
Sessions and Cost Summary enabled could flicker forever. Every menu open
(including each hover-opened hosted chart submenu) kicked off an
agent-session rescan whose completion unconditionally invalidated all
menus and rebuilt the tracked parent in place. Overview parents cannot
smart-update, so the structural rebuild replaced the hovered row and
force-closed its submenu; the reopen triggered another rescan, closing
the loop.

Three bounded changes break it:
- AgentSessionsStore no longer publishes rescans that reproduce the
  current local/remote session content.
- menuWillOpen triggers the session rescan (and menu-open note) only for
  root menus, not hover-opened submenus.
- Session updates invalidate menus with deferred tracked-parent rebuild
  and stale-content preservation, matching the store-observation path.

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: provider-owned credential adapters (#2676)

* test: characterize provider credentials

* refactor: add provider credential adapters

* refactor: register provider cookie settings

* refactor: move credential policy to providers

* fix: qualify static credential adapter

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: cut over Crof and Venice plugins (#2677)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* feat: classified errors, deadlines, and overrides for JS plugins; cut over four more providers (#2678)

* feat: classify plugin fetch failures

* feat: add plugin request deadlines

* feat: support plugin request overrides

* refactor: cut over OpenRouter plugin

* feat: map plugin data confidence

* refactor: cut over ClawRouter plugin

* refactor: cut over Deepgram plugin

* refactor: cut over sub2api plugin

* style: satisfy plugin cutover lint

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* test: accept GMT/UTC alias in sub2api timezone golden (#2680)

* test: accept GMT/UTC alias in sub2api timezone golden

Foundation reports GMT where JS Intl reports UTC on UTC-configured
machines; the plugin sends the real current zone via Intl. Proven under
TZ=UTC, America/Los_Angeles, Asia/Tokyo. Unbreaks main CI after the
#2678 admin-merge left this red on UTC runners.

* style: satisfy redundant-type lint in sub2api golden

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: derive the remaining per-provider bookkeeping from descriptors (#2682)

* refactor: derive widget colors from provider branding

* refactor: generate provider manifests from one registry

* refactor: colocate provider cookie import priorities

* refactor: move share plan labels into descriptors

* refactor: move debug defaults into provider descriptors

* refactor: derive debug pane provider curation

* refactor: derive provider presentation capabilities

* refactor: unify standard provider config bindings

* style: remove stale lint suppression

* fix: use grep instead of rg in manifest regeneration script

CI lint runners do not have ripgrep; grep -rlE --include is portable and
matches the same files.

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: unify CLI onto descriptor-owned provider capabilities (#2685)

* test: characterize CLI provider policy output

* test: exempt CLI JSON golden from body limit

* test: scope CLI golden lint exemption

* refactor: derive CLI rendering from provider descriptors

* refactor: derive CLI source availability from descriptors

* refactor: finish descriptor-owned CLI credentials

* refactor: derive plan history policy from descriptors

* refactor: document CLI provider-specific boundaries

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: derive remaining provider registries and correct authoring docs (#2686)

* refactor: derive config validation capabilities

* refactor: derive provider log categories

* refactor: derive menu metric capabilities

* build: derive provider manifest order

* fix: preserve provider bootstrap order

* test: derive credential gatekeeper coverage

* docs: document provider registration truth

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>

* refactor: move usage presentation policy into provider descriptors (#2688)

* test: pin provider presentation policy

* refactor: move usage window policy into descriptors

* refactor: move usage menu policy into descriptors

---------

Co-authored-by: Peter Steinberger <steipete@clawstudio.local>

* refactor: derive or justify every remaining provider special case (#2691)

* refactor: derive settings provider special cases

* refactor: justify usage store provider state

* refactor: derive controller provider policy

* refactor: derive cli provider capabilities

* refactor: justify scanner provider ownership

* test: gate provider special case clusters

* build: refresh codex parser hash

* style: satisfy special case gate lint

* style: accept cli cards command size

---------

Co-authored-by: Peter Steinberger <steipete@clawstudio.local>

* test: harden the provider special-case gatekeeper and derive its findings (#2693)

* test: harden provider architecture gatekeeper

* refactor: derive provider presentation policies

* test: pin provider dispatch constructs

* test: document gatekeeper catalog lint scope

---------

Co-authored-by: Peter Steinberger <steipete@clawstudio.local>

* refactor: close remaining provider-architecture gaps and declare gatekeeper scope (#2694)

* refactor: close provider architecture gaps

* refactor: triage loose provider references

* test: harden provider architecture gatekeeper

* docs: declare gatekeeper threat model

---------

Co-authored-by: Peter Steinberger <steipete@clawstudio.local>

* refactor: close provider architecture gaps (#2698)

Co-authored-by: Peter Steinberger <steipete@clawstudio.local>

* test: close provider gatekeeper gaps (#2699)

Co-authored-by: Peter Steinberger <steipete@clawstudio.local>

* fix: harden provider gatekeeper lexer (#2700)

Co-authored-by: Peter Steinberger <steipete@clawstudio.local>

* fix: scope provider gatekeeper suppressions (#2702)

* test: broaden provider architecture gatekeeper (#2704)

* test: normalize inline block comments in gatekeeper scanning (#2705)

Blank single-line block comments with index-preserving spaces so
punctuation adjacency stays visible to position heuristics; document
nested-suppressed-call attribution and multi-line block comments as
parser-requiring out-of-scope patterns.

* fix: retain flicker-probe sessions so release builds actually record (#2709)

ProbeSession(...).begin() ran as an unretained temporary whose only other
reference was the driving timer's [weak self], so release-mode builds
deallocated the session at creation (compiler: "weak reference will always
be nil") and the probe recorded nothing. Sessions are now strongly owned in
MenuSwitchFlickerProbe.activeSessions until finish() unregisters them, and
ProbeSession gained a Configuration seam (timings + menu opener) so a test
can run a full session against a synthetic merged menu and prove it records
switch activity and frame samples.

* feat: nest claude-swap accounts in dashboard snapshot (#2713)

* Fix Codex manual reset refresh (#2710)

* Fix Antigravity agy cold-start quota readiness wait (#2665)

* fix(serve): bound the whole request head, not just each read (#2684)

* fix(serve): bound the whole request head, not just each read

`readRequest` applied `requestReadTimeoutMilliseconds` per `recv` with no overall
limit, so a client trickling one byte just inside that window never timed out and
held its connection — and the cooperative-executor thread serving it — for as long
as it kept sending. The Host allowlist and bearer-token check both run after the
head is read, and over-cap connections are closed rather than queued, so a few such
clients denied service to well-behaved ones entirely pre-auth.

Track a monotonic start time and cap each wait at the remaining budget, failing the
request once the overall deadline passes. A client that merely goes silent was
already handled by the per-read timeout; this covers the one that keeps sending.

Verified red→green on Linux: before, a legitimate client never got a slot within a
25s budget; after, it is served at 10.26s, matching the deadline.

* fix(test): gate the serve deadline suite to Linux

`TestsLinux` is declared unconditionally in Package.swift, so this file also
compiles on macOS, where the raw socket calls (`SOCK_STREAM.rawValue`, Glibc-only
imports) do not build and broke both macOS test shards.

Wrap the file in `#if canImport(Glibc) || canImport(Musl)`, matching
AntigravityProcessLauncherLinuxTests and the other syscall suites here.

* test(serve): inject a short deadline so the suite cannot stall others

The deadline test held executor threads for the full 10s production budget, which
starved unrelated suites: CLIHooksWatchSleepLinuxTests asserts a stop signal is
honored within 2s and instead measured 10.31s on CI. `.serialized` only orders
cases within a suite, so it does not prevent that.

Make the budget injectable on CLILocalHTTPServer, defaulting to the production
value, and have the test pass 1500ms. Full suite drops from ~10.5s to ~2.0s.

Verified the test still fails when the deadline logic is disabled (12.37s
starvation), so the shorter budget did not weaken it, and ran the full suite 8x
with no flake.

* docs: add CodexBar Meter to Linux desktop integrations (#2696)

* feat: add KRW to the preferred currency picker (#2669)

KRW was the one major Asian currency missing from the picker added in
#2490, so won-billed users converted USD estimates by hand.

No new exchange-rate logic is needed: fetchLatestRatesIfNeeded already
merges every rate in the open.er-api.com payload, which carries KRW.
Listing the code in supportedCurrencies is what opens the
requiresLiveRates gate, so selecting KRW triggers the same live-fetch and
24h-cache path as the existing currencies. The hardcoded fallback rate is
only read before the first successful fetch.

KRW is zero-decimal and currencyString pins en_US, so ICU resolves the
fraction digits and won renders without a fractional separator on any
host.

Refs #2449

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Align cost cache save overshoot with documented load cap (#2703)

* Align cost cache save overshoot with documented load cap

* Keep preceding codex cache producer key compatible

* Keep current main codex cache producer key compatible

* fix: accept all shipped codex producer keys back to #2632

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* fix(commandcode): Add support for individual-goat plan ($70/mo credits) (#2706)

* fix(commandcode): Add support for individual-goat plan (/mo credits)

* fix(commandcode): Support new commandcode_prod_.session_token cookie name

* fix(packaging): Restore widget extension packaging in package_app.sh

* fix(commandcode): preserve legacy bare-token cookie fallback

Keep __Secure-better-auth.session_token as the bare-token default until a
renamed production cookie is proven live; accept the new
commandcode_prod_ cookie names additively with regression coverage for
both name families.

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* docs: changelog for review round (#2710, #2665, #2684, #2706, #2703, #2669)

* feat(cli): add built-in web dashboard to codexbar serve (#2715)

GET / now serves a self-contained HTML page that polls
/dashboard/v1/snapshot and renders provider cards with usage windows,
credits, and cost. The static UI stays unauthenticated (it carries no
account data); tokens are entered in-browser, kept in localStorage, and
sent as a bearer header. No visibility gating on polling so embedded
panes that report document.hidden permanently still refresh.

* Document Antigravity data sources when the app is closed (#2666)

* Document Antigravity sources when the app is closed

* docs: keep Antigravity experimental tag and reflect cold-start readiness wait (#2665)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* fix(cli): deliver late serve results instead of discarding them (#2717)

When a serve operation outlived its request deadline, the coordinator
marked the slot timed out and threw away the eventually-produced value:
it never reached the accept commit (response cache) and the queued
same-config successor restarted the source from zero. On machines where
the dashboard snapshot build outlasts --request-timeout, GET
/dashboard/v1/snapshot 504ed forever while each retry burned another
full cost-history scan.

Timed-out sources now still commit through accept and hand the fresh
result to a same-fingerprint successor without rerunning the source;
changed-config successors promote as before. The web UI shows a
friendlier 504 message noting it retries automatically.

* feat(cli): add --identity redacted|full to the dashboard snapshot command (#2716)

The one-shot `codexbar dashboard` always redacted account identity even
though the builder supports full mode. Personal dashboards on trusted
private networks (e.g. a tailnet portal) want real account emails.
The serve HTTP transport is unchanged and stays always-redacted;
--identity none is rejected.

* feat(cli): add --output <path> to the dashboard snapshot command (#2719)

Static-webroot publishers currently shell-wrap the one-shot command
(codexbar dashboard > tmp && install tmp target) to avoid readers seeing
a partial document. --output writes the snapshot atomically instead:
staged temp file in the destination directory, fsync, rename(2) over the
target, 0644. Stdout stays untouched when the flag is absent and silent
on success when writing to a file. Empty paths are an args error and a
missing parent directory fails with a clear message; directories are
deliberately not created.

* feat(serve): render claude-swap accounts in the web dashboard; add --identity full (#2720)

The built-in web dashboard ignored the dashboard-v1 accounts[] array, so
multi-account Claude setups showed a single ambient row, and the serve
transport's always-redacted identity left the email unknown. Per-account
sections now replace the ambient windows when accounts are present (label
fallback when identity is redacted), accountsError surfaces on the card,
and a serve-level --identity redacted|full startup flag reuses the
dashboard command's decoder so trusted private deployments can show real
emails. Default stays redacted.

* docs: retitle unreleased to 0.48.0 and order changelog by user impact

* feat(serve): spend charts, provider brand icons, grouped card layout for the web dashboard (#2722)

The web dashboard showed only aggregate cost numbers while the app has
graphs, used a plain top-stripe accent, and nested claude-swap accounts
as rows inside one card. It now fetches /cost daily buckets and renders
an inline SVG spend chart per provider (failures never block the
snapshot render), serves the app's provider brand SVGs from an embedded
/icons/ route (names validated against the embedded set; immutable
cache), and lays out multi-account providers as one card per account
under a titled group with status pills, replacing the border stripe
with an icon-led header.

* fix(serve): hide the web dashboard status chip when no provider status exists (#2723)

The serve snapshot collector never fetches provider status pages, so
status is always nil and every card rendered a gray 'unknown' chip.
Render the chip only when the snapshot actually carries status data.

* chore: bump version to 0.48.0 (112) and date the changelog

* style: fix 69 SwiftLint violations from serve web-dashboard merges

The serve dashboard PRs (#2715-#2723) merged during the GitHub Actions
outage without CI lint validation. Mechanical, behavior-preserving fixes:

- CLIServeProviderIcons.swift: generator now emits scoped
  swiftlint:disable/enable line_length around the base64 data (icon
  payload is byte-identical after regeneration)
- CLIServeWebUI.swift: moved the html template constant to
  CLIServeWebUI+HTML.swift (enum body 1093 -> under 800 lines); the
  string literal content is verbatim-identical
- Data->String conversions switched to failable String(bytes:encoding:)
  with fallbacks, matching repo convention
- Long @Option help strings wrapped via adjacent + concatenation

* chore: publish 0.48.0 appcast entry

* chore: open 0.48.1 unreleased changelog section

* fix: make usage labels follow bar fill (#2744)

* feat(cli): paint the serve dashboard instantly and stream data in (#2746)

Three layers, one goal: never make the browser wait on a snapshot build
(measured 46.5s cold vs 22ms warm on the portal host).

- Stale-while-revalidate: expired cache entries within staleTTL answer
  immediately from the last-good response while a coalesced background
  rebuild commits fresh data. Applies to /usage, /cost, and the
  dashboard snapshot; failure-fallback rules are unchanged.
- Snapshot query params (schema stays v1): ?provider=<id> builds and
  caches one provider row independently; ?detail=shell returns
  config-only rows with no provider fetches or cost scans.
- Web UI: renders the last snapshot from localStorage instantly on
  revisit, paints skeleton cards from the shell on cold start, fills
  each card as its provider resolves, then hands off to normal polling.

* fix: pull 0.48.0 from the appcast — packaged app crashes at launch loading the CodexBarCore resource bundle (#2738, #2730)

* fix: resolve CodexBarCore resource bundle safely in packaged apps

0.48.0 crashed at launch on every machine without the build worktree:
the plugin runtime's Bundle.module accessor only probes the app root and
the compiled-in build path, but the packaged app ships the bundle in
Contents/Resources. Route all CodexBarCore resource loads through a safe
resolver (mirroring the existing localization/brand-icon pattern), and
add a gate test forbidding raw Bundle.module in CodexBarCore.

Fixes #2738. Fixes #2730.

* chore: bump version to 0.48.1 (113) and date the changelog

* feat(cli): default dashboard identity to full emails (#2748)

Redaction was the dashboard-v1 default at every layer; the --identity
flag already existed on both the one-shot command and serve. Flip the
default to full identity and keep --identity redacted as the opt-in
privacy setting for snapshots that cross untrusted networks. The web
UI stops re-redacting rows before persisting them to localStorage so
cached paints match what the server serves in either mode.

* docs: update appcast for 0.48.1

* Decode Codex monthly credit limit from spend_control.individual_limit (#2737)

* Decode Codex monthly credit limit from spend_control

* Keep spend_control below root and rate_limit precedence

* Centralize credit-limit precedence in resolvedIndividualLimit

* docs: move Codex spend_control credit-limit entry to 0.48.2 (#2737)

* fix(cli): keep claude-swap account emails when the usage fetch fails (#2752)

An account's name was derived only from its usage snapshot, which the
projection returns as nil for any status other than ok. Accounts with
expired or missing credentials therefore lost their identity and fell
back to the hardcoded "Account N" slot label, even though claude-swap
reports an email for every slot and the menu bar app shows it.

Resolve the email from the projection's displayLabel when the snapshot
has none, and feed that single value through the existing identity-mode
treatment for both label and identity so redacted mode stays honest.

* fix(zai): parse CREDIT_LIMIT quota entries; restore 5-hour primary reset (#2751)

* fix(zai): parse CREDIT_LIMIT quota entries from credit-based plans

z.ai's GLM Coding Plans (lite/standard/pro) now return CREDIT_LIMIT
instead of TOKENS_LIMIT from the quota API. Both the Swift parser and
the bundled zai.js plugin rejected the unknown type, leaving accounts
stuck at 100% remaining / 0% used and letting the reset display fall
back to a non-5-hour limit.

Treat CREDIT_LIMIT like TOKENS_LIMIT for window selection, window
minutes, and the 5-hour reset description; label credit plans as
"Credit quota" / "Session credit quota"; accept "level" as a plan
name in the plugin for Swift parity. Adds a CREDIT_LIMIT Swift/JS
parity fixture.

Squashed from PR #2725.

Fixes #2724

* test(zai): lock CREDIT_LIMIT parsing and 5-hour reset selection (#2724, #2712)

Parser-level regression tests built from the exact payload reported in
#2724: the 5-hour credit window becomes the primary lane (300-minute
window, 3.55% used, "5-hour" reset description) and the weekly credit
window the secondary, with the primary reset timestamp taken from the
5-hour entry rather than a fallback limit's longer schedule (#2712's
reported symptom). Adds the changelog entry crediting @stuible.

---------

Co-authored-by: Josh Stuible <joshstuible@gmail.com>

* fix(claude): render the menu-bar indicator from the active claude-swap account (#2750)

* fix(claude): render the menu-bar indicator from the active claude-swap account

When the claude-swap adapter owns Claude account presentation (2+
accounts, or 1 with the single-account opt-in), the menu renders adapter
account cards while every menu-bar icon path still read the ambient
Claude snapshot. With an ambient probe that yields no usable rate
windows, the bar drew empty even though the adapter had fresh usage for
the active account.

Route the icon render, layout/reset scheduling, unified-icon provider
pick, highest-usage ranking, icon observation signature, and switcher
mini-bars through a single menu-bar presentation-snapshot selector that
prefers the active claude-swap account snapshot and falls back to the
ambient snapshot when the adapter is disabled, below its presentation
threshold, or the active account has no usable usage.

Fixes #2731

* test: reconcile provider gatekeeper anchors for cswap menu-bar fix

* build: add packaged-app launch smoke check to the packaging pipeline (#2755)

0.48.0 crashed at launch on every machine without the build checkout
because SwiftPM's Bundle.module accessor only probes the app root and
the compile-time build path; local testing passed only because the
build machine still had the checkout at that path. Release packaging
now copies the packaged app to a temp dir and runs it with sandbox-exec
denying every file read under the checkout: a deterministic
CODEXBAR_RESOURCE_SMOKE=1 probe forces the resource loads that trapped
in 0.48.0 (they are lazy, so a liveness check alone cannot reach them),
then a 6s direct-launch liveness check guards other startup fatals.
Direct binary spawn skips LaunchServices and cleanup kills only the
spawned PID, so the check cannot fight a running production CodexBar.
Headless runs hard-fail only on the resource-bundle trap signature.

Pipeline guard suggested by the reporter of #2738.

* feat: portable QuickJS plugin engine; run JS providers on Linux (#2753)

* build: vendor pinned quickjs-ng engine

* feat: add portable QuickJS plugin engine

* refactor: remove Linux provider twins

* fix: use canImport C-library chain in CLIPluginsCommand for musl

* Fix CLI resource bundle resolution and packaging (#2757)

* fix: ship and resolve the CodexBarCore resource bundle for CLI contexts

Fixes #2756

* fix: support Linux CLI resource layouts

* Add codexbar-cosmic-applet to Linux desktop integrations (#2734)

* Handle Codex reset credits omitted after redemption (#2728)

* Handle consumed Codex reset credits omitted by provider

* Exclude expired Codex reset credits from consumption detection

* docs: changelog for omitted reset credits fix (#2728)

* Kimi: hide Code 7-day row when it duplicates the weekly lane (#2741)

* Kimi: hide Code 7-day row when it duplicates the weekly lane

GetUsages (FEATURE_CODING detail) and GetSubscriptionStats (ratelimitCode7d) report the same 7-day Code quota through two endpoints; live data shows identical usage and reset times. Hide the extra row when both lanes agree (within 1pt and 5 minutes), keep it when they diverge or the weekly detail is unreliable.

* Require positive evidence before hiding Kimi Code 7-day row

* Align Kimi lanes with official usage naming

* fix: align menu bar reset countdown with menu (#2735)

* docs: changelog for Kimi lane and countdown fixes (#2741, #2735)

* feat: cut over Synthetic, Poe, xAI, and z.ai to JavaScript on all platforms (#2758)

* feat(synthetic): cut over provider to JavaScript

* feat(poe): cut over provider to JavaScript

* feat(xai): cut over provider to JavaScript

* feat(zai): cut over provider to JavaScript

* style(zai): use predicate count

* test: give the hung-script watchdog assertion CI headroom

The 0.15s interrupt must fire promptly, not wait out the hang; the 1s
elapsed bound flaked at 1.66s on a loaded ARM64 runner. 5s still proves
prompt termination against an unbounded loop.

* test: give hooks-watch interrupt assertion CI headroom

The 0.3s stop signal must beat the 10s interval; the 2s elapsed bound
flaked at 2.02s on a loaded x64 runner. 5s still proves prompt
interruption.

* docs: permit opt-in Claude statusLine JSON as an explicit data source (#2733)

* Menu: let metric meta and reset rows wrap instead of truncating (#2742)

* Menu: let metric meta and reset rows wrap instead of truncating

Compact usage detail rows (#2620) put all pace detail on one line and moved reset times into the title row; longer locales (e.g. Chinese) truncate tail content. Allow the meta line and reset label to wrap to two lines so all information stays visible while keeping the compact layout.

* Menu: track wrapping metric text in height-cache fingerprint

* fix: track reset title width in height cache

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* docs: changelog for wrapping metric rows (#2742)

* feat: add SQLite CostUsageStore foundation (refs #2760) (#2761)

* feat: cut Codex cost persistence over to SQLite, delete the JSON artifact (refs #2760) (#2762)

* feat: cut Codex cost persistence over to SQLite

* docs: document SQLite cost history storage

* test: reconcile provider gatekeeper for the SQLite cutover

* fix: share the cost store serial executor

* fix: keep the cost-usage database on transient SQLite failures, only rebuild on corruption (refs #2760) (#2765)

withDatabase treated every error as corruption and deleted the entire
database. That destroyed user history on lock contention (app and CLI
both open writable connections), disk-full, and even a plain NOT NULL
constraint violation from appending rows for an unregistered path --
which then failed again on the fresh database and nuked it a second time.

Classify SQLite primary result codes: transient/environment/data-shape
errors (BUSY, LOCKED, FULL, NOMEM, IOERR, CONSTRAINT, READONLY, ...) now
return the fallback and keep the file; corruption and schema drift
(CORRUPT, NOTADB, ERROR, invalid data, incompatible schema) still
rebuild. Rebuilds and preserved failures are now logged via the
token-cost category so field rebuilds are observable, and a failed
transaction that could not roll back drops the connection instead of
leaving it wedged.

Adds a failure-injection suite: constraint orphans, connection reuse
after a rolled-back transaction, mid-file structural corruption, deleted
-wal / stale -shm sidecars, zero-byte databases, the database path
occupied by a directory, and a future schema version on disk.

* fix: restore JSON-cache retention semantics lost in the SQLite cutover (refs #2760) (#2767)

* fix: restore JSON-cache retention semantics lost in the SQLite cutover (refs #2760)

Semantics-parity audit of the CostUsageCache -> CostUsageStore migration
found four behaviors the cutover dropped:

- Retention pruned only the typed discovery columns while the scanner
  round-trips discovery through the opaque payload, so deleted files
  resurfaced on the next load; prune now updates both in lockstep and
  resets cursors/isComplete like the old cache did.
- The row budget deleted the oldest files regardless of window; the old
  entry budget never sacrificed in-window or recently active files (the
  byte budget remains the only authority that may).
- Fork-parent protection ignored the lineage-only dependency key and a
  stale parent survived when its only referencing child was pruned in
  the same pass; candidates now honor the dependency key and iterate to
  a fixpoint.
- Out-of-window files with an in-window mtime (active sessions with
  unscanned rows) were deletable; they are protected again.

saveCodexCache budgets are injectable for tests, restoring pins for
previous-report preservation across trims and the non-Gregorian
catch-up report (#2703), plus strip/compaction coverage.

* docs: changelog for the SQLite retention semantics parity fixes (refs #2760)

* perf: precompute per-path baseline counts in saveCodexCache instead of rescanning all rows per file (refs #2760) (#2770)

* test: make cost-usage debounce and token-hydration tests order-independent (#2777)

- Prevent a background models.dev pricing refresh from flipping codexPricingKey mid-test.
- Replace the fixed 1s poll that raced the process-global CostUsageScanExecutor.

* test: add StoreStress harness and contention regression tests (#2766)

* fix: stabilize OpenRouter plugin parity (#2779)

* Fix Codex cost pricing race (#2776)

* fix: price persisted usage at report read time

* fix: reprice project usage snapshots at read time

* test: add corpus-scale proof gates for the SQLite cost store (refs #2760) (#2763)

* test: add corpus-scale proof gates for the SQLite cost store (refs #2760)

* test: freeze scale-proof reference date and assert exact retention count

* test: bound persisted size of legacy payload

* fix: wrap the cost-store save cycle in one transaction so a crash mid-save is all-or-nothing (refs #2760) (#2771)

* fix: wrap the cost-store save cycle in one transaction so a crash mid-save is all-or-nothing (refs #2760)

* fix: inline save-transaction control so the operation closure stays actor-isolated

* fix: replace the save-transaction closure with begin/end calls to satisfy older Swift 6 region analysis

* test: widen slow-refresh sleeps in CLIServeRouterTests so the request deadline always wins the race on loaded CI runners

* Add CodexBar Plasma integration (#2768)

Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com>

* Add Fireworks provider (30-day billing spend) (#2687)

* Add Fireworks provider showing 30-day billing spend

* Fix Fireworks URL/auth interpolation and test assertions

* Validate Fireworks account slug; add malformed-slug test

* fix: keep recently modified files out of cost-store window pruning (refs #2760) (#2764)

* fix: keep recently modified files out of cost-store window pruning (refs #2760)

* fix: use inclusive calendar-day bounds for retention mtime recency

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* docs: changelog for Fireworks provider and calendar-day pruning fix (#2687,…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants