Skip to content

claude-swap: render the usage fields the adapter already receives, add source-chosen switch targets - #3452

Open
QuantIntellect wants to merge 6 commits into
steipete:mainfrom
QuantIntellect:feat/claude-swap-richer-usage-and-strategies
Open

claude-swap: render the usage fields the adapter already receives, add source-chosen switch targets#3452
QuantIntellect wants to merge 6 commits into
steipete:mainfrom
QuantIntellect:feat/claude-swap-richer-usage-and-strategies

Conversation

@QuantIntellect

@QuantIntellect QuantIntellect commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Three changes to the opt-in claude-swap adapter, in separate commits so any of them can be taken alone.

1. Render the usage fields the adapter already receives (commit 1 — inside the accepted Phase 1 contract)

cswap --list --json carries more per-account display data than the parser decoded. Most visibly, a row whose live usage is null projected to a nil snapshot, so a token-expired or Keychain-blocked account rendered as an empty card while cswap list still showed its numbers.

Now parsed and projected, all display-only:

Field Effect
lastGoodUsage / lastGoodFetchedAt / lastGoodAgeSeconds Keep the last known bars on rows with no live usage instead of blanking the card
usageFetchedAt / usageAgeSeconds Date a snapshot by claude-swap's own fetch time, so "updated N ago" states the measurement's real age. claude-swap serves a per-account cache, so an ok row can legitimately be minutes old
usage.spend Pay-as-you-go spend on the shared provider cost row — the 5h / 7d / spend triple claude-swap's own menu shows
disabled "(disabled)" marker for slots held out of claude-swap's rotation
foreign_credential Recognized instead of falling through to "Unrecognized claude-swap status" (new in cswap 0.26)

No new argument array, no credential access, no change to process authority — the adapter still runs exactly cswap --list --json. Additive sections parse leniently so a malformed spend or fallback can never suppress valid rate windows. claude-swap's human message field is deliberately not parsed: it embeds the account email and must not bypass Hide Personal Info.

Last-known measurements are marked on the projection and excluded from the menu-bar icon override. A card states a snapshot's age; the bar icon has no such affordance, and isStale tracks provider errors rather than measurement age, so the bar keeps falling back to the ambient snapshot rather than presenting hours-old numbers as current. The at-limit retention path from #2731 is unaffected.

2. Rotate / best / next-available switch actions (commit 2 — a proposed extension to Phase 2)

Phase 2 activates one slot the user names. claude-swap's own menu bar additionally offers three switches where the tool picks the destination. Added as a Switch Using claude-swap submenu, named apart from the per-card Switch Account… action:

  • ClaudeSwapSwitchStrategy maps each case to one fixed argument array — --switch [--strategy best|next-available] --json — from a closed enum. No free-form arguments, no shell.
  • Still one explicit click per switch, one bounded transaction run to natural exit, serialized against slot switches. Not claude-swap's background auto loop, which stays out of scope.
  • A strategy run that finds nothing viable reports to: null; that surfaces as noViableTarget rather than a silent no-op. already-active stays a successful outcome. A slot-targeted switch still requires the exact slot it asked for.
  • ClaudeSwapSwitchMenuPlanner decides availability off the account projection, so the rule is testable without live NSMenu construction. Hidden when there is nowhere to go; an account disabled with cswap disable stays an explicit target but is never chosen on the user's behalf — claude-swap's own rule.

docs/claude-multi-account-and-status-items.md records this under "Proposed: source-chosen switch targets" rather than folding it into the accepted-decisions list, since decision 2 currently reads "automatic switching … stays out of scope". It is clarified to "automatic (background) switching". If you'd rather not take this, commits 2 and 4 and that doc section can be dropped; commit 1 stands alone.

3. Confirm what a switch actually did (commit 4)

A completed switch was silent. Clicking the action closes the menu, so the only signal was the active marker moving on the next open — and when the target's usage reads the same before and after, nothing visibly changed at all. This is easy to hit in practice: a freshly reset account reports pct: 0.0 on every window with no resetsAt, so it renders identically before and after being activated. A switched: false / already-active result produced no feedback whatsoever.

The parsed result is now recorded as a ClaudeSwapSwitchOutcome, and the card it landed on reports it: "Switched", or "Already Active". The confirmation replaces that card's action label for 90 seconds — long enough to still be there when the user reopens the menu after the click that closed it, short enough not to read as a permanent state — and any newer switch supersedes it. The destination comes from the result rather than the request, so a strategy switch confirms on whichever slot claude-swap chose. A run that named no destination confirms nothing, and failures keep surfacing through the existing per-account switch error.

ClaudeSwapAccountActionPlanner now owns the whole action-label decision (confirmation → in-flight → active → offer → none) in Core, so the ordering rules are covered by model tests instead of live NSMenu construction; the app layer only maps cases to localized strings.

4. Report why a switch did not happen (commit 5 — addresses the three review findings)

  • Blocked outcomes no longer read as success. cswap 0.26 returns the current account as to for
    candidates-exhausted, usage-unavailable, no-valid-target, only-one-account and unmanaged-account, not just
    for benign no-ops, so a strategy that could not move anywhere confirmed "Already Active".
    ClaudeSwapSwitchResultClassifier now separates a real move, a deliberate stay-put (already-active,
    already-best) and a blocked attempt by reason. A reason a future release adds is treated as blocked and
    surfaced, never silently confirmed.
  • Strategy errors have somewhere to render. They carry no requested slot, so lastErrorAccountID was nil and the
    card renderer never matched — unsupported arguments, malformed output and no-target runs vanished when the menu
    closed. Errors owned by no single card now render on the section beside the switch actions.
  • Retention no longer promotes stale data to live. An unavailable row retaining a last-known fallback kept the
    .live classification and slipped past the menu-bar exclusion; RetainedUsageStore.Record also dropped the flag on
    the disk round-trip. Provenance now survives both. The added test fails without the fix.

5. Make a completed switch visible immediately (commit 6)

The completion handler awaited the full ambient Claude refresh — OAuth, CLI and web probes — before bumping the
revision that redraws the cards, so for seconds the menu kept rendering the pre-switch state and the click looked
inert. claude-swap is re-listed first (one short subprocess that owns the active marker), then the ambient reconcile
runs. The adapter configuration is re-checked after each await rather than read once up front, so a configuration
change mid-flight still discards the result as Phase 2 requires. The exact-argv test now pins the whole sequence
(--switch-to <slot> --json, --version, --list --json), a stronger guarantee than before.

The same commit gives the status label three weights via PlanEmphasis — plain for a plan name, accent for the active
account, green for an action that just completed — because among sibling cards that otherwise look identical the
active account did not stand out. Highlighted rows keep the selection color, so menu contrast is unchanged.

Commands run

swift build                                          # clean
make check                                           # 0 violations in 2137 files, locale parity across 23 locales
make test                                            # full sharded suite: 1032 selections, 0 failed groups
swift test --filter ClaudeSwap                       # 160 tests, 14 suites

Tests

Four new suites, fake executables and fixtures only — no real claude-swap install, no credentials, no Keychain access:

  • ClaudeSwapRichUsageParsingTests — spend (incl. currency default and unusable-limit rejection), disabled, freshness, lastGoodUsage (incl. dropped when empty/untimestamped, and malformed-never-fails-the-row), foreign_credential, and a regression test pinning the reset-timestamp shape cswap actually emits (six fractional digits with a +00:00 offset — a parse failure there would reject the whole account list).
  • ClaudeSwapLastGoodProjectionTests — last-known bars survive, updatedAt is the measurement time, spend → providerCost, disabled marker, foreign-credential note.
  • ClaudeSwapSwitchStrategyTests — exact argv per strategy, null-to handling, already-active, slot-switch strictness, planner availability rules.
  • ClaudeSwapSwitchConfirmationTests — confirmation targeting, expiry boundary, backwards-clock tolerance, in-flight precedence, and outcome derivation from the switch result.

ClaudeSwapMenuBarSnapshotTests gains a case pinning that an active account carrying only last-known usage still falls back to the ambient snapshot on the bar.

Verification

Verified against cswap 0.26.0 with a real three-account setup, including a live switch. Strategy arguments require a claude-swap release supporting switch --strategy; on older builds the run surfaces a parse error rather than switching silently.

Related: #1756, #1268, #2444 (same class as the relogin_required gap), and the open layout decision in #3382 — this PR does not touch that layout.

@clawsweeper

clawsweeper Bot commented Sep 6, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

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

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Sep 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cebc10e00

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

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

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

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


let executablePath = self.settings.claudeSwapExecutablePath
self.claudeSwapTransientState.switchingStrategy = strategy
self.runClaudeSwapSwitch(executablePath: executablePath, attributingErrorTo: nil) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Surface strategy-switch failures outside account-scoped errors

When a source-chosen switch fails—such as to: null or an unsupported strategy on an older claude-swap—passing nil here stores lastError with a nil lastErrorAccountID. The only UI consumer in StatusItemController+ClaudeSwapMenu.swift displays this error when lastErrorAccountID == account.id, which can never hold for a real account, so the promised error is silently discarded from the menu. Add a strategy/global error presentation path or associate the failure with a visible row.

Useful? React with 👍 / 👎.

Comment on lines +166 to +167
if let retained = self.retainedAtLimitSnapshot(previous, matching: row, now: now) {
return .live(retained)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve last-known classification when retaining limit data

If a row previously projected lastGoodUsage with an exhausted, not-yet-reset window and a later refresh reports .unavailable without live usage, this branch reclassifies that retained snapshot as live even though previous.usesLastKnownUsage is true. An active row then passes claudeSwapMenuBarSnapshotOverride and renders the old measurement on the age-less menu-bar icon, defeating the new last-known exclusion. Preserve the previous classification or do not use last-known snapshots in this retention path.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed September 7, 2026, 5:55 AM ET / 09:55 UTC.

ClawSweeper review

What this changes

The PR enriches Claude account cards with cached usage, measurement age, spending and disabled-account information, and adds source-selected switching with outcome feedback.

Merge readiness

Blocked before merge - 5 items remain

The PR remains useful, and the three previous findings are addressed. The new transcript establishes timestamp behavior, but merge readiness still depends on switching-scope approval, visible app feedback, and cache compatibility coverage.

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

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) The revised patch repairs all previous findings and adds useful live evidence, but the remaining app-proof and cache-compatibility gaps prevent a merge-ready rating.
Proof confidence 🦐 gold shrimp (3/6) Needs stronger real behavior proof before merge: The branch-built dashboard transcript proves measurement-age projection on a real three-account setup. The direct cswap no-op trace supports the upstream result shape, but does not show CodexBar’s menu-triggered switch, confirmation/error rendering, new visual emphasis or last-known card behavior. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Needs proof Needs stronger real behavior proof before merge: The branch-built dashboard transcript proves measurement-age projection on a real three-account setup. The direct cswap no-op trace supports the upstream result shape, but does not show CodexBar’s menu-triggered switch, confirmation/error rendering, new visual emphasis or last-known card behavior. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 10 items Repository policy and identity: Read the complete root AGENTS.md and verified the origin repository. Applied guidance on provider isolation, model-level tests, credential-safe validation and freshly built UI proof. No additional scoped AGENTS.md or maintainer-note files were found for the changed paths.
Current main still lacks the requested display behavior: Current main returns no snapshot for token-expired and other sentinel rows, stamps ordinary projected measurements with the refresh time, and lacks the new spend and fallback projection.
Main and latest-release necessity check: Searches of the adapter on fetched main and the supplied latest-release commit found neither lastGoodUsage nor ClaudeSwapSwitchStrategy. No supplied related item establishes a merged replacement for this work.
Findings None None.
Security None None.

How this fits together

CodexBar’s optional claude-swap adapter converts an external tool’s account and usage output into menu cards and bar indicators. User-initiated switches run through that tool, which owns credential changes, before CodexBar refreshes its display.

flowchart TD
  A[Account usage from claude-swap] --> B[Parse and retain measurements]
  B --> C[Account cards and bar indicators]
  D[User selects a switch action] --> E[Serialized external switch]
  E --> F[Classify switch outcome]
  F --> C
  E --> A
Loading

Decision needed

Question Recommendation
Should CodexBar accept source-chosen Claude account switching now, or land the display improvements separately? Land display work separately: Preserve the display improvements while deferring source-chosen switching to an explicitly approved follow-up.

Why: The accepted contract authorizes named-slot activation; the branch explicitly proposes broader target selection, and separable commits do not resolve that product/auth choice.

Before merge

  • Add real behavior proof - Needs stronger real behavior proof before merge: The branch-built dashboard transcript proves measurement-age projection on a real three-account setup. The direct cswap no-op trace supports the upstream result shape, but does not show CodexBar’s menu-triggered switch, confirmation/error rendering, new visual emphasis or last-known card behavior. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • Resolve merge risk (P1) - Source-chosen actions let the external tool select which account’s credentials become active; the documented extension has no recorded maintainer approval.
  • Resolve merge risk (P2) - The retained-usage JSON gains a provenance field, but old-record decoding and serialized fallback round-trip behavior lack demonstrated compatibility coverage.
  • Complete next step (P2) - Resolve the switching-scope decision, add freshly built app evidence for the visible changes, and demonstrate retained-cache upgrade and serialization compatibility; redact identities, credentials and private endpoints.
  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test growth Swift production +799/-53, net +746; tests +799/-19, net +780 Growth is explained by richer parsing, switch planning and feedback, with separate focused test suites.

Merge-risk options

Maintainer options:

  1. Separate the proposed switching scope (recommended)
    Keep display changes independently reviewable and defer source-selected credential activation until its contract is approved.
  2. Verify cache compatibility
    Demonstrate decoding a pre-change record and preserving last-known provenance through JSON serialization and reload.

Technical review

Best possible solution:

Keep the richer display work independently landable, preserve explicit-slot switching until the proposed extension is approved, and verify retained measurements remain correctly classified across upgrades and relaunches.

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

Yes for the display gap: current main drops sentinel-row snapshots and uses refresh time for ordinary measurements. This review established that from source and did not execute the app.

Is this the best way to solve the issue?

Partly: the parser and projection changes reuse the existing adapter well, and the prior defects are repaired; source-chosen switching remains a separate product choice with incomplete app-level proof.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 0863fe838a77.

Labels

Label changes:

  • add merge-risk: 🚨 compatibility: Persisted measurement provenance changes without demonstrated old-record and serialization compatibility coverage.
  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🦐 gold shrimp, so this older rating label is no longer current.

Label justifications:

  • P2: This is a bounded improvement to an optional account adapter, with no established urgent core outage.
  • merge-risk: 🚨 auth-provider: The proposed strategies delegate active credential destination selection beyond the accepted named-slot activation contract.
  • merge-risk: 🚨 compatibility: Persisted measurement provenance changes without demonstrated old-record and serialization compatibility coverage.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The branch-built dashboard transcript proves measurement-age projection on a real three-account setup. The direct cswap no-op trace supports the upstream result shape, but does not show CodexBar’s menu-triggered switch, confirmation/error rendering, new visual emphasis or last-known card behavior. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

What I checked:

Likely related people:

  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • sf-jin-ku: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

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

  • Provide redacted screenshots or a recording from the freshly built app showing retained usage and menu-triggered switch feedback; runtime logs or a transcript may supplement the visible proof. Update the PR body for automatic re-review, or ask a maintainer to comment @clawsweeper re-review.
  • Record approval for source-chosen switching or separate it from the display changes.
  • Cover pre-change cache decoding and last-known provenance through the actual JSON encoding/decoding boundary, using isolated fixtures.

Rating scale

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

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

Workflow

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

History

Review history (3 earlier review cycles)
  • reviewed 2026-09-06T19:11:28.874Z sha 2cebc10 :: needs real behavior proof before merge. :: [P2] Surface strategy-switch errors without requiring an account ID | [P2] Preserve last-known provenance through retention and reload | [P2] Interpret unsuccessful strategy outcomes beyond a null target
  • reviewed 2026-09-07T07:06:22.632Z sha 3c59e96 :: needs real behavior proof before merge. :: [P2] Surface strategy-switch errors without requiring an account ID | [P2] Preserve last-known provenance through retention and reload | [P2] Classify unsuccessful strategy outcomes before confirming success
  • reviewed 2026-09-07T07:24:35.777Z sha c72d2bb :: needs real behavior proof before merge. :: [P2] Surface strategy-switch errors without requiring an account ID | [P2] Preserve last-known provenance through retention and reload | [P2] Classify blocked strategy outcomes before confirming success

cswap schema v1 carries more per-account display data than the adapter
decoded. Rows whose live `usage` is null projected to a nil snapshot, so a
token-expired or keychain-blocked account rendered as an empty card while
`cswap list` still showed its numbers.

Parse and project the additive display-only fields:

- `lastGoodUsage` / `lastGoodFetchedAt` / `lastGoodAgeSeconds`: keep the
  last known bars on rows with no live usage, instead of blanking the card.
- `usageFetchedAt` / `usageAgeSeconds`: date a snapshot by claude-swap's own
  fetch time, so "updated N ago" reports the measurement's real age rather
  than the refresh tick. claude-swap serves a per-account cache, so an `ok`
  row can legitimately be minutes old.
- `usage.spend`: project pay-as-you-go spend onto the shared provider cost
  row, matching the 5h / 7d / spend triple claude-swap's own menu shows.
- `disabled`: mirror the "(disabled)" marker for slots held out of
  claude-swap's rotation. They stay valid explicit switch targets.
- `foreign_credential`: recognize the cswap 0.26 status sentinel instead of
  falling through to "Unrecognized claude-swap status".

Last-known measurements are marked on the projection and excluded from the
menu-bar icon override. A card states a snapshot's age, but the bar icon has
no such affordance and `isStale` tracks provider errors rather than
measurement age, so the bar keeps falling back to the ambient snapshot
rather than presenting hours-old numbers as current. The at-limit retention
path from steipete#2731 is unaffected.

All of these are display-only. No new argument array, no credential access,
and no change to the Phase 1/2 process authority: the adapter still runs
exactly `cswap --list --json`. Additive sections parse leniently so a
malformed spend or fallback can never suppress valid rate windows.
The adapter could only activate one explicitly chosen slot. claude-swap's
own menu bar also offers the three switches where it picks the target:
rotate, most quota left, and next account not at its limit.

Add them as a "Switch Using claude-swap" submenu on the claude-swap section,
named apart from the per-card "Switch Account…" action that names its target:

- `ClaudeSwapSwitchStrategy` maps each case to one fixed argument array
  (`--switch [--strategy best|next-available] --json`). CodexBar still
  forwards no user-supplied arguments and never invokes a shell.
- The switch parser now accepts a null `to`, which only a strategy run
  produces; the reader turns it into `noViableTarget` so "nothing to switch
  to" surfaces instead of looking like a silent success. A slot-targeted
  switch still requires the exact slot it asked for.
- `switched: false` with a target ("already-active") stays a successful
  outcome rather than an error.
- Both entry points share one serialized transaction, so a strategy switch
  cannot overlap a slot switch, and both run to natural exit.
- `ClaudeSwapSwitchMenuPlanner` decides availability off the account
  projection, keeping the rule testable without live NSMenu construction.
  Strategies are hidden when claude-swap has nowhere to go, and an account
  disabled with `cswap disable` is never chosen on the user's behalf — it
  remains an explicit switch target, which is claude-swap's own rule.
- `message` is deliberately not parsed: it embeds the account email and
  must not bypass Hide Personal Info. Only the machine `reason` is kept.

Each action is still one explicit user click. This does not start
claude-swap's background `auto` loop, and CodexBar still handles no
credentials.

Strategy arguments require a claude-swap release that supports
`switch --strategy`; verified against cswap 0.26.0.
Update the adapter contract in docs/claude-multi-account-and-status-items.md
and the provider notes in docs/CLAUDE.md to match what the adapter now
parses and renders, and record the source-chosen switch actions as a
proposed extension to Phase 2 rather than an accepted decision.

The Phase 1 allow-list gains the additive display-only fields (spend,
disabled, measurement freshness, lastGoodUsage), together with the rule
that additive sections parse leniently and that claude-swap's human
`message` field is never parsed because it embeds the account email.

Accepted decision 2 is clarified to say "automatic (background) switching"
stays out of scope, and points at the proposal for explicit switches where
claude-swap picks the destination slot. That proposal changes no storage,
status item migration, process authority, or credential boundary, so it is
left for maintainer review rather than folded into the accepted list.
A completed switch was silent. Clicking the action closes the menu, so the
only signal was the active marker moving on the next open — and when the
target's usage reads the same before and after (a freshly reset account
showing 0% either way), nothing visibly changed at all. A `switched: false`
"already-active" result produced no feedback whatsoever.

Record the parsed switch result as a `ClaudeSwapSwitchOutcome` and let the
card it landed on report it: "Switched", or "Already Active" when
claude-swap said that slot was already the active one. The confirmation
replaces that card's action label for 90 seconds — long enough to still be
there when the user reopens the menu after the click that closed it, short
enough not to read as a permanent state — and any newer switch supersedes it.

The destination is derived from the result rather than the request, so a
strategy switch confirms on whichever slot claude-swap chose. A run that
named no destination confirms nothing, and failures keep surfacing through
the existing per-account switch error.

`ClaudeSwapAccountActionPlanner` now owns the whole action-label decision
(confirmation, in-flight, active, offer, none) in Core, so the ordering
rules are covered by model tests instead of live NSMenu construction; the
app layer only maps cases to localized strings.
@QuantIntellect
QuantIntellect force-pushed the feat/claude-swap-richer-usage-and-strategies branch from 3c59e96 to c72d2bb Compare September 7, 2026 07:19
@clawsweeper clawsweeper Bot added the merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. label Sep 7, 2026
Review of steipete#3452 found three ways the new switch actions could leave a user
with no idea why their active account did not change.

1. `switched: false` was treated as "already active". cswap 0.26 returns the
   *current* account as `to` for blocked attempts as well as benign no-ops —
   `candidates-exhausted`, `usage-unavailable`, `no-valid-target`,
   `only-one-account` and `unmanaged-account` all report a non-null
   destination — so a strategy that could not move anywhere confirmed
   "Already Active". `ClaudeSwapSwitchResultClassifier` now separates a real
   move, a deliberate stay-put (`already-active`, `already-best`) and a
   blocked attempt by reason, and only the first two confirm on a card. A
   reason a future claude-swap release adds is treated as blocked and
   surfaced, rather than silently confirmed as success.

2. Strategy failures had nowhere to render. They carry no requested slot, so
   the completion stored `lastError` with a nil `lastErrorAccountID`, while
   the only consumer shows that error when the ID matches an account — which
   never happened. Unsupported strategy arguments, malformed output and
   no-target runs therefore vanished when the menu closed. Errors that belong
   to no single card now render on the section beside the switch actions.

3. At-limit retention promoted stale data to live. An `unavailable` row
   retains the previous snapshot when a window is still exhausted; when that
   previous snapshot was itself a last-known fallback, the retained copy was
   marked live, letting it through the menu-bar exclusion and onto the bar
   icon as current. Retention now carries provenance forward, and
   `ClaudeSwapRetainedUsageStore.Record` persists it so a relaunch cannot
   launder it either. Payloads written before the field predate the fallback,
   so decoding them as live stays correct.

Covered by tests over every blocked and benign reason cswap 0.26 emits, and
by a retention case that fails without the provenance fix.
Two things made a completed switch feel like a click that did nothing.

Ordering: the completion handler awaited a full ambient Claude refresh —
which probes OAuth, the CLI and the web and can take seconds — before
bumping the revision that redraws the cards. For that whole window the menu
kept rendering the pre-switch state, including the old active marker. Re-list
claude-swap first instead: it is one short subprocess and it owns the active
marker and the numbers the cards show, so the menu reflects the switch right
away. The ambient reconcile still runs, just afterwards, and the transaction
marker is held until it finishes so two credential writes cannot overlap.

Legibility: the status label sat in the same grey secondary text as a plan
name, so among sibling cards that otherwise look identical the active account
did not stand out, and a landed switch was easy to miss. `PlanEmphasis` gives
the label three weights — plain for a plan name or an offered action, accent
for the active account, green for an action that just completed — and the
claude-swap cards select one from the action the planner already computes.
Highlighted rows keep the selection color, so menu contrast is unchanged.
@QuantIntellect

Copy link
Copy Markdown
Contributor Author

Real behavior proof

All output below is from a build of this branch, against a real claude-swap 0.26.0 install with three accounts. Emails are redacted; nothing else is edited.

1. Cards are dated by claude-swap's own measurement, not the refresh tick

CodexBarCLI dashboard --identity redacted, reformatted for readability:

generatedAt (refresh tick): 2026-09-07T08:56:31Z

claude-swap:1    active=True   updatedAt=2026-09-07T08:55:03Z   session=5%  weekly=1%  fable=0%
claude-swap:2    active=False  updatedAt=2026-09-07T08:54:23Z   session=25% weekly=30% fable=22%
claude-swap:3    active=False  updatedAt=2026-09-07T08:54:23Z   session=36% weekly=34% fable=59%

Three distinct updatedAt values, all older than generatedAt, because claude-swap serves a per-account cache and had last fetched those rows 88s and 128s earlier. Before this change every row was stamped with the refresh tick, so all three read "updated just now" regardless of how old the measurement was. This is the usageFetchedAt / usageAgeSeconds parsing in commit 1.

2. switched: false really does come back with a non-null destination

A live --strategy best run, which is the exact argument array the new submenu executes:

$ cswap --switch --strategy best --json
{
  "schemaVersion": 1,
  "switched": false,
  "from": { "number": 1, "email": "<redacted>" },
  "to":   { "number": 1, "email": "<redacted>" },
  "strategy": "best",
  "reason": "already-best",
  "message": "Already on the account with the most remaining quota (Account-1).",
  "warnings": []
}

This is the shape the review flagged: switched: false with a non-null to. already-best is benign, so it confirms on the card. The blocked reasons (candidates-exhausted, usage-unavailable, no-valid-target, only-one-account, unmanaged-account) return the same non-null shape — see the _switch_noop call sites in cswap 0.26.0 switcher.py — which is why classification is by reason and not by a non-null target. ClaudeSwapSwitchResultClassifier covers every one of those reasons, and treats an unrecognized future reason as blocked rather than confirming it as success.

3. What changed since the last review

  • Blocked outcomes no longer read as success. Previously every switched: false became "Already Active".
  • Strategy errors have somewhere to render. They carry no requested slot, so lastErrorAccountID was nil and the card renderer never matched; they now render on the section beside the switch actions.
  • Retention no longer promotes stale data to live. An unavailable row retaining a last-known fallback kept the .live classification and slipped past the menu-bar exclusion. Provenance now survives both retention and the RetainedUsageStore disk round-trip. The added test fails without the fix.
  • A completed switch now shows up immediately. The completion handler awaited the full ambient Claude refresh (OAuth + CLI + web) before redrawing, so the cards kept rendering the pre-switch state for seconds and the click looked inert. claude-swap is re-listed first; the ambient reconcile still runs, afterwards.

Commands run

swift build      # clean
make check       # 0 violations in 2137 files, locale parity across 23 locales
make test        # full sharded suite, 1032 selections, 0 failed groups
swift test --filter ClaudeSwap   # 160 tests, 14 suites

@QuantIntellect

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

All three findings from the previous review are addressed, and real behavior proof is posted above.

  • Surface strategy-switch errors without requiring an account ID — errors that belong to no single account card now render on the claude-swap section beside the switch actions, instead of being stored with a nil lastErrorAccountID that no card renderer could ever match.
  • Preserve last-known provenance through retention and reload — retention carries usesLastKnownUsage forward instead of reclassifying a retained fallback as live, and ClaudeSwapRetainedUsageStore.Record persists it so a relaunch cannot launder it either. The added test fails without the fix.
  • Classify blocked strategy outcomes before confirming success — classification is now by reason, not by a non-null target. Benign already-active/already-best confirm on the card; every other non-switch, including a reason a future claude-swap release adds, is reported as a failure with its explanation.

Also fixed since the last revision: a completed switch was invisible for seconds because the completion handler awaited the full ambient Claude refresh before redrawing the cards.

On the merge-risk decision: the split you recommended is already in place. Commit 1 is the display work and stands alone inside the accepted Phase 1 contract; the source-chosen switching is commits 2, 5 and 6 plus the "Proposed: source-chosen switch targets" section, and can be dropped together without touching commit 1.

@clawsweeper

clawsweeper Bot commented Sep 7, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

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

@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Sep 7, 2026
steipete added a commit that referenced this pull request Sep 8, 2026
Preserve optional usageFetchedAt instead of resetting cached measurement age on each poll. Keep valid usage and the existing fallback for missing or malformed timestamps.

Extracted from #3452; its remaining last-good display and source-selected switching stay separate. Closes no issue.

Co-authored-by: Persian ROss <37_privacy.blends@icloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant