Report unreadable Claude OAuth refresh as terminal - #2650
Conversation
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 steipete#2634
|
Codex review: needs real behavior proof before merge. Reviewed August 4, 2026, 10:46 PM ET / August 5, 2026, 02:46 UTC. ClawSweeper reviewWhat this changesThe PR carries a completed-but-unreadable Claude CLI refresh result internally so background refreshes use a longer cooldown and accurate guidance without changing the public outcome enum. Merge readiness⛔ Blocked until real behavior proof is added - 4 items remain This is a substantive partial repair for the open Claude OAuth report, and current main still has the retryable failure path it targets. The patch appears correct on source review, but it needs redacted after-fix signed-build evidence before merge. Priority: P2 Review scores
Verification
How this fits togetherWhen Claude OAuth credentials expire, CodexBar may invoke Claude Code to refresh them and then retry usage retrieval. The refresh coordinator reports the result to the usage fetcher, which controls cooldowns, explicit-refresh handoff, and user-facing errors. flowchart LR
A[Expired Claude OAuth cache] --> B[Claude usage fetcher]
B --> C[Delegated refresh coordinator]
C --> D[Claude CLI credential touch]
D --> E{Readable credential source?}
E -->|Yes| F[Retry usage fetch]
E -->|No| G[Background terminal guidance]
G --> H[Long cooldown]
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Land the narrow terminal classification once a signed build proves the new background message and cooldown, while keeping credential recovery itself tracked in #2634. Do we have a high-confidence way to reproduce the issue? Yes in source: a clean CLI touch with an unchanged fingerprint, unavailable Keychain reads, and no valid credentials file reaches the retryable failure on current main. A real signed-build after-fix capture is still missing. Is this the best way to solve the issue? Yes for the narrow reporting and retry-churn problem: internal result detail preserves the public contract and keeps touch errors retryable. It is not a complete OAuth recovery solution, which remains tracked separately. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2d76cd9e9fca. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (7 earlier review cycles)
|
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.
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
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 steipete#2634 has explicit coverage.
|
Landed. Verification before merge (independent maintainer-agent review):
Root-cause context for the wider #2634 regression (why |
A profile whose keychain CodexBar cannot read produces an indeterminate observation and also satisfies isRefreshResultUnreadable. The indeterminate branch was evaluated first, so it dropped isUnreadableAfterRefresh, replaced the switch-source guidance with a generic retry message, and put the CLI back on the short-cooldown relaunch loop removed in steipete#2650. Order the terminal verdict first and cover it with a regression test.
* Port upstream 0.48.0: bound serve request heads * Test serve deadline-driven connection recovery * Port upstream 0.48.0: serve dashboard + snapshot contract * Port upstream 0.48.0: codexbar dashboard command * fix(dashboard): coordinator lost-wakeup/cancellation, daily totalCost wire, bare --output, camelCase schema F1: coordinator.rs registers the Building waiter's Notified while the lock is held (await_build helper), closing the notify_waiters lost-wakeup window; a BuildGuard resets a stranded Slot::Building to Empty + wakes waiters when a build is cancelled or panics. Adds deterministic lost-wakeup + cancel/panic regression tests with tokio::time::timeout bounds. Single-flight, TTL, late-result delivery, and errors-not-cached semantics preserved. F2: data.rs emits the upstream daily wire key 'totalCost' (was cost_usd); dashboard.html gates on 'daily.some(v => (v.totalCost || 0) > 0)' and every per-row read uses 'd.totalCost'. Adds wire/HTML agreement and positive/zero/ empty chart behavior tests. F3: dashboard.rs write_atomic treats an empty Path::parent as '.' so '--output bare-name.json' writes to the working directory. Test covers bare relative create, replacement, and no temporary leftover. F4: AccountPayload and StatusPayload serialize camelCase (updatedAt) to match pinned v1; golden assertions verify 'updatedAt' exists and 'updated_at' does not. * Port upstream 0.48.0: OpenCode Go per-model daily cost breakdown 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 Codex/Claude. Rows with no modelID fall back to an 'unknown' bucket; whitespace-only ids collapse to 'unknown', whitespace-padded ids merge with the trimmed bucket (upstream steipete#2649). Provider-local: the SQL extraction, model normalization and (day, model) aggregation live in the OpenCode Go local reader. The shared cost surfaces are reused, not duplicated -- get_daily_cost_history gains an 'opencodego' arm and CostScanner gains scan_opencodego_with_cancel that maps the provider-local summary onto the existing CostSummary (total_cost_usd, by_model, sessions_count, period), so the chart's local-usage panel and daily cost history treat OpenCode Go like Codex/Claude without a bespoke chart surface. No Codex/Claude pricing policy is copied; no duplicate chart machinery. Tests: 11 focused regressions covering multiple models/days, same-model merge, step-finish model inheritance, unknown/whitespace-only/whitespace-padded model ids, zero-cost rows, malformed rows, history-window exclusion, local-day boundary keying, deterministic (day, model) ordering, summary aggregation, and Zen-wait independence of the pure aggregation. * Port upstream 0.48.0: parse CommandCode rolling windows + GOAT plan F12 (steipete#2630): parse windowLimits.fiveHour/weekly (root or nested in credits) into primary/secondary rate windows with number-or-string coercion and epoch-s/epoch-ms/ISO-8601 resetAt handling; monthly grant moves to tertiary and uses the plan catalog for its total. F13 (steipete#2706): add the individual-goat plan (0/mo) to the plan catalog, recognize the new commandcode_prod_.session_token cookie names ahead of the legacy better-auth family (bare tokens keep the legacy name), and narrow pasted headers to the session cookie per upstream CommandCodeCookieHeader.override. Regressions: both upstream window-limits fixtures copied verbatim; cookie priority/case-folding/family tests; plan catalog + login-method and monthly-window mapping tests. * Port upstream 0.48.0: derive OpenRouter key meter from server remaining F14 (steipete#2612): the key-limit meter and its left/used text now come from the server-reported current-period limit_remaining (clamped to [0, limit]: negative reads exhausted, above-limit reads 0%) instead of lifetime usage. Without a server remaining, fall back to the period usage matching the declared limit_reset window, then cumulative usage; keys without any usable quota source keep the meter hidden. Adds wire decoding for limit_remaining/limit_reset. * Port upstream 0.48.0: preserve ZoomMate browser cookie scope F16 (steipete#2627): browser-imported cookies keep their raw host scope instead of being merged into one header reused on host failover. Chromium/Firefox host keys carry the scope (leading '.' = parent-domain), so each destination host (ai.zoom.us / zoommate.zoom.us) now gets its own header via RFC 6265 domain matching: parent-domain sessions reach both API hosts, host-only cookies never leak onto sibling hosts, non-API hosts are never destinations. Upstream fixture issue-2507-cookie-scope.json copied verbatim; regression tests cover the per-host partition, suffix-attacker rejection, empty domains, and the hostOnly/domain matrix. * Port upstream 0.48.0: decode Copilot AI credits counter A15 (steipete#2593/steipete#2613): quota snapshots now decode credits_used (number or string) for token-billed seats. The absolute counter stays off the rate-window path — it surfaces as an informational extra window (ai-credits), matching the existing snapshot/bridge/diagnostics pipeline without inventing a fake quota denominator. Upstream carriesCreditsCounter parity: zero-entitlement placeholder snapshots still yield their counter, so a business seat with no renderable quota window no longer blanks out — its snapshot becomes an informational credits row (preferred premium-, then chat-classified entries) instead of the previous hard error. Seats without any counter keep the existing token-billing error. * Port upstream 0.48.0: classify Claude OAuth refresh failures, terminal backoff F3 (steipete#2650): on Windows the credential file is readable, so the upstream touch-completes-but-unreadable state has no equivalent — the matching provably-unrecoverable-by-retry state is the refresh endpoint rejecting the stored refresh token (400/401/403). Those now classify as terminal: a 5-minute per-source backoff (upstream defaultCooldownInterval) instead of a doomed grant on every poll, and an honest re-login message without the useless retry tail. Transient failures (network, 429, 5xx) keep a short 20-second cooldown (upstream shortCooldownInterval) so recovery still lands quickly. Successful refresh clears state; a re-login via the shared credentials file is adopted past any backoff as before. Regressions: classification matrix, long/short backoff gating + purge, distinct terminal/cooldown user messages. * Port upstream 0.48.0: bounded Zen balance wait in OpenCode Go usage reads F15 (steipete#2583): the Zen balance now joins usage reads with an explicit policy bound measured from task creation — CLI usage/serve /usage reads (requires_optional_usage_completeness, new FetchContext field) join for the remainder of the 5 s optional-balance budget; background/UI/guard/ diagnose hooks keep the 250 ms grace, so a slow subscription fetch can never stack a second full wait. Local (SQLite) reads gain the same optional enrichment as web reads, matching the upstream local strategy. The balance fetch itself is the upstream chain: dashboard page parse first, dedicated billing server-fn (raw 1e-8 USD behind a customerID marker, RSC-fragment tolerant) as fallback, 25 ms start delay, bounded per-request, and abandoned-over-budget tasks are aborted instead of leaking. A zero-cost balance embedded in the usage page still wins without any extra request. A14 (per-model cost breakdown by day) intentionally NOT in this commit. * Port upstream 0.48.0: Claude OAuth refresh gate + serve/opencodego fixes (M1-M4) M1: from_http_status terminal iff (400|401) AND OAuth error == invalid_grant (case-insensitive). 403 and 400/401-without-invalid_grant are transient. M2: terminal gate indefinite until credential fingerprint changes or success clears; transient base 5min flat; eliminates repeated dead-grant retries. M3: serve /usage sets requires_optional_usage_completeness false (background poll grace); CLI usage remains true. M4: abort spawned Zen balance task on usage-page/parse error before early return. * Port upstream 0.48.0: Kimi/GLM/z.ai China routing (WS5) - Kimi Desktop monthly membership pool enrichment: read-only WAL-safe kimi-auth token reader for the Electron Chromium store (%APPDATA%/kimi- desktop), AES-256-GCM via existing browser cookie crypto; Code API + CLI snapshots merged with Monthly + Code 7-day membership windows (steipete#2622/A10). - Cookie Source Off disables Kimi Desktop + browser import (manual cookie headers keep working) (steipete#2623/A12). - Moonshot/Kimi Open Platform: MOONSHOT_REGION + region-bound CODEXBAR_MOONSHOT_API_KEY(_REGION) binding so CN/intl keys stay on their issuing hosts; provider renamed per upstream (steipete#2621/A11). - GLM Coding Plan: 5-hour TOKENS_LIMIT window is primary, weekly secondary, MCP rendered as a separate named window; plan name falls back to plan/plan_type/packageName/level (steipete#2621/A11). - z.ai region routing: BIGMODEL/ZHIPU(ZHIPUAI)/GLM env aliases and coding-relay key files only for BigModel CN; canonical cross-region endpoint overrides rejected before bearer auth (steipete#2623/A12). - Shared WAL-safe read-only SQLite helper (core::sqlite) replacing the OpenCode Go-local copy, reused by the Kimi Desktop reader (steipete#2544 pattern). * Port upstream 0.48.0: unify Pi-family (pi + OMP) agent sessions (WS6) - One dialect-aware scanner: live pi/OMP process detection (basename + bun-shim handling, helper filtering), session-jsonl correlation per CWD, PID-only rows when no transcript can be attributed, upstream OMP profile / PI_CONFIG_DIR / --session-dir / settings.json root resolution with the same fail-closed validation, upstream fixtures copied verbatim (steipete#2626/A13). - Wire shape: AgentSession gains optional dialect + sessionName; --json stays legacy Codex/Claude-only (v1) while --json-v2 emits the complete array; SSH session discovery negotiates --json-v2 with --json fallback. - Local scanner adopts the shared bounded directory budget; provider labels Pi/OMP in the sessions UI (bridge DTO + locale keys). - Remote plumbing (RemoteSessionFetcher) moved to agent_sessions/remote.rs and the Pi scanner split into pi_family/{mod,parser,roots} to keep every file under 1000 lines. * fix(dashboard): enable waiter notification under the decision guard (F1) Corrects the lost-wakeup fix in 87ff071: await_build re-acquired the slot mutex AFTER the decision guard was released, so a build completing in that gap could fire notify_waiters (and swap the slot) before the waiter ever registered — the waiter then slept forever on an already-fired Notify. Now the waiter constructs and enable()s an OwnedNotified (which owns the Arc<Notify>, Send+Sync) while still holding the SAME decision mutex that observed Slot::Building, carries the registered future out of the critical section, and awaits it only after the guard drops. The builder can update the slot and notify_waiters only while holding that same mutex, so registration is provably ordered before any wakeup attempt for this build. await_build helper removed. Regression: completion_in_decision_window_sets_waiter_notified drives the waiter with manual polls and forces build completion + notify_waiters into the exact decision->await window (slot manipulated directly; zero scheduler dependence). Existing racing/cancel/panic regressions and single-flight, TTL, late-result, and errors-not-cached behavior unchanged. Also repairs two FetchContext initializers in source.rs for the integrated requires_optional_usage_completeness field (E0063 at d2a63eb; false = FetchContext::default, no behavior change). Verified: cargo fmt --all --check clean; cargo clippy -p codexbar --all-targets -- -D warnings clean; 10/10 coordinator tests pass (2 threads). * Port upstream 0.48.0: Codex cost-scanner robustness (F1,F2,F18,F19) WS2 — Codex cost cache and scan robustness, ported from upstream 0.48.0. F1 (cache bounds): add CostUsageCacheBudget module with upstream's 256 MiB save / 320 MiB load / 25 000 entry caps. Load refuses to decode artifacts above MAX_LOAD_BYTES (cheaper to rebuild bounded). Save prunes out-of-window entries, then trims oldest in-window entries to fit the budget, protecting partially-parsed (growing) files so append-only resume keeps its catch-up progress. F2 (fork catch-up resume): validate the cached resume offset is a real line boundary (byte at offset-1 == newline) before resuming an append-only parse. A partial trailing-line write leaves the offset mid-line; resuming there corrupts the first record. When the check fails, fall back to a full re-parse from zero instead of the append-only merge. F18 (priced + unpriced Auto Review): codex-auto-review and the model-less sentinel are now deliberately unpriced routing rows — tokens counted, by_model row present with 0 cost, no fallback to gpt-4o rates. Add typed ModelPricingCompleteness (Complete | Partial{unpriced_models}) to CostSummary so the dashboard can label a partial breakdown. F19 (overshoot contract + predecessor keys): document the save/load overshoot contract — save may exceed MAX_FILE_BYTES up to MAX_LOAD_BYTES when protected entries cannot be trimmed further. Predecessor-key acceptance is N/A locally (local cache uses filename -v1 versioning, no producer-key field); documented as a documented divergence. * Port upstream 0.48.0: Codex windows/pricing (F5,F6,C4) WS3 — Codex duration classification and pricing, ported from upstream 0.48.0. F5 (duration classification 5h/weekly/30-day): centralize duration policy in RateWindowCadence (Session/Weekly/Monthly/Unknown) with from_minutes() and from_seconds(). Add MONTHLY_WINDOW_MINUTES (43 200) next to the existing SESSION/WEEKLY constants. Update codex_window_role to use RateWindowCadence so 30-day windows classify as Monthly instead of being swallowed into Weekly. C4 (Fast cost semantics + Terra/Luna refresh): add codex_api_fast_multiplier() (gpt-5.4/5.4-mini/5.6-sol/5.6-terra/5.6-luna → 2.0; gpt-5.5 → 2.5; else nil) and codex_fast_cost_usd() (standard cost × multiplier with long-context guard at 272 000 input). Wire into codex_costs::codex_cost_usd after canonical resolution fails but before legacy gpt-4o fallback, for fast/priority model IDs. Refresh Terra rates (2e-6/1.2e-5, long 4e-6/1.8e-5) and Luna rates (2e-7/1.2e-6, long 4e-7/1.8e-6). Fast detection is name-based locally (upstream uses a priority-trace SQLite DB scan — documented divergence). * Port upstream 0.48.0: complete WS2+WS3 follow-up (A16,F6,F8,F5) A16 (scan completeness JSON): add historyCoverageIsEstablished to CostSummary and surface it in the CLI cost JSON as historyCoverageIsEstablished (Bool?, null for non-Codex providers). Set from cache freshness + catch-up state so callers know when a re-scan is pending. Provider-native-only flag is N/A locally (no pi/OMP mirror sessions) — documented divergence. F6 (manual reset backfill): add codex_reset_backfill in Tauri providers.rs — backfills missing resets_at/reset_description on fresh Codex windows from the cached snapshot when the cached reset is still future (fresh used_percent untouched). Wired into refresh_provider before publishing so every surface (tray, CLI, frontend) sees the backfilled reset. Implemented through the existing provider refresh abstraction, not a generic trait hook. F8 (cached spend during refresh): add refreshing + stale_updated_at to UsageSpendRow (backward-compatible optional fields). When the Codex cache was pruned for budget (previous_report set), the spend row shows the stale timestamp and refreshing indicator so the UI can show old data while a re-scan rebuilds the artifact. Frontend UsageSpendTab renders the indicator and uses the UsageSpendRefreshing locale key. F5 (monthly cadence wiring): add Monthly to WindowRole (managed accounts) and wire monthly through the ambient provider (normalize_array_windows 4-tuple routes monthly to UsageSnapshot.tertiary). Add tertiary_label to bridge ProviderUsageSnapshot (duration-cadence label via RateWindowCadence). Frontend MenuCard.tsx uses tertiaryLabel with monthly localization (ProviderMonthly). Tray provider_status_label for Codex picks first non-informational lane (session → weekly → monthly). CLI usage.rs appends a monthly lane line with RateWindowCadence-based label. Test added for Monthly role classification. * Port upstream 0.48.0: fix fmt/clippy integration issues - Remove untracked package-lock.json (pnpm repo, npm artifacts incompatible) - cargo fmt --all: bridge.rs, providers.rs, usage_spend.rs, tray_bridge.rs - clippy: move constant-size budget assertion into const block (assertions_on_constants) - clippy: add TestCache type alias to simplify test helper return type (type_complexity) - clippy: collapse nested if-let in tray_bridge.rs codex_lane_headline_window using let-chains (let-chains stable since 2025 edition) * Port upstream 0.48.0: C4 centralize fast suffix stripping (audit fix) Extract codex_fast_base_model() that strips -fast/-priority suffixes. Both codex_api_fast_multiplier() and codex_fast_cost_usd() now use it so the original suffix does not leak into the Standard base lookup. Previously codex_fast_cost_usd passed the original model name to codex_cost_usd, which failed for suffixed IDs like gpt-5.5-fast. Tests added: - test_codex_fast_cost_usd_suffixed_models_resolve_to_base: gpt-5.5-fast -> base gpt-5.5 × 2.5, gpt-5.6-sol-priority -> base gpt-5.6-sol × 2.0 - test_codex_fast_base_model_unsuffixed: unsuffixed and unknown models resolve to themselves. * Port upstream 0.48.0: F8 clear previous_report after full scan (audit fix) A completed full scan rebuilds the cache for the current window, so any prior catch-up state is no longer pending. Clear previous_report before save_cache so the persisted artifact no longer signals stale/refreshing. Previously previous_report was set during save-time budget pruning but never cleared, causing a permanent Refreshing indicator. Test: previous_report_clears_after_successful_full_scan — first scan clears, inject previous_report to simulate trim, full scan clears it. * Port upstream 0.48.0: A16/F18 expose coverage+completeness in CLI JSON (audit fix) A16 historyCoverageIsEstablished and F18 modelPricingCompleteness were added to CostSummary in the prior follow-up but never wired into the CLI cost JSON or text output. Now: - JSON emits historyCoverageIsEstablished (bool for Codex, null otherwise) and modelPricingCompleteness ("complete" or {partial:{unpriced_models}}). - Text output labels partial pricing and partial coverage when present. - --provider-native-only flag added, maps to CostScanOptions::include_pi_sessions = false, excluding pi/OMP session mirrors. Documented divergence: no pi/OMP mirror sessions on this Windows build so the flag is accepted but has no observable effect locally. Tests: json_output_emits_a16_and_f18_fields, json_output_a16_null_for_non_codex, provider_native_only_flag_default_false. * Port upstream 0.48.0: F19 refuse oversized cache + fix trim double-subtraction (audit fix) F19: save_cache now checks the encoded JSON length against MAX_LOAD_BYTES before persisting. If the artifact still exceeds the load budget after pruning+trimming (e.g. a single protected entry alone exceeds the limit), the save is refused — no persist/refuse/rebuild loop. Extracted as CostUsageCacheBudget::should_refuse_persistence() pure helper for testability. Trim double-subtraction fix: trim_in_window_for_budget pre-subtracted droppable[0] from the initial estimate, then subtracted it again inside the loop — a double count. Now the initial estimate is the full estimated_cache_bytes and the loop subtracts each candidate once. Tests: - should_refuse_persistence_at_and_above_limit (boundary 1024/1025) - trim_estimate_no_double_subtraction_of_first_entry - trim_drops_until_target_reached_then_stops - save_cache_persists_small_codex_artifact (no false-positive) - save_cache_refuses_non_bounded_provider_oversize (Claude gate) * Port upstream 0.48.0: F2 boundary helper + scan-level negative regression (audit fix) F2 (upstream 0.48.0 steipete#2648): add boundary helper tests for all edge cases and a scan-level negative regression proving midline/truncated rewrite forces full parse (no resume from stale offset). Tests: - is_line_boundary_offset_zero_returns_true (offset 0) - is_line_boundary_offset_at_or_past_size_returns_true (EOF) - is_line_boundary_offset_exact_newline_returns_true (valid boundary) - is_line_boundary_offset_midline_returns_false (fall through) - is_line_boundary_offset_missing_file_returns_false (probe fail) - cost_scan_midline_rewrite_forces_full_parse_not_resume (scan-level) * Port upstream 0.48.0: F5 cadence/routing/headline regression tests (audit fix) F5 (upstream 0.48.0): boundary table for RateWindowCadence and routing regression for normalize_array_windows + tray headline preference. Tests: - cadence_boundary_session_exactly_300 (Session) - cadence_boundary_weekly_10080 (Weekly) - cadence_boundary_below_monthly_43199_is_weekly (boundary) - cadence_boundary_monthly_43200 (Monthly) - cadence_from_seconds_rounding (0/neg→Unknown, 18001s→301→Unknown) - cadence_label_keys (session/weekly/monthly/unknown) - f5_normalize_array_routes_session_weekly_monthly_to_lanes - f5_normalize_array_monthly_routes_to_tertiary_not_secondary - f5_normalize_array_empty_returns_placeholder_primary - f5_normalize_array_unknown_windows_fall_to_code_review - f5_headline_prefers_non_informational_primary (Tauri) - f5_headline_falls_back_to_secondary_when_primary_informational (Tauri) - f5_headline_falls_back_to_tertiary_when_primary_and_secondary_informational (Tauri) - f5_headline_returns_primary_when_all_informational (Tauri) * Port upstream 0.48.0: F6 reset-backfill regression tests (audit fix) F6 (upstream 0.48.0 UsageStore+CodexResetBackfill): regression tests for codex_reset_backfill covering future/stale/no-cached/non-codex/existing paths. Tests: - f6_backfills_future_cached_reset (future reset backfilled, used untouched) - f6_does_not_backfill_stale_cached_reset (past reset skipped) - f6_does_not_overwrite_existing_resets_at (fresh reset preserved) - f6_skips_non_codex_provider (Claude skip) - f6_skips_when_no_cached_snapshot (None cached) Divergence: upstream weekly-confirmation exemption N/A locally — local backfill is the observable analog (no weekly-confirmation guard exists). * Port upstream 0.48.0: fix clippy lint in audit-followup test code (audit fix) Fix 4 clippy -D warnings violations in the audit-followup test code: - collapsible_if in cli/cost.rs partial pricing label - field_assignment_outside_initializer in cli/cost.rs test - field_assignment_outside_initializer in jsonl_scanner.rs test - needless_borrows_for_generic_args in cost_scanner.rs test * Port upstream 0.48.0: F19 refusal removes preexisting destination artifact (audit fix) The prior F19 commit refused on oversized post-encode but left any existing destination cache file in place; a stale/oversized artifact could persist and trip the load-refusal path on the next scan, forcing an unnecessary full rebuild from a poisoned artifact. - save_cache now deletes the destination file on refusal (best-effort, mirroring the fs-delete idiom used elsewhere in core). - Extracted save_cache_with_limit(provider, cache, cache_root, max_load_bytes) as a private testable helper; save_cache delegates with the production MAX_LOAD_BYTES const. Production limit behavior is unchanged. Integration regressions (jsonl_scanner.rs): - save_cache_refusal_removes_preexisting_destination_artifact: precreate real destination via save_cache_with_limit(usize::MAX), trigger refusal with limit=1, assert destination gone, no tmp artifact with content, and load yields empty cache (no rebuild loop). - save_cache_at_exact_limit_is_accepted: encoded artifact at exactly the injected limit is persisted (boundary). - save_cache_one_over_limit_is_refused_and_removes_destination: one byte over limit is refused and destination removed. * fix: fan out dashboard build errors * fix: pin Windows globalization timezone module iana-time-zone resolves the Windows system zone through WinRT's Windows.Globalization Calendar class, but nothing keeps that DLL loaded. COM cleanup exercised by the notification sound/toast tests unloads it, leaving windows-core's process-static factory cache pointing into an abandoned mapping; the next get_timezone() call then access-violates (observed under LLDB; full lib suite crashed 3/3 single-threaded). Load Windows.Globalization.dll from System32 and pin it for the process lifetime before any get_timezone() call, gated behind a one-shot LazyLock so every caller waits for load+pin to settle. On pin failure return UTC without calling iana-time-zone (an AV is uncatchable). Route both project call sites (claude cli_reset, sub2api) through the new crate-internal helper. No lockfile change. * test: make globalization pin test host-independent The self-hosted PR runner is a stripped Windows image without registered WinRT types (ToastNotification not registered), where pinning Windows.Globalization.dll can fail by design; the helper then correctly falls back to UTC. Assert the environment-independent contract instead: the pin attempt settles exactly once and every caller observes the same decision.
* 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,…
Fixes part of #2634.
Problem
When the delegated Claude CLI refresh completes cleanly but the Keychain fingerprint does not move,
performAttemptreports.attemptedFailed("Claude keychain did not update after Claude CLI touch."). On 0.47.0 that verdict is wrong in a specific and permanent way.Success is defined as the fingerprint changing inside the touch window (
min(timeout, 2)s,delays = [0.2, 0.5, 0.8]). If Claude Code already refreshed its own item on its own schedule, the CLI has nothing to do, the fingerprint does not move, and the attempt fails. SincekeychainAccessAllowedis an unconditionalreturn falsein release, the refreshed credential is unreadable anyway — so on a profile with no~/.claude/.credentials.json, no amount of retrying can succeed.The background path then printed
... delegated Claude CLI refresh failed: Claude keychain did not update after Claude CLI touch. Run 'claude login', then retry.Both halves mislead: retrying cannot help, andclaude loginrefreshes the Keychain item that CodexBar does not read.Change
The verdict is carried on an internal
AttemptResultrather than a newOutcomecase:Outcomeships in theCodexBarCorelibrary product, so adding a case would break downstream exhaustive switches on source update. The public enum andattempt(now:timeout:environment:)are unchanged; the fetcher reads the detail through a new internalattemptDetailed.git diff upstream/mainadds and removes nopublicline.The state is detected after the touch, not before:
keychainReadAllowedis captured inAttemptConfigurationbecauseperformAttemptruns in a detached task, where task-local overrides do not apply.claudeevery 20s for a state that cannot change. User-initiated refresh still bypasses the cooldown.The message change is scoped to the background path.
shouldPreserveOwnerCLIHandoffreturnstrueonly for.userInitiated, so an explicit Refresh still rethrows the typed.refreshDelegatedToClaudeCLIhandoff and reaches the owner-CLI pipeline exactly as before. Only background refreshes fall through to the generic catch, and those are the ones that were printing "Runclaude login, then retry" for a state where neither helps.Two presentation helpers moved to
ClaudeUsageFetcher+DelegatedRefreshMessages.swiftto stay under the file-length limit; visibility widened fromprivate statictostatic, no logic change.Scope and limits
SessionErrorpath in the report. When the touch itself errors, the outcome stays.attemptedFailedwith the underlying error, on the short cooldown. Masking a real touch error behind a terminal verdict would hide the CLI-hang defect, which looks like a separate problem. Only the completes-but-unobservable variant is addressed here.isolatedTestCredentialsURL— a per-process random temp path that is never created — rather than an overridden URL. The captured pre-touch value is what the tests steer, and a stray file there would fail the assertion loudly rather than pass wrongly.On after-fix runtime output
I tried to capture it and could not; the obstacle is structural.
Scripts/package_app.shsigns ad hoc, so a locally built bundle has a different code identity than the installed notarized one and cannot read CodexBar's own cached-credential Keychain item:It therefore starts with no cached credentials at all — a different state from the bug, which needs an expired cache. It never entered the OAuth path (zero delegated-refresh log lines) and fell through to the CLI scrape. The build holding the reproducing state is the notarized one, which lacks the patch; the build with the patch cannot reach the state. Closing that needs a Developer ID signed build.
The pre-fix side is live rather than fixture-based: on this setup the retained log holds no
touch succeededand notouch failed, onlytouch did not update Claude keychain→attemptedFailed, with the Keychainmdatsitting 31 minutes after the cached token's expiry.Also worth stating: the changed message is background-only.
ClaudeProviderDescriptorsetsallowBackgroundDelegatedRefresh: false,performAttemptreturns.skippedByPromptPolicyfor background work unless the prompt policy isalways, andshouldPreserveOwnerCLIHandoffkeeps the existing handoff for.userInitiated. On the "only on user action" configuration from the linked issue the message path is unreachable by design, so no local capture would surface it either.Tests
ClaudeOAuthDelegatedRefreshUnreadableResultTests:isUnreadableAfterRefresh, and the touch is asserted to have run once.attemptedFailed, flag clearBoth use testing overrides and a temp credentials URL; no real Keychain access.
Commands run
swift buildandswift build -c releaseswift test --filter ClaudeOAuthDelegatedRefresh— 27 tests, all greenmake test— full suite, 813 selections./Scripts/lint.sh formatand./Scripts/lint.sh— 0 violationsTwo pre-existing failures showed up on my machine during the full run; both reproduce identically on pristine
main:ProviderPluginExtensionParityTests—PerplexityUsageSnapshot.promoExpiryFormatternever setslocale, soMMMresolves throughLocale.currentand renders1월on a Korean-locale host. Unrelated to this change, fixed separately in Pin Perplexity promo expiry formatter to en_US_POSIX #2651.ClaudeOAuthDelegatedRefreshProfileIsolationTests / legacy cooldown migrates only to the default credentials profile— fails when run alongside sibling suites, passes in isolation, and does not reproduce on every run.