Conversation
|
Thanks for your interest in contributing to OpenUsage! External pull requests must reference an open issue that:
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 |
There was a problem hiding this comment.
🟡 Changes recommended
Nine unresolved moderate findings affect account discovery, credential fallback, provider scoping, sync identity handling, and per-card pricing refresh.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds account-scoped Codex support across Codex homes and pi logins, with separate cards, credentials, usage, reset claims, sync identities, and UI state.
Changes:
- Discovers and merges Codex accounts by account ID.
- Scopes authentication, usage, history, reset claims, and layouts per card.
- Updates shell parsing, UI integration, documentation, and regression tests.
File summaries
| File | Reviewed change |
|---|---|
Tests/OpenUsageTests/UsageHistoryAggregatorTests.swift |
Tests identity-aware history synchronization. |
Tests/OpenUsageTests/LoginShellEnvironmentTests.swift |
Covers banner-tolerant shell parsing. |
Tests/OpenUsageTests/CodexMultiAccountTests.swift |
Tests discovery, assembly, scoping, expiry, and spend splitting. |
Sources/OpenUsage/Views/WidgetRowView.swift |
Routes reset claims by provider card. |
Sources/OpenUsage/Views/WidgetGroupedListView.swift |
Passes provider IDs to widget rows. |
Sources/OpenUsage/Views/CustomizeProviderDetailView.swift |
Adds Codex pricing controls. Finding (moderate; 3 votes): refresh state remains hardcoded to the default codex card. |
Sources/OpenUsage/Stores/WidgetDataStore.swift |
Exports account-aware history. Finding (moderate; 1 vote): schema selection is based on inserted histories rather than registered cards. |
Sources/OpenUsage/Stores/ProviderAccountsStore.swift |
Adds Codex and pi source types. |
Sources/OpenUsage/Stores/DefaultLayout.swift |
Translates default layouts for additional cards. |
Sources/OpenUsage/Services/UsageReader.swift |
Builds scoped Codex providers. |
Sources/OpenUsage/Services/UsageHistoryAggregator.swift |
Matches history by account identity. |
Sources/OpenUsage/Services/ProviderAccountAssembly+Codex.swift |
Assembles Codex cards. Findings (moderate; 1 vote each): only the first pi credential is retained; codex mapping cleanup can lose swapped-account history; absent default identity assigns unscoped sources by ordering. |
Sources/OpenUsage/Services/ProviderAccountAssembly.swift |
Integrates Codex account assembly. |
Sources/OpenUsage/Services/LoginShellEnvironment.swift |
Handles prefixed begin markers. |
Sources/OpenUsage/Providers/ProviderCatalog.swift |
Registers scoped Codex providers. |
Sources/OpenUsage/Providers/Pi/PiUsageScanner.swift |
Filters pi usage by provider ID. |
Sources/OpenUsage/Providers/Pi/PiProviderMapping.swift |
Maps multiple Codex pi logins. |
Sources/OpenUsage/Providers/ErrorCategory.swift |
Categorizes expired pi tokens. |
Sources/OpenUsage/Providers/Codex/CodexResetClaimService.swift |
Supports per-card reset claims. |
Sources/OpenUsage/Providers/Codex/CodexProvider.swift |
Scopes Codex refresh and usage. Finding (moderate; 1 vote): empty pi ID sets can produce a false pi source note. |
Sources/OpenUsage/Providers/Codex/CodexLogUsageScanner.swift |
Scans account-specific Codex homes. |
Sources/OpenUsage/Providers/Codex/CodexAuthStore.swift |
Supports scoped and read-only pi credentials. |
Sources/OpenUsage/Providers/Codex/CodexAccountDiscovery.swift |
Discovers Codex homes and pi accounts. Findings (moderate; 1 vote each): observer lacks the access-token identity fallback; comma-separated CODEX_HOME values are not shared with observer logic; JWT-derived IDs are not propagated into provider and claim requests. |
Sources/OpenUsage/Models/UsageHistoryDocument.swift |
Validates account identities in v2 sync. |
Sources/OpenUsage/App/StatusItemController.swift |
Injects per-card reset services. |
Sources/OpenUsage/App/AppContainer.swift |
Wires scoped providers, layouts, and claims. |
docs/research/account-first-plan.md |
Marks Codex account support as shipped. |
docs/providers/codex.md |
Documents multi-account Codex behavior. |
docs/icloud-sync.md |
Documents account-aware synchronization. |
Review details
Suppressed comments (8)
Sources/OpenUsage/Providers/Codex/CodexAccountDiscovery.swift:96
- Discovery accepts an access-token JWT claim when
id_tokenhas no account identity, butDefaultAccountObserver.observeCodex()only examinesid_token(lines 157-164). A default auth.json with the account claim only inaccess_tokenis therefore discovered here but leaves assembly unresolved, so no scoped cards are created and the provider falls back to the unscoped Codex card. Use the same access-token fallback in the observer.
let payload = auth.tokens?.idToken.flatMap { ProviderParse.jwtPayload($0) }
?? auth.tokens?.accessToken.flatMap { ProviderParse.jwtPayload($0) }
let accountID = auth.tokens?.accountID?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
?? DefaultAccountObserver.chatGPTAccountID(inIDTokenPayload: payload)
Sources/OpenUsage/Providers/Codex/CodexAccountDiscovery.swift:60
- This parser now treats
CODEX_HOMEas a comma-separated list, butDefaultAccountObserver.observeCodex()still wraps the raw value in a single-element array. With a configured list, the observer reads a path containing the comma and returns.absent, so assembly cannot mark the default account/anchor and can assign the bare ID and unattributed sources to the wrong discovered account. Share the same splitting and expansion logic with the observer.
if let raw = environment.value(for: "CODEX_HOME")?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty {
homes += raw.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
Sources/OpenUsage/Providers/Codex/CodexAccountDiscovery.swift:96
- This fallback lets an auth file create a card from the JWT's account claim when
tokens.account_idis absent, but the derived ID is not carried into the auth state used byCodexProviderandCodexResetClaimService. Those requests will consequently omitChatGPT-Account-Idfor these otherwise supported logins, despite the endpoint contract requiring it. Propagate the discovered account ID into the provider/claim-service request state.
let accountID = auth.tokens?.accountID?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
?? DefaultAccountObserver.chatGPTAccountID(inIDTokenPayload: payload)
Sources/OpenUsage/Providers/Codex/CodexProvider.swift:189
- For a scoped card with no matching pi login,
piProviderIDsis an empty set, but this still starts a pi scan. When any pi session file exists, the scanner returns a non-nil empty result after filtering, and the later source-note check reports pi even though this card has no pi source. Skip the scan for an empty scoped ID set (or otherwise base the note on matching entries).
async let pi = PiUsageScanner.shared.scan(
cardID: "codex", piProviderIDs: piProviderIDs, now: now(), pricing: pricing,
estimateCost: { CodexUsagePricing.estimatedCost(pricing: pricing, model: $0, tokens: $1) }
)
Sources/OpenUsage/Services/ProviderAccountAssembly+Codex.swift:92
pis.firstis the only pi credential retained for a card even thoughpiProviderIDsincludes every matching login. If one ChatGPT account is signed into pi under multiple provider IDs and the first token is expired or rejected while another is valid, this card reports the pi-expired/error state without trying the usable login. Preserve the pi credentials as candidates and fall back across them while keeping all provider IDs for spend filtering.
piProviderIDs: pis.map(\.providerID),
piLogin: pis.first,
ownsUnattributedSources: identity == unattributedOwner
Sources/OpenUsage/Services/ProviderAccountAssembly+Codex.swift:98
- When the default-home account changes, this removes the
codexidentity mapping even though the loop above has already assigned that bare ID to the other account's card. The swap case therefore leaves that card's history unattributed, so it can be omitted or merged incorrectly on peers. The cleanup block should not remove the mapping created for the card that still ownscodex.
if let defaultIdentity, let card = cards.first(where: { $0.identityKey == defaultIdentity }), card.id != "codex" {
identityKeys.removeValue(forKey: "codex")
}
Sources/OpenUsage/Services/ProviderAccountAssembly+Codex.swift:75
- When
observeCodex()is.absent(no login in either default home),defaultIdentityis nil and this fallback assigns all unscoped sources toidentities.first. With accounts only in sibling homes, filesystem/discovery order therefore decides which account receives unattributable OpenCode usage even though no source has a default badge. Leave these sources unowned unless a default-home identity was resolved, or define and test an explicit default-source rule instead of using ordering.
let unattributedOwner = defaultIdentity ?? identities.first
Sources/OpenUsage/Stores/WidgetDataStore.swift:446
hasAccountCardsis derived from histories already inserted intoproviders, not from the registered Codex cards. If one card on a multi-account Mac has no local history yet, this can see only the other card and downgrade the export to v1 while stripping its identity; peers with both cards then cannot attribute that history. Base the schema/remapping decision on the enabled account-card registry rather than history presence.
let hasAccountCards = providers.keys.contains { $0.contains("@") }
if !hasAccountCards {
identities = identities.filter { ProviderAccountID.family(of: $0.key) == "claude" }
}
return UsageHistoryDocument(
- Files reviewed: 29/33 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if ProviderAccountID.family(of: providerID) == "codex" { | ||
| CodexPricingSection() | ||
| } |
robinebers
left a comment
There was a problem hiding this comment.
AI Review (Codex) — Request Changes
Posted at the maintainer’s request after reviewing the approved issue, PR implementation, existing discussion, and current main.
Reviewed PR head 835265ec08b38fa24587db40ebbbecd6a7e46519 against remote main at 56378e5765f85d38ff413036fd984afe3d4664e4, verified unchanged at the end of the review. The common ancestor is 86df736e. This is a review of the submitted implementation and its compatibility with current main; no conflict-resolved merged implementation exists yet.
Issue #1265 is open, approved, and assigned to the PR author. Its additional discovery scope remains useful. The earlier policy-bot comment is stale; the current approved-issue check succeeds.
PR #1264 was merged after this branch diverged. Main already supports Codex Swap account cards, workspace/user identities, matching file and Keychain fallback, per-card reset claims, account layout defaults, and account-aware sync. It also protects against changed credentials, token writes to Swap snapshots, and ambiguous local/cached spend. Main does not yet provide #1265’s general sibling-home and pi-auth discovery.
The PR is marked DIRTY. A three-way comparison found conflict markers in 14 files. This requires an integration rewrite around main’s existing account model, beyond resolving text conflicts.
Findings
-
[P1] Bind each credential read and action to the card’s expected identity. CodexProvider.swift:20
The card’s identity is discarded when constructing its auth store; only its launch-time file paths remain. A subsequent login switch makes those paths return another account’s credential. The original card then displays that account’s limits, and its reset service can consume that other account’s credit. Mocked tests reproduce both: A’s card sends B’s account header and successfully consumes B’s fake credit. A separate pending-refresh test also replaces auth.json with B’s login during token renewal; the PR overwrites that replacement with A’s refreshed credentials. Main’s identity filtering, current-credential rechecks, and read-only Swap handling prevent these cases. Preserve those protections, including checks around network awaits and source-specific write policy. -
[P1] Preserve main’s workspace-plus-user identity contract. ProviderAccountAssembly+Codex.swift:40
Discovery and reconciliation use the bare ChatGPT workspace/account id. Main’s Codex identity key isaccountID|email, deliberately keeping two users in one workspace separate. The PR collapses that fixture into one card. Seeding a registry with main’s existingacct-a|a@example.testrecord then running PR discovery mints a newcodex@…card for the same login, breaking the binding to its existing layout/pins. A valid main v2 document using the composite identity also fails to match the PR’s bare identity. Reuse CodexAccountIdentity and extend its validated credential parsing for the new sources. -
[P1] A home’s current login does not own its historical spending. ProviderAccountAssembly+Codex.swift:89
The PR assigns every rollout under a home to the account currently logged into that home. The regression fixture writes 120 tokens while A occupies the default home, then swaps only the auth files. B’s card receives those unchanged historical 120 tokens. Shared or copied session roots can also contribute to multiple cards because event deduplication is per scan/card. OpenCode’s unattributed history is always assigned to the current default, or to the first discovered identity when no default resolves. Main intentionally excludes these histories when multiple accounts are known and strips invalidated cached spend before display/export. Keep that exclusion unless durable ownership is proven. The same history caveat applies to pi provider IDs if their login identity is replaced or pool slots are reused. -
[P2] Keep the bare card’s identity mapping after the default changes. ProviderAccountAssembly+Codex.swift:96
In the swap fixture A permanently ownscodex, while B permanently owns its hashed id. The loop correctly installs both identity mappings, then this cleanup deletes A’scodexmapping because B is now the default. That bypasses A’s cache ownership check and omits its history from multi-card export. The same assembly returns cards in current-default observation order, reversing their registry order after a switch. Main returns persistent record order. The PR’s existing swap test checks IDs and the default-source flag, but neither the ownership map nor order. -
[P2] Reload pi credentials when pi renews them. CodexAuthStore.swift:141
loadPiAuth()reconstructs auth from the PiCodexLogin snapshot captured at launch; it never rereads pi’s auth.json. Expiry checks also use that captured expiry. Renewing the token in pi and pressing Refresh therefore keeps returning authExpired with no HTTP request. This contradicts the new troubleshooting instruction to use the account in pi and refresh. Keep pi read-only, but reread the matching entry and validate its identity on each refresh. -
[P2] Retain all matching pi credentials for fallback. ProviderAccountAssembly+Codex.swift:91
All matching provider IDs are retained for spending, but onlypis.firstsupplies credentials. With one expiredopenai-codexlogin and a validopenai-codex-2login for the same account, the card reports authExpired without trying the valid login. Preserve the candidate set and try matching usable access tokens without rotating pi credentials. -
[P2] Do not strip ownership based on which cards have history today. WidgetDataStore.swift:442
Export decides account awareness from the histories actually inserted, remaps a single exported account to the bare family id, and drops all Codex identities if no hashed provider remains. Two registered Codex accounts with only A’s history export an unowned v1 document. A peer with two matching cards then imports none of A’s spending. A single-account peer can instead accept unowned spending without proving an account match. Main retains complete Codex ownership even when only one account exports. Preserve its export/import identity rules. -
[P2] Keep account-aware exports valid when Claude is unresolved. WidgetDataStore.swift:447
Two Codex histories cause v2 export, but an unresolved bare Claude history is still included without its required identity. The produced document throwsinvalidIdentity("claude"), blocking the whole device’s history export. Main explicitly omits only unresolved Claude history so other providers can sync. Preserve that behavior. -
[P2] Make default identity observation and discovery use compatible parsing. ProviderAccountAssembly+Codex.swift:22
Discovery accepts an access-token JWT identity, while the unchanged DefaultAccountObserver only checks the id_token and stored account_id. An access-token-only default login yields unresolved, and this early return suppresses even independently identifiable sibling/pi cards. A controlled fixture produces zero cards instead of two. Comma-separated CODEX_HOME is also split by discovery but treated as one literal path by the observer, so the promised list support cannot reliably establish a default-source owner. The discovery code’s payload fallback is only used when the id_token cannot be decoded; a decodable id_token missing the claim does not trigger access-token fallback. -
[P2] Carry a discovered JWT account id into usage/reset request credentials. CodexAccountDiscovery.swift:95
A login with an id_token account claim but no stored tokens.account_id creates a correctly identified card. The auth store then returns the original unmodified auth, and usage/reset requests omit ChatGPT-Account-Id. A mocked request test reproduces the missing header. Main’s scoped auth store fills the selected workspace into the request auth state. Discovery and request routing must use the same resolved identity.
Existing main issue, rather than a new PR regression
The Copilot comment about CodexPricingSection is valid behaviorally: every Codex card exposes the global fallback-model preference, but the wait/backoff/refresh code still targets literal codex. Those view files are identical on current main and PR head. Track this as an existing main follow-up. Because the preference is global, recalculation should reach every applicable Codex card; changing only the selected card’s refresh id would still leave other cards stale.
The empty pi-ID scan can also produce a misleading pi source note. It is a smaller presentation issue, not evidence of token/spend leakage by itself. Skip empty scoped scans or derive the note from matching entries.
Verification
| Scope | Result |
|---|---|
| PR’s new multi-account, shell, aggregation, and document suites | 36 passed |
| Existing affected PR auth, resets, native/pi scanners, account registry/cache, fallback pricing, and CLI suites | 104 passed, 1 skipped |
| Current main’s Codex Swap, history, shell, aggregation, and document suites | 43 passed |
| Additional isolated review reproducers | 15 failed tests, 21 assertion failures reproducing the findings above |
Both isolated revisions compiled through swift test. A local Xcode beta/Sparkle test-runner setup issue was resolved before executing the suites. Review reproducers were added only to the temporary review checkout; repository source was unchanged.
These are focused suites, not a new full-suite run. GitHub’s CI run reports failure but contains zero jobs and zero check runs; it provides no build/test result to interpret. A fresh Build and Test run is needed after rebasing. The current approved-issue check is successful.
All account requests and reset consumption in review reproducers use fake HTTP clients. Real credentials were not edited and no live credit was consumed. iCloud transport and a fresh live UI session were not exercised. The supplied screenshot was inspected; it documents the intended cards but cannot verify credential/history ownership.
Recommended revision
Rebase onto current main and add sibling-home/pi discovery as additional source types. Keep xswap discovery, existing composite identities and record IDs, background Keychain reads, matching-only fallback, read-only Swap/pi credentials, current-credential checks, cache history exclusion, and main’s valid account-aware sync export. Add live pi token reload and complete fallback candidates. Do not claim per-account historical spending merely from the current home or provider-slot assignment. Keep the shell-banner parser fix: it is small, independently tested, and still absent from main. Re-run the existing Codex Swap regression suites alongside the new source tests and obtain fresh CI after integration.
Each ChatGPT account signed in on the Mac gets its own Codex card. Logins come from every Codex home (CODEX_HOME, ~/.codex, ~/.config/codex, and sibling ~/.codex-* / ~/.config/codex-* directories) and from pi's auth.json openai-codex* entries, merged by ChatGPT account id. The account at the default home keeps the bare `codex` id; other accounts mint codex@<hash8> cards, seeded with the same layout defaults. Each card scopes its credentials, rollout homes, pi session spend (by pi provider id), and reset-credit claims to its own account. pi credentials are read-only: an expired pi token asks the user to refresh it in pi rather than rotating tokens pi owns under its lockfile. Sync documents now carry Codex identities under the v2 schema so peer Codex history merges by account, while single-account Macs keep writing v1 files older builds accept.
…ling newline A login shell that prints a banner (fastfetch, MOTD) right before the capture command leaves the begin marker in the same NUL-delimited token as the banner, so the parse never found it and every launch kept the previous snapshot. The launch account pass then never ran on such machines. Match the marker as a token suffix instead.
|
@robinebers fixed, re requested |
…sion Upstream ClaudeSwapAccountTests.swift (added in robinebers#1266 Claude Swap account support) pattern-matches MetricLine.badge with 4 tuple elements, but our fork extended the case to 5 by adding an optional `resetsAt: Date?` field for the MiniMax session-reset bar (see `33926a0 refactor(minimax): drive menu-bar reset via badge resetsAt and split provider`). Swift 6.3.2 emits "failed to produce diagnostic for expression" on the unmatched pattern, which surfaces as a fatal error during `swift test`. Adding the missing placeholder restores compilation without changing the asserted behavior. Local-only: not propagated to robinebers/openusage per the user's fork-only convention. Co-Authored-By: Claude Code <noreply@anthropic.com>
There was a problem hiding this comment.
Important
Changes are still needed in credential rotation, sibling-home discovery, refresh fallback, and unresolved default-account handling. Four findings are inline.
Reviewed changes across the complete current PR diff, including the integration with the existing Codex Swap account model.
- Account discovery: Additional Codex homes and pi logins join the existing workspace-and-user identity model and persistent card registry.
- Credential handling: Pi credentials reload read-only, matching candidates remain identity-scoped, and selected Codex homes gain token refresh and write-back.
- Account integration: Catalog construction supplies the new credential sources while retaining existing ambiguous-history exclusion and account-aware sync behavior.
- Shell capture and coverage: Banner-tolerant environment parsing, new account regression tests, provider documentation, and supplied screenshots.
ℹ️ PR Evidence Describes the Earlier Implementation
The description and screenshots still claim per-account local spending and single-account v1 sync. The current revision instead excludes unattributed spending when multiple accounts are known and preserves complete Codex identities in account-aware exports, so that evidence no longer describes the implementation being reviewed.
Technical details
# Update the evidence for the integrated revision
## Affected sites
- PR description: What this changes, Heads-up, Tests, and Screenshots.
- The screenshot links reference 835265ec, before the integration commit.
## Required outcome
- Describe the current spending exclusions and sync compatibility rather than the superseded implementation.
- Refresh the live screenshots and macOS verification results for the revised behavior.
## Review verification
- An isolated Linux Swift check reproduced the hidden-folder omission using the production FileManager options.
- The production CodexProvider+Accounts.swift extension, compiled with minimal injected test doubles, returned an error after a rejected token refresh without trying a valid second credential; that credential succeeded alone.
- Credential ownership was traced through discovery, assembly, auth scoping, and refresh, with the Codex Swap contract checked against upstream source.
- Full SwiftPM tests and a live app rebuild/run were not performed: this runner is Linux and the package requires macOS frameworks. No Build and Test result was listed among this PR's current checks.GPT Astra | 𝕏
| writableAuthHomes: matchingHomes.filter { home in | ||
| !matchingSwaps.contains(where: { $0.home == home }) | ||
| }, |
There was a problem hiding this comment.
[P1] Keep the active Swap home read-only too. This excludes a saved slot's home, but not its mainHome, which discovery explicitly adds to matchingHomes. An active Swap login therefore retains its refresh token and can be rotated by OpenUsage while Codex is running with a cached copy, reintroducing the token-conflict risk that the existing Swap integration deliberately prevents.
Technical details
# Preserve Swap credential ownership
## Affected sites
- Sources/OpenUsage/Services/ProviderAccountAssembly+Codex.swift:33 and 141-143 include a matching Swap mainHome among writable homes.
- Sources/OpenUsage/Providers/Codex/CodexAuthStore+Accounts.swift:20-31 preserves its refresh token because that home is writable.
- Sources/OpenUsage/Providers/Codex/CodexProvider+Accounts.swift:17-29 rotates and saves that credential.
## Required outcome
- Both saved Swap credentials and the active matching main-home credential must retain the existing read-only/access-token-only policy.
- Keep genuinely independent Codex homes writable.
- Add an assembly-to-runtime regression using a Swap registry whose mainHome and saved home initially contain the same credential, and assert no refresh request or write occurs for either source.
## Evidence
- Existing Swap tests build auth stores without the new writableAuthHomes argument, so they do not exercise the production catalog's newly writable main home.
- Upstream documents that the active account uses mainHome and that Codex owns token refresh:
https://github.com/maddada/codex-swap/blob/d217d4b01fb70af193b9914f42918becb4d7f209/README.md
- Credential snapshot/copy behavior:
https://github.com/maddada/codex-swap/blob/d217d4b01fb70af193b9914f42918becb4d7f209/src/account_state.rs| let urls = (try? FileManager.default.contentsOfDirectory( | ||
| at: URL(fileURLWithPath: path), | ||
| includingPropertiesForKeys: [.isDirectoryKey, .isSymbolicLinkKey], | ||
| options: [.skipsHiddenFiles] |
There was a problem hiding this comment.
[P2] Include hidden folders in sibling-home discovery. .skipsHiddenFiles removes every ~/.codex-* directory before candidateHomes() can apply its .codex- prefix filter, so these advertised account sources are never discovered unless explicitly configured elsewhere. Remove that enumeration option and cover the real directory-listing implementation with a temporary hidden-folder fixture; the current tests inject directory names and bypass this failure.
| if authStore.needsRefresh(candidate.auth), | ||
| let refreshToken = candidate.auth.tokens?.refreshToken?.nilIfEmpty { | ||
| let refreshed = try await usageClient.refreshToken(refreshToken) |
There was a problem hiding this comment.
[P2] Continue to matching credentials when a home-token refresh is rejected. If the first writable home has an expired access token and a reused, revoked, or expired refresh token, this new call throws into the outer catch, which immediately returns an error. A valid pi, alternate-home, or Keychain login for the same identity is never attempted, so one stale home prevents the account from refreshing despite its usable fallback.
Technical details
# Preserve candidate fallback on refresh-auth failures
## Affected sites
- Sources/OpenUsage/Providers/Codex/CodexProvider+Accounts.swift:17-19 introduces the throwing refresh call; lines 51-53 terminate the whole card refresh.
- Sources/OpenUsage/Providers/Codex/CodexUsageClient.swift:36-51 converts rejected refresh tokens to CodexAuthError values.
- Sources/OpenUsage/Providers/Codex/CodexProvider.swift:90-98 already distinguishes allowsAuthFallback errors from failures that should terminate refresh.
## Required outcome
- After the current-credential recheck, recoverable authentication failures should advance to the next identity-matching candidate.
- Preserve non-auth error reporting and changed-credential invalidation.
- Add a regression with an expired writable home whose refresh returns refresh_token_reused, followed by a valid matching pi credential; usage must succeed using the latter without writing pi auth.
## Evidence
- Executing the production refreshAccount extension with injected candidates produced only the rejected refresh request and an error snapshot.
- The second candidate succeeded when tested alone.| if let identity = CodexAccountIdentity(auth: auth) { | ||
| return .resolved(identityKey: identity.accountID, label: identity.email, anchor: anchor) |
There was a problem hiding this comment.
[P2] Do not resolve an email-only login to an empty account key. CodexAccountIdentity(auth:) intentionally accepts an email without a workspace and represents its accountID as "", so this now returns .resolved(identityKey: "") where the previous observer returned .unresolved. With no established scoped cards, different email-only logins receive the same empty cache-ownership stamp, preventing the account-switch check from invalidating the previous account's cached limits.
Technical details
# Preserve the strict default-observer identity boundary
## Affected sites
- Sources/OpenUsage/Providers/DefaultAccountObserver.swift:151-152 exports identity.accountID without requiring it to be nonempty.
- Sources/OpenUsage/Providers/Codex/CodexSwapAccount.swift:16-23 permits email-only identities and stores an empty accountID.
- Sources/OpenUsage/Services/ProviderAccountAssembly+Codex.swift:33-37 excludes incomplete identities on an ordinary first launch, so assembly falls back to this observer.
- Sources/OpenUsage/Services/ProviderAccountAssembly.swift:109-116 uses the resolved value as the identity stamp and registry key.
## Required outcome
- The legacy default observer must not emit an empty identity key; a login without a workspace/account ID must remain unresolved.
- Preserve the access-token account-claim fallback added by this PR.
- Add a fixture with a nonempty access token and an id_token containing only email, but neither stored account_id nor an account claim; assert unresolved rather than an empty resolved key.|
This pull request adds 1,176 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. |
|
Run failed. View the logs →
|
|
Run failed. View the logs →
|
|
Run failed. View the logs →
|
|
Run failed. View the logs →
|
|
Warning Your Codex subscription hit its usage limit, so every run stops before the agent starts. Every Pullfrog run on robinebers has failed since September 20 (5 runs, no successes), so this review did not happen. Codex returns "The usage limit has been reached" for your ChatGPT subscription, which means you've used all messages available in the current limit window. No OpenAI API key is stored to fall back on, and the Router balance is empty. To fix it:
|

TL;DR
Adds account-aware Codex cards for Codex Swap, custom Codex homes, and pi logins. Each account keeps its own limits and reset actions, while spending without a reliable owner stays out of multi-account cards and sync.
Fixes #1265
What was happening
What this changes
auth.json, and rejected credentials fall through to the next matching login.Heads-up
Restart OpenUsage after adding, removing, or renaming an account. Close active Codex sessions before using
xswap switch, as required by Codex Swap.Tests
swift buildswift test --filter CodexPR1266ReviewTests(4 passed)CFFIXED_USER_HOME=/tmp/openusage-test-home swift test(1,396 passed, 3 skipped)Screenshots
The current revision shows both Codex cards and no unattributed cost data. The popover sizes to its content, so the two-account view is taller.