Skip to content

fix: discover Claude Swap accounts and isolate their usage - #1226

Merged
robinebers merged 6 commits into
robinebers:mainfrom
maddada:fix/claude-swap-accounts
Sep 15, 2026
Merged

robinebers merged 6 commits into
robinebers:mainfrom
maddada:fix/claude-swap-accounts

Conversation

@maddada

@maddada maddada commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

Fixes #1227.

Discover accounts saved by Claude Swap and merge matching Claude Code, Desktop, and Swap logins into one card per account and organization. Cards retain matching credential fallbacks and show the organization before the email.

What was happening

  • Saved Swap accounts were missing because discovery only inspected the default Claude Code login and Desktop organizations.
  • The initial Swap integration could discard Desktop fallback credentials or omit Swap credentials from a Desktop-backed card.
  • Email-only names made organizations sharing an email indistinguishable.

What this changes

  • Discover Swap metadata and retain stable account-and-organization card IDs.
  • Keep matching default, session, Desktop, and read-only vault sources on merged cards; reject credentials for a different identity.
  • Put the organization first in card names so long emails do not hide it.
  • Preserve generation checks for in-flight login changes and source-scoped credential writes; never refresh or write vault credentials.
  • Deduplicate copied/shared session history and filter spending by recorded ownership.
  • Add overlap, fallback, layout/pin persistence, credential-change, source-write, and same-email/different-organization regression coverage.
  • Preserve a separate default card when the login has an account UUID but no organization ID; do not infer a saved organization or count its spending on that card.
  • Try live-capable matching credentials before limited-scope credentials, so a default login without user:profile cannot hide a working saved session.
  • Merge current upstream main (bb055e2), preserving its ownership and nested workflow fixes alongside Swap history folders.

Heads-up

  • Discovery runs at launch. Restart after adding or removing saved accounts.
  • Unattributed history remains excluded when multiple accounts are known. Broader SDK/Conductor attribution is outside this PR.

Tests

Latest verification, 2026-09-13:

  • Four new regression tests reproduced the review cases before the fixes and all passed afterward. They cover UUID-only default discovery, matching full-scope session usage, wrong-organization rejection, and history isolation.
  • Full host swift test: 1,345 total, 1,342 passed, 3 skipped, zero failures.
  • Focused swift test --filter 'Claude|ProviderAccountAssemblyTests': 158 total, 156 passed, 2 skipped, zero failures.
  • Rebuilt and signed the app with CONFIG=debug ./script/build_and_run.sh build, then launched the fresh host app. All four discovered Claude cards completed live refreshes successfully.
  • git diff --check: passed.
  • Full suite executed inside Tart macOS 15.7.7: 1,345 total, 1,342 passed, 3 skipped, zero failures. The four new review regressions also passed separately. Compilation used the host toolchain; execution used the VM's staged XCTest runtime. A missing shell-script fixture was staged before the successful full rerun.
  • Repeated live overlap with the exact updated app, Desktop signed in, and two concurrent Swap sessions at normal prompts. Switched work -> personal -> work and forced refreshes with the bundled CLI through GUI Terminal using cua-driver. Both states returned two identical card IDs and organization-first names, no errors, and no stale cards. Work remained at 4% session / 0% weekly; personal remained at 1% session / 52% weekly.
  • After refreshing, both session directories and the restored work default reported the correct signed-in identity. Read-only profile and usage requests from each session's own credential file verified its account and organization and returned the matching limits. Both original session processes remained running.
  • The updated GUI independently completed a scheduled refresh at 05:23 UTC with both cards successful. Logs retained Desktop, default/session, and read-only vault sources on the work card, and session/vault sources on the personal card.

Earlier overlap verification, 2026-09-07:

  • Full host swift test: 1,328 tests total, 3 skipped, zero failures.
  • Focused Claude suite executed inside Tart macOS 15.7.7: 94 passed, 2 opt-in tests skipped, zero failures. The test bundle was compiled on the host and run in the VM with its XCTest runtime.
  • Rebuilt and launched the exact debug app in the VM through cua-driver 0.23.2.
  • Live check with Desktop and two concurrent cswap run sessions at normal prompts: switched the default work -> personal -> work and forced OpenUsage refreshes from GUI Terminal. Exactly two stable card IDs remained, with work at 0% weekly usage and personal at 52% in both states.
  • Real usage requests using both session directories passed again after the switches and refreshes. Desktop remained a usable preferred source on the work card, and both session processes remained running.
  • The GUI app independently logged successful scheduled refreshes. Direct automation clicks on Refresh returned unverified delivery; forced-refresh evidence comes from the bundled OpenUsage CLI, not an assumed button click.
  • git diff --check: passed.

Detailed regression and VM report.

Screenshots

  1. Dashboard showing separate account cards:

Claude Swap account cards on the dashboard

  1. Customize showing all three Claude accounts:

Three Claude accounts enabled in Customize

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Thanks for your interest in contributing to OpenUsage!

External pull requests must reference an open issue that:

  1. Has been approved by a maintainer with the approved label.
  2. Is assigned to the pull request author (@maddada).

Please discuss the change on an issue first, wait for a maintainer to approve and assign it to you, then reopen this pull request with Fixes #123 in its description.

Read the contribution guidelines.

@robinebers robinebers left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This fits our account discovery model, and I've approved issue #1227 and assigned it to @maddada. Approval is for the contribution's scope; the following changes and regression coverage are required before merging.

  1. Preserve every matching credential source on a merged card. If the same account and organization are available through Claude Code, Desktop, and Swap, they should produce one card that can use any valid matching login. Adding Swap must not remove a working Desktop fallback, and a Desktop-backed card should also be able to use its matching Swap credentials.
  2. Keep organization names visible. Replacing a card's name with only an email makes personal and work organizations with the same email indistinguishable. Preserve the organization label alongside any account label.
  3. Document the history limitation: spending without reliable account ownership remains excluded when multiple accounts are known. Solving the broader SDK/Conductor attribution issue is outside this PR's scope.

Required regression test scenario:

  • Set up two distinct account-and-organization identities, A and B, both saved in Swap and also discovered through Claude Code and Desktop. Include overlapping sources for the same identity.
  • Assert that discovery produces exactly two cards, with stable card IDs across repeated discovery and changes to the default login. Existing layout and pins must remain attached to the same identities.
  • Expire or reject the preferred credential and assert that each card falls back to another valid credential for that same account and organization. Cover Desktop-to-Swap and CLI/Swap-to-Desktop fallback. A credential for the other identity must never supply the card's limits.
  • Switch the default login from A to B and simulate credential changes while both Swap sessions are running. Assert that limits remain on the correct cards, stale refresh results cannot overwrite a changed login, credential writes stay with the source that supplied them, and vault refresh tokens are never used or written.
  • Include shared/copied session history and assert that each account's spending is counted once, never crosses to the other card, and unattributed history remains excluded.
  • Add a same-email, different-organization case and assert that it produces separate cards with distinguishable names.

Please add automated regression coverage for these cases and report the results in the PR. Also repeat a live overlap/switching check with Claude Code, Desktop, and concurrent cswap run sessions to confirm that refreshing OpenUsage does not break their logins. These are merge requirements, not optional follow-ups.

…each account card, show organization names, and test credential fallback, account switching, and isolation.
@maddada

maddada commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Hey @robinebers,
Tested all the scenarios listed and everything works as expected.

2026-09-07_Screen Sharing_17-48-50

Can work on adding codex-swap next if you want.

@maddada
maddada requested a review from robinebers September 10, 2026 19:02
@validatedev

Copy link
Copy Markdown
Collaborator

@codex review

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

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread Sources/OpenUsage/Services/ProviderAccountAssembly.swift
Comment thread Sources/OpenUsage/Providers/Claude/ClaudeAuthStore.swift
@maddada

maddada commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Hey @validatedev!
Want me to fix those and the conflicts or you'll handle them? Just so we don't do double work.

@robinebers robinebers left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Please fix these two cases before merging:

  1. Keep the default Claude account visible when its login file has no organization ID. When .claude.json contains oauthAccount.accountUuid but no organizationUuid, adding a valid Swap account bypasses the existing default-provider fallback. The default account is then omitted from the cards. OpenUsage already handles this input; adding Swap accounts should preserve that behavior. We have not established how common this state is in real installations, so this is not a claim about a particular older Claude version.

  2. Try a working saved login when the default login cannot read usage. A matching default credential without user:profile currently precedes the full-scope Swap session credential. The provider returns a local-only snapshot for the first credential and never tries the working fallback, leaving Session and Weekly unavailable. Prefer live-capable credentials or continue to the next matching candidate when a credential cannot read live usage, while retaining account-and-organization verification.

Please add regression tests showing that both the default and Swap accounts remain available in the first case, and that the matching full-scope session supplies live usage in the second.

Both cases were reproduced with controlled fixtures on cced50edcb1cf754f28648d677500f9ac5edcc0c. The existing focused Claude suite passed with 94 tests passed and 2 skipped; the two additional reproductions failed as described.

@validatedev

Copy link
Copy Markdown
Collaborator

Hey @validatedev! Want me to fix those and the conflicts or you'll handle them? Just so we don't do double work.

Better if you can fix them. Thanks!

# Conflicts:
#	Sources/OpenUsage/Providers/Claude/ClaudeLogUsageScanner.swift
…each account card, show organization names, and test credential fallback, account switching, and isolation.
@github-actions

Copy link
Copy Markdown

This pull request adds 1,060 lines.

Pull requests with more than 1,000 added lines are rarely merged because they are difficult to review and often extend beyond the approved issue. Please consider breaking this into a smaller, more surgical contribution.

…each account card, show organization names, and test credential fallback, account switching, and isolation.
@maddada

maddada commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

@robinebers @validatedev Fixed and checked in real environment and everything is working as requested/expected.

@maddada
maddada requested a review from robinebers September 14, 2026 02:42
@robinebers

Copy link
Copy Markdown
Owner

great work @maddada thank you so much

merging this now into the next beta. it would be great if you could create a follow-up PR with:

  • docs/testing/claude-swap-overlap.md is a one-off verification log checked into the repo. That belongs in the PR, not docs/.
  • New Swap section in docs/providers/claude.md is dense/technical vs. the "simple, skimmable" docs rule.

@robinebers
robinebers merged commit a0b408c into robinebers:main Sep 15, 2026
3 checks passed
liaomingxin added a commit to liaomingxin/openusage that referenced this pull request Sep 16, 2026
Brings in upstream v0.7.12-beta.2 (tip 86df736): Claude usage records
whose nested iteration model is null are no longer dropped (robinebers#1261), the
Claude plan badge follows Anthropic's live profile (robinebers#1262), an exhausted
Devin weekly quota is recognized when its percentage is omitted (robinebers#1251),
PostHog 3.72.0, and upstream's Claude Swap account support (robinebers#1226).

The fork keeps its own claude-swap cards (config snapshots, read-only
live tier with a cached-usage fallback, rate-limit and Keychain
cooldowns). Upstream's Swap support keys a slot by the same
accountUuid|organizationUuid identity, so wiring both would mint two
runtimes for one record id. Its code merges in unchanged but unwired.

Conflict resolutions:
- ProviderAccountAssembly: keep the fork's claude-swap observations and
  ClaudeSwapCard minting; do not call ClaudeSwapAccount.discover, and drop
  upstream's swap card loop and its org-less default-login branch. Keep
  the fork's accountLabel, adopt upstream's optional organizationID and
  organizationName.
- ProviderAccountsStore: keep both .credentialFile and .claudeSwap.
- ClaudeUsageMapper: keep the fork's limits[] windows and add upstream's
  formatLivePlan.
- docs/providers/claude.md: keep the fork's claude-swap accounts section.

Test adaptations required by the merge:
- Remove the three upstream tests that assert ProviderAccountAssembly
  mints upstream Swap cards (that path is unwired in the fork).
- Add the fork's 8th `detail` wildcard to `.progress` patterns in
  upstream's new Claude tests.
- The claude-swap structural guard skips upstream's ClaudeSwapAccount.swift
  and forbids the fork's card sources from referencing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjLwoj52ke83U7NEp3u6MJ
liaomingxin added a commit to liaomingxin/openusage that referenced this pull request Sep 16, 2026
- Last-sync record: upstream v0.7.12-beta.2 (86df736), 2026-09-16.
- New conflict-table row: upstream's Claude Swap support (robinebers#1226) shares
  the fork's claude-swap identity keys; the fork keeps its own cards,
  leaves upstream's code unwired, removes the three wiring tests, and
  fences the structural guard.
- claude-swap notes: live plan badge port and the widened HTTP double.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjLwoj52ke83U7NEp3u6MJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude Swap saved accounts are missing from OpenUsage

3 participants