Skip to content

Usage tab hides a Codex workspace seat that shares an email with a personal account #9058

Closed

Description

@tsavo-at-pieces

Where did this happen?

Desktop app (macOS)

Version

2.0.0. The same code is on main at 86e667a.

What happened?

One email can hold a personal ChatGPT account and a seat in a ChatGPT workspace. I sign each one in to Codex from its own CODEX_HOME. Settings → Usage → Codex shows only one card for that email, the personal one. The workspace seat's card is missing while both logins work.

The workspace card shows up only when its usage read fails, and then it reads "Sign-in expired". So a workspace seat is visible only while it is broken.

A hidden home also can't be made the default or removed. Both mutations check the selection against the same list and answer BAD_REQUEST ("No codex login found at …").

Expected: one card per ChatGPT account. The personal account and the workspace seat each get a card with their own plan and weekly window.

Steps to reproduce

  1. Sign in with a personal ChatGPT account: codex login (default ~/.codex).
  2. Sign in to a workspace seat on the same email from another home: CODEX_HOME=~/.codex-work codex login, and choose the workspace.
  3. Open Settings → Usage. Only one Codex card shows for that email.

Cause

fetchCodexAccounts() in packages/host-service/src/trpc/router/usage/codex.ts dedupes the cards by account.email ?? account.accountKey. The default home sorts first, so the workspace seat is dropped whenever both reads succeed. A failed read has no email, so its key falls back to its auth.json path and it is not deduped. That is why the seat appears only when it is broken.

The fix is to key the dedupe by email plus the ChatGPT account id. The same file already reads that id from tokens.account_id to send as chatgpt-account-id. A PR follows.

Activity

  1. github-actions commented on Oct 29, 2026

    @github-actions
    Contributor

    I've confirmed the dedupe bug in codex.ts and checked the related mutations, so I'm writing up the triage comment now.

    Summary

    Two Codex logins can share one email, for example a personal ChatGPT account and a ChatGPT workspace seat, each signed in from its own CODEX_HOME. When both usage reads succeed, Settings → Usage shows only one card for that email. The workspace seat's card appears only when its usage read fails, and then it shows "Sign-in expired". While the seat is hidden, it also can't be made the default or removed: both mutations look for the selection in the same deduped list and fail with BAD_REQUEST. Reading the code confirms the report and the cause it gives.

    Affected code

    • packages/host-service/src/trpc/router/usage/codex.ts:115-123: fetchCodexAccounts() dedupes cards with account.email ?? account.accountKey and keeps the first one. discoverCodexHomes() puts the default home first (codex.ts:107-109), so the personal ~/.codex card wins and the workspace card is dropped.
    • codex.ts:147 and codex.ts:185-191: a failed read returns email: null, so the key falls back to accountKey, which is the auth.json path (codex.ts:204). Each home's path is different, so a failed card is never deduped. That is why the seat shows up only while it is broken.
    • packages/host-service/src/trpc/router/usage/usage.ts:208-219 (set default) and usage.ts:258-262 (remove): both check account.selection === input.selection against getQuota(false), which is the deduped list. A hidden home's selection is never found, so the mutation throws No codex login found at ….

    Root cause

    The dedupe treats the email as the account identity. ChatGPT can put several accounts under one email (personal plus workspace or team seats), and tokens.account_id is what tells them apart. codex.ts already reads that id (codex.ts:241) and sends it as chatgpt-account-id (codex.ts:152), but never uses it for the dedupe. Other code in the repo already keys Codex identity on the account id: session-account/session-account.ts:60-72 hashes [account_id, sub, email].

    Reproduction

    Manual:

    1. Run codex login and sign in with a personal account (writes ~/.codex/auth.json).
    2. Run CODEX_HOME=~/.codex-work codex login and choose a workspace on the same email.
    3. Open Settings → Usage. Only one Codex card shows.

    bun:test sketch, with no network:

    • Create two temporary Codex homes. Give each an auth.json with a different tokens.account_id.
    • Stub discoverCodexHomes (or move the dedupe into a pure helper and test that) and stub fetch so both requests return { email: "same@x.com", plan_type, rate_limit: {...} }.
    • Assert that fetchCodexAccounts() returns 2 accounts. It returns 1 today.
    • Add a control case: two homes with the same account_id and the same email should still collapse to 1 card.

    Fix direction

    • Use email plus tokens.account_id as the dedupe key. If either value is missing, fall back to accountKey as today, which also covers API-key homes.
    • account_id is not on UsageAccount today. Either return it from fetchCodexAccountForHome for the key only, or build the key there and filter on it before the account goes into the list. Don't add it to the client-facing type unless it's needed, because released clients call this API (see packages/trpc/AGENTS.md).
    • Optional hardening: check the selection in usage.ts:208 and :258 against the discovered homes rather than the deduped quota list. A home that is hidden for any reason could then still be made the default or removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions