Skip to content

Usage → Limits merges two Codex subscriptions that share an email address #10835

Description

@uzairansaruzi

What happened

I have two ChatGPT subscriptions, a Business one and a personal Plus one. I set them up as
two Codex provider instances using the shared CODEX_HOME plus shadow home layout from the
Codex provider docs. Both instances work and both report as authenticated, each showing its
own plan in provider settings.

Under Usage → Limits, only one of the two Codex accounts appears. I expected to see both,
one segment per account, the way the docs describe pooling.

Diagnosis

T3 Code collects usage limits from both instances correctly. The provider status caches for
both instances were refreshed in the same batch and each holds distinct, live data:

Instance Plan Windows reported Used Reset credits
A ChatGPT Business Subscription weekly only 8% 0
B ChatGPT Plus Subscription 5-hour and weekly 0% and 26% 3

The loss happens in the display layer, in packages/shared/src/usageLimits.ts.

accountKey (line 148) identifies a subscription account by provider driver plus lowercased
email address:

function accountKey(driver: ServerProvider["driver"], email: string | undefined): string | null {
  const normalizedEmail = email?.trim().toLowerCase();
  return normalizedEmail ? `${driver}:${normalizedEmail}` : null;
}

collectLimitAccounts (line 187) merges on that key at line 257. Both of my Codex logins
report the same email address, so both instances collapse into one LimitAccount and only
one set of windows survives.

The assumption is stated in the LimitAccount doc comment at line 153:

The same email signed in natively on two environments, or reported by a hub as well as
natively, is one account: its quota is one bucket, so counting it twice would misstate
what is left.

That holds for one subscription signed in twice. It does not hold here. A single ChatGPT
email can carry a personal Plus subscription and separately belong to a Business workspace.
Those are two subscriptions with two independent quotas, so this merge understates what is
available rather than avoiding a double count.

Which of the two survives is arbitrary. The merge keeps the fresher snapshot, compared with
a strict > at line 220. Both instances are probed in the same pass and carry an identical
checkedAt, so the comparison is false and whichever instance is iterated first wins.

The plan string that distinguishes them is already carried on the account as provider.auth.label,
so the two subscriptions are separable with data the collector already has.

Two other call sites use the same key and may want the same treatment. collectLimitSources
uses it at lines 105 and 133. collectProviderUsageLimits at line 639 pushes one entry per
native instance and uses the key only to suppress hub duplicates, so it looks unaffected, but
I did not verify its rendered output.

Steps to reproduce

  1. Have one ChatGPT email address that holds a personal Plus subscription and also belongs to
    a ChatGPT Business workspace. These are two subscriptions with separate quotas.
  2. Sign the first subscription into the default Codex home with codex login.
  3. Sign the second one into a shadow home:
    mkdir -p ~/.codex-t3/personal
    CODEX_HOME=~/.codex-t3/personal codex login
  4. In Settings → Providers, add a second Codex instance. Leave its CODEX_HOME path at the
    default and set its Shadow home path to ~/.codex-t3/personal. Give the two instances
    different display names.
  5. Confirm both instances show as authenticated in provider settings, each naming its own plan.
  6. Open Usage → Limits.

Expected: both Codex accounts appear, each contributing its own windows and its own segment.

Actual: one Codex account appears. The other subscription's quota is not shown anywhere on
the page.

Version

0.0.41-nightly.20260908.1414

Environment

macOS 26.6.2 (arm64), Node v24.14.0, codex-cli 0.153.4

Evidence

Provider status caches, two Codex instances, same refresh batch. Redacted.

instance A (display name "Business")
  auth.label                       ChatGPT Business Subscription
  usageLimits.checkedAt            2026-09-08T20:47:48.157Z
  usageLimits.windows[0].kind      weekly    id=primary    usedPercent=8
  usageLimits.windows[0].resetsAt  2026-09-15T18:08:38.000Z
  usageLimits.resetCredits.availableCount  0

instance B (display name "Personal", shadow home)
  auth.label                       ChatGPT Plus Subscription
  usageLimits.checkedAt            2026-09-08T20:47:48.157Z
  usageLimits.windows[0].kind      session   id=primary    usedPercent=0
  usageLimits.windows[0].resetsAt  2026-09-09T01:47:49.000Z
  usageLimits.windows[1].kind      weekly    id=secondary  usedPercent=26
  usageLimits.windows[1].resetsAt  2026-09-15T04:07:47.000Z
  usageLimits.resetCredits.availableCount   3

The auth email in both caches is byte-identical, confirmed by comparing SHA-256 digests
without reading the values. Identical checkedAt in both, so the line 220 freshness
comparison is false and merge order decides the winner.

Related issues

None found. I searched the tracker for usage limit account merging, duplicate accounts,
pooled limits, and same-email variations, and nothing matches. #9040, #9884, #7170 and
#9676 all touch DPoP or relay authentication and are unrelated.

Fix applied or workaround

None. Nothing was changed on the machine. There is no setting that disables the
deduplication, so there is no in-app workaround. Reading the second subscription requires
going outside T3 Code.

Filed by

Claude Code (claude-opus-5, 1M context) via t3 triage

Activity

  1. juliusmarminge commented on Sep 8, 2026

    @juliusmarminge
    Member

    Triage

    Confirmed. Usage → Limits drops one of two independent Codex subscriptions when they share an email.

    Both instances are collected correctly (distinct plans, windows, reset credits, same checkedAt). The loss is in packages/shared/src/usageLimits.ts: accountKey is driver:normalizedEmail, and collectLimitAccounts merges on that key. The LimitAccount comment treats one email as one quota bucket. That is right for one subscription signed in twice (native + hub, or two environments). It is wrong for a personal Plus plan and a Business workspace on the same ChatGPT email — those are two quotas. Equal checkedAt makes the freshness > false, so iteration order decides which snapshot survives.

    collectLimitSources uses the same key and would hide a hub seat that shares the email. collectProviderUsageLimits on current main still emits one row per native instance, so /usage-limits should still show both.

    This is the opposite of #10701 (double-count the same subscription). #10700 fixes #10701 by applying this email key to native /usage-limits rows and must not be treated as a fix here. Landing #10700 unchanged would extend this over-merge into the slash command.

    Web and mobile both render collectLimitAccounts (UsageLimitsPooled). Pooling itself came from #10300; accountKey from #9584. #9040 / #9884 / #7170 / #9676 are unrelated.

    Fix: keep same-email merge when the plan matches (provider.auth.label / hub plan — Codex already labels Plus vs Business via codexPlanLabel). Split when the plan differs. Add a same-email Plus + Business regression next to the existing “two instances, one environment” test. Coordinate with #10700 so both collectors share that identity. Residual: two Business workspaces with the same email and the same plan string would still collapse.

    No in-app workaround; the second quota is only visible outside T3 Code.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Sep 8, 2026
  3. mattesick commented on Sep 10, 2026

    @mattesick

    Same bug reproduces with Claude, and the proposed fix (split when the plan label differs) only partly covers it.

    Setup

    • One Anthropic login (one email) that belongs to two Claude orgs: a personal org on a Max plan, and a company Team org.
    • Two Claude provider instances in T3 Code, one with the default home and one with CLAUDE_CONFIG_DIR pointing at a second home (~/.claude-team), each signed in to a different org.
    • Both instances authenticate and both report their own limits when probed directly. Anthropic's oauth/usage endpoint returns different session / weekly / model-scoped windows for each token, with different reset times.

    Result

    Usage → Limits shows a single Claude card. accountKey is claudeAgent:<email> for both instances, so collectLimitAccounts merges them. Because the merge keeps the fresher checkedAt, the surviving numbers silently flip between the two orgs depending on which instance was probed last. In my case the card showed the personal org at 36% weekly used while the Team org was at 2%, with nothing indicating which org the card represented.

    Why plan label is not enough for Claude

    Two orgs on the same email can legitimately share a plan string (two Team orgs, or Team plus a Max seat both labelled by tier). For Claude the reliable identity is the org, and it is already on disk: .claude.json in each config dir carries oauthAccount.organizationUuid and organizationName, and they differ between the two instances while accountUuid and emailAddress are identical. Keying the Claude account on driver + organizationUuid (falling back to email when it is absent) would split the two quotas correctly and keep the "same subscription signed in twice" merge working, since a duplicate login to the same org has the same org UUID.

    For Codex the equivalent is chatgpt_account_id in the auth.json token claims, which differs per workspace even when the email and plan match. That would also close the residual "two Business workspaces, same plan string" case noted in triage.

    A small UI improvement regardless of the key: when a card is backed by more than one instance, show the org / workspace name so users can tell which quota they are looking at.

  4. LiquidBacn commented on Oct 3, 2026

    @LiquidBacn

    T3 code shows three of my four accounts from cli proxy.

    Same issue, two share an email address.

  5. WhiteWarrior625 commented on Oct 3, 2026

    @WhiteWarrior625

    Confirmed the Claude organization variant on Linux desktop with T3 Code Nightly 0.0.46-nightly.20261003.2632, build commit f391794a35c604d57e166a3ab48d56fc6e4e469a, and Claude Code 2.1.288. Adding this here because #13674 was closed as a duplicate of this issue.

    Two configured Claude instances are enabled, installed, ready, and authenticated. They have the same email but different oauthAccount.organizationUuid values in their separate Claude config directories. One is a Team subscription and the other is Max. Usage → Limits hides the Max instance under the Team instance's display name.

    The current T3 provider caches contain distinct quota data. No credentials or personal identifiers are included below.

    Instance Auth label Usage checked at, UTC Weekly used Weekly reset, UTC
    Enterprise Claude Team Subscription 2026-10-03 17:25:46.469 62% 2026-10-07 17:00:00.133
    Work Claude Max Subscription 2026-10-03 17:25:48.107 41% 2026-10-06 11:59:59.807

    Work also has a separate seven_day_fable window at 3% used. Both caches report one reset credit.

    The installed frontend matches upstream's accountKey at this build commit: when an email is present, the key is only driver:normalizedEmail. The collector retains the first instance's display name and the freshest quota snapshot, so separate organizations can appear as one named account with the other organization's usage numbers.

    The existing same-email, separate-organization reproduction still applies on this nightly. The account identity needs to distinguish organizations while continuing to merge duplicate logins to the same organization.

  6. alex73630 commented on Oct 9, 2026

    @alex73630

    Written and posted by an AI agent (GPT-6-Astra through the Codex harness in T3 Code) on behalf of @alex73630, at their request. The observations below are the user's.

    Friendly bump — I'm seeing the same issue with Orchestrator V2. I have two ChatGPT subscriptions on the same email: one on my Personal workspace and one on my Team workspace. Usage → Limits only shows one, and which subscription it represents looks arbitrary to me.

    This makes the Limits tab mostly unusable for my setup: I can't reliably check the remaining quota for each subscription. I'd like both workspaces to appear separately, with clear labels and their own usage limits and reset times.

    Could #10845 and #14750 get a review toward merging, once any remaining issues are addressed? #10845 addresses the Codex case I'm experiencing, while #14750 covers the related Claude organization case reported here. I haven't tested either patch locally; this is confirmation of the current problem and a request to help move the fixes forward.

    Thanks for the work on these fixes — having reliable limits for each subscription would make this page much more useful!

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

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions