Skip to content

fix(menu): label quota windows by duration, not position (#635) - #636

Merged
ndycode merged 3 commits into
mainfrom
fix/635-monthly-quota-window-label
Jul 23, 2026
Merged

fix(menu): label quota windows by duration, not position (#635)#636
ndycode merged 3 commits into
mainfrom
fix/635-monthly-quota-window-label

Conversation

@ndycode

@ndycode ndycode commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Fixes #635.

Problem

Codex Business accounts use a 30-day monthly quota as their primary window. The account menu, however, hardcoded the positional labels 5h (primary) and 7d (secondary), so a monthly window rendered as:

Limits: 5h ##------- 18% reset 28d 11h | 7d ########## 100%

The 18% and reset 28d 11h values were already correct — the 5h label was the only thing wrong (a 5-hour window that resets in 28 days is the giveaway). The percent/reset data flows correctly; only the window's identity was mislabeled.

Root cause

quota-probe already parses windowMinutes per window and the quota cache persists it. But toExistingAccountInfo() projected the parsed snapshot into the view model by position (primary → quota5h*, secondary → quota7d*) and dropped windowMinutes, and auth-menu-builder's formatQuotaSummary() then hardcoded the "5h"/"7d" labels. The compact quota summary string (formatAccountQuotaSummary) already labels by duration, so the two surfaces disagreed.

Fix

  • Carry windowMinutes through the ExistingAccountInfo / AccountInfo view models as quotaPrimaryWindowMinutes / quotaSecondaryWindowMinutes.
  • Populate them in toExistingAccountInfo() from the (already-cached) entry.primary/secondary.windowMinutes.
  • Derive the displayed label from the duration in auth-menu-builder, matching the Nd/Nh/Nm scheme the summary string already uses. A 30d window now renders 30d.
  • Fallback preserved: when a window's duration is unknown (e.g. quota entries cached before durations were persisted), the label falls back to the positional 5h/7d, so Plus/Pro rows are unchanged.

The existing quota5h*/quota7d* field names are kept (they've always meant primary/secondary by position) to keep the change surgical; a comment now documents that the duration lives in the new fields.

Scope note

Sorting (compareReadyFirstAccounts et al.) keys off the primary/secondary percent fields, which are correct numbers independent of the label, so no sort changes were needed.

Tests

  • New regression tests in auth-menu-builder.test.ts: a 30d monthly primary window renders 30d (and no longer 5h), and the unknown-duration fallback still yields 5h/7d.
  • Full suite green: 5188 passed, 3 skipped. Lint + typecheck clean.

🤖 Generated with Claude Code

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr fixes the mislabeled quota window for codex business accounts: a 30-day monthly primary window was displayed as "5h" because the menu hardcoded positional labels. the fix carries windowMinutes through the view model and derives the label from duration, with a positional fallback for old cache entries.

  • adds quotaPrimaryWindowMinutes / quotaSecondaryWindowMinutes to ExistingAccountInfo and AccountInfo, populated in toExistingAccountInfo() from the already-cached windowMinutes fields.
  • introduces resolveQuotaWindowLabel() in auth-menu-builder.ts (exported) to convert minutes → Nd/Nh/Nm labels, and applies it in both formatQuotaSummary and readQuotaLeftPercent (login-menu-data sort helpers) so the summary-string parse path uses the real label rather than the hardcoded positional one.
  • regression tests cover all three label branches, invalid-duration fallbacks, summary-string recovery, and sort ordering for monthly-window accounts.

Confidence Score: 5/5

safe to merge — the change is additive and surgical, existing fields are untouched, and the fallback path preserves plus/pro behavior.

the fix correctly threads windowMinutes through the view model and resolves labels from real duration in both the display path and the sort/parse path. regression tests cover all three label branches, invalid-duration guards, summary-string recovery, and sort ordering. no data loss, no token handling changes, no concurrency surface touched.

no files require special attention.

Important Files Changed

Filename Overview
lib/ui/auth-menu-builder.ts adds resolveQuotaWindowLabel (exported), updates formatQuotaSummary to resolve labels before parsing, widens label param types from literal "5h"
lib/codex-manager/login-menu-data.ts carries windowMinutes through toExistingAccountInfo, switches readQuotaLeftPercent to primary/secondary enum, imports resolveQuotaWindowLabel from the ui layer — creates a data→ui dependency that could be cleaner but is not a bug
lib/cli.ts adds quotaPrimaryWindowMinutes and quotaSecondaryWindowMinutes optional fields to ExistingAccountInfo — additive, backward-compatible
test/auth-menu-builder.test.ts new regression tests cover 30d label, fallback, all three resolveQuotaWindowLabel branches (d/h/m), invalid-duration guard, and summary-string recovery path — good coverage
test/login-menu-data.test.ts adds integration tests asserting windowMinutes flows from cache to row fields and that sort order reflects real headroom for monthly-window accounts

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[quota-probe: windowMinutes cached per window] --> B[toExistingAccountInfo]
    B --> C[quotaPrimaryWindowMinutes\nquotaSecondaryWindowMinutes\nin ExistingAccountInfo / AccountInfo]

    C --> D[resolveQuotaWindowLabel]
    D --> E{windowMinutes valid?}
    E -- no --> F[fallback: '5h' or '7d']
    E -- yes --> G{divisible by 1440?}
    G -- yes --> H["Nd label (e.g. '30d')"]
    G -- no --> I{divisible by 60?}
    I -- yes --> J["Nh label (e.g. '5h')"]
    I -- no --> K["Nm label (e.g. '100m')"]

    H --> L[formatQuotaSummary / formatQuotaWindow]
    J --> L
    K --> L
    F --> L

    H --> M[parseLeftPercentFromSummary with resolved label]
    J --> M
    K --> M
    F --> M

    L --> N[menu row: '30d ##---- 18% reset 28d 11h']
    M --> N
Loading

Reviews (3): Last reviewed commit: "fix(menu): rank rows by window duration ..." | Re-trigger Greptile

Codex Business accounts carry a 30d monthly primary window, but the
account menu hardcoded the positional "5h"/"7d" labels. A monthly window
therefore rendered as "5h ... reset 28d 11h" — the percent and reset were
correct, only the label lied about the window it belonged to.

Plumb each window's windowMinutes (already parsed by quota-probe and
persisted in the quota cache) through the ExistingAccountInfo/AccountInfo
view models, and derive the displayed label from it — the same
data-driven labeling the compact quota summary already uses. Falls back
to the positional "5h"/"7d" labels when a window's duration is unknown
(e.g. entries cached before durations were persisted), so Plus/Pro rows
are unchanged.

Adds regression coverage for a 30d monthly primary window and for the
unknown-duration fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfmQjR1jh1pb6n4YPiHDsJ
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ndycode, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 06b27302-2365-4667-93fa-1760cee1eab3

📥 Commits

Reviewing files that changed from the base of the PR and between 7f01e23 and 43e390b.

📒 Files selected for processing (3)
  • lib/codex-manager/login-menu-data.ts
  • lib/ui/auth-menu-builder.ts
  • test/login-menu-data.test.ts
📝 Walkthrough

Walkthrough

quota window durations now propagate from cached account data into login menu rendering. the ui derives labels such as 30d when valid durations exist and retains 5h/7d fallback labels when they do not.

Changes

Quota window metadata

Layer / File(s) Summary
Quota metadata propagation
lib/cli.ts:71, lib/codex-manager/login-menu-data.ts:468, lib/ui/auth-menu-builder.ts:30
ExistingAccountInfo and AccountInfo expose optional primary and secondary window durations, populated from cached quota entries.
Data-driven quota rendering
lib/ui/auth-menu-builder.ts:249, lib/ui/auth-menu-builder.ts:329, lib/ui/auth-menu-builder.ts:379
resolveQuotaWindowLabel derives Nd/Nh/Nm labels, falls back to positional labels, and applies resolved labels when parsing and rendering quota summaries.
Regression coverage
test/auth-menu-builder.test.ts:268, test/auth-menu-builder.test.ts:295, test/auth-menu-builder.test.ts:314, test/auth-menu-builder.test.ts:350, test/auth-menu-builder.test.ts:369, test/login-menu-data.test.ts:312
Tests cover monthly labels, fallback behavior, multiple duration mappings, invalid values, summary percent parsing, and cached metadata propagation. fractional and unusually large duration edge cases are not covered. no concurrency-related changes or tests are present.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: bug

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed the title matches the main change: quota windows are labeled by duration instead of position.
Description check ✅ Passed the description covers the problem, root cause, fix, and tests; the template sections are mostly optional here.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/635-monthly-quota-window-label
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/635-monthly-quota-window-label

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread lib/ui/auth-menu-builder.ts Outdated
Comment thread test/auth-menu-builder.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/codex-manager/login-menu-data.ts`:
- Around line 468-469: The existing tests do not cover the quota-duration
mapping in toExistingAccountInfo. Add a deterministic Vitest regression that
supplies a 30-day cached entry, maps it through toExistingAccountInfo, and
renders the resulting account information, asserting both primary and secondary
durations are preserved and the rendered label reflects 30 days.

In `@lib/ui/auth-menu-builder.ts`:
- Around line 381-389: The quota summary parsing in the auth-menu builder still
uses hard-coded “5h” and “7d” windows while rendering resolved labels. In the
function containing primaryLabel, secondaryLabel, and
parseLeftPercentFromSummary, resolve the labels before parsing and pass
primaryLabel and secondaryLabel to the parser so custom windows such as “30d”
are recognized. Add a regression in the existing auth-menu builder tests
covering summary-only input without positional percentage fields.

In `@test/auth-menu-builder.test.ts`:
- Around line 295-309: Expand the test coverage for the duration-label logic
used by formatAccountHint, adding deterministic cases for 0, -1, Number.NaN,
Number.POSITIVE_INFINITY, 12h, and 90m. Assert positional 5h/7d fallbacks for
invalid or non-day durations as appropriate, and assert the derived
hourly/minute labels for valid 12h and 90m values, preserving the existing
undefined-duration regression case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: da657572-7455-424a-811d-49d4bdac40bd

📥 Commits

Reviewing files that changed from the base of the PR and between a35f7c8 and 9603e05.

📒 Files selected for processing (4)
  • lib/cli.ts
  • lib/codex-manager/login-menu-data.ts
  • lib/ui/auth-menu-builder.ts
  • test/auth-menu-builder.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only ("type": "module"), Node >= 18.17

Files:

  • lib/cli.ts
  • test/auth-menu-builder.test.ts
  • lib/codex-manager/login-menu-data.ts
  • lib/ui/auth-menu-builder.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Do not use as any, @ts-ignore, or @ts-expect-error in TypeScript files

Files:

  • lib/cli.ts
  • test/auth-menu-builder.test.ts
  • lib/codex-manager/login-menu-data.ts
  • lib/ui/auth-menu-builder.ts
{lib,scripts}/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Windows filesystem safety: retry transient EBUSY/EPERM/ENOTEMPTY cleanup and write failures where tests cover Windows locks

Files:

  • lib/cli.ts
  • lib/codex-manager/login-menu-data.ts
  • lib/ui/auth-menu-builder.ts
**/*.{js,ts,mjs,cjs}

📄 CodeRabbit inference engine (README.md)

**/*.{js,ts,mjs,cjs}: Keep npm installation scripts side-effect-free; postinstall may print a short notice but must not modify runtime state or perform setup, especially in CI or non-interactive installs.
Never run npm install or update commands automatically; only display a manual upgrade notice when appropriate.
Do not publish or take ownership of a global codex binary; preserve the official OpenAI installation as the owner of the codex command.
Keep runtime rotation and local bridge services loopback-only, and protect local bridge access with hashed client tokens.
Keep OAuth credentials and account state local; do not send them to external services as part of normal account management.
Treat Responses background mode as opt-in: requests with background: true must use stateful store=true, while default stateless routing uses store=false.
Use bounded outbound request budgets, avoid whole-pool replay when every account is rate-limited, and enter cooldown after repeated cross-account 5xx bursts.
Make experimental synchronization and backup flows non-destructive by default: preview before applying sync, preserve destination-only accounts, and fail safely on backup filename collisions.

Files:

  • lib/cli.ts
  • test/auth-menu-builder.test.ts
  • lib/codex-manager/login-menu-data.ts
  • lib/ui/auth-menu-builder.ts
lib/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/*.ts: Route all public exports through lib/index.ts or documented package subpaths.
Keep module dependencies acyclic and preserve the layering types/constants → storage → accounts → runtime → manager/CLI; lower layers must not import higher layers.
Preserve runtime rotation pass-through semantics except for intentionally changed auth or provider headers.
Deduplicate emails using normalizeEmailKey(), which trims and lowercases the email.
Use classes for state requiring multiple independent instances or dependency injection, including AccountManager, CircuitBreaker, SessionAffinityStore, and the CodexError hierarchy. Reserve module-level state for genuinely process-global concerns and provide a test reset helper for such state.
Never import from dist/ in source tests or library code.
Never suppress type errors.
Never patch official Codex application binaries for desktop routing.
Never use bare recursive cleanup in Windows-sensitive paths without retry handling.

Files:

  • lib/cli.ts
  • lib/codex-manager/login-menu-data.ts
  • lib/ui/auth-menu-builder.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/cli.ts
  • lib/codex-manager/login-menu-data.ts
  • lib/ui/auth-menu-builder.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

test/**/*.test.ts: Write Vitest test suites with globals enabled (describe, it, expect)
Maintain 80%+ coverage threshold across statements, branches, functions, and lines
Use removeWithRetry() for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY, EPERM, and ENOTEMPTY errors
Do not rely on dist/ in tests; use source files instead
Do not skip tests without justification
Relax lint rules for test files as configured in eslint.config.js

Files:

  • test/auth-menu-builder.test.ts
{scripts/**/*.js,test/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use bare recursive delete logic in Windows-sensitive scripts/tests without retry handling

Files:

  • test/auth-menu-builder.test.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/auth-menu-builder.test.ts
{scripts/*.js,lib/codex-manager/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

Canonical package name is codex-multi-auth; canonical command family is codex-multi-auth ...

Files:

  • lib/codex-manager/login-menu-data.ts
lib/{ui,codex-manager/settings-hub}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Settings Q hotkey = cancel without save; theme live-preview restores baseline on cancel

Files:

  • lib/ui/auth-menu-builder.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.

Applied to files:

  • test/auth-menu-builder.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.

Applied to files:

  • test/auth-menu-builder.test.ts
🔇 Additional comments (4)
lib/cli.ts (1)

71-72: LGTM!

lib/ui/auth-menu-builder.ts (2)

30-39: LGTM!


329-350: LGTM!

Also applies to: 352-353

test/auth-menu-builder.test.ts (1)

268-293: LGTM!

Comment thread lib/codex-manager/login-menu-data.ts
Comment thread lib/ui/auth-menu-builder.ts
Comment thread test/auth-menu-builder.test.ts
The summary-string fallback still searched for a literal "5h"/"7d". That string is produced by formatAccountQuotaSummary, which already labels segments by duration, so a Business row reads "30d 42%, ..." — the lookup found nothing and silently dropped the whole segment (bar and percentage). Resolve the window labels before parsing and pass them in; parseLeftPercentFromSummary now takes a plain string label (its matching is length-driven, so multi-char labels parse correctly, and resolveQuotaWindowLabel already emits lowercase).

Note: with today's only producer this fallback is unreachable — the typed percent and the summary segment both derive from usedPercent and disappear together — so the defect is latent rather than live. It is still wrong by construction (the fallback can only ever succeed for 5h/7d windows), and is now covered by a test.

Adds regression coverage for every duration->label branch (1d/30d boundary, 5h/12h, 100m/90m), for invalid durations (0, -1, NaN, Infinity) falling back to the positional labels, for summary-string recovery via the resolved label, and a mapping-level test proving toExistingAccountInfo carries the cached windowMinutes onto the row (a field-name drift there would restore the "5h" label with every builder test still green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfmQjR1jh1pb6n4YPiHDsJ
The sort helpers resolved a row's quota percentage by building a regex from the positional "5h"/"7d" label. The summary string they fall back to is labelled by DURATION, so a Codex Business row reads "30d 18%, ...": the lookup found nothing, scored the account -1, and sank it to the bottom of a ready-first sort. readQuotaLeftPercent now selects the window by position (primary/secondary) and derives the summary label from that window's real duration, reusing the same resolveQuotaWindowLabel the renderer uses (now exported).

As with the renderer's summary fallback, this path is latent today — toExistingAccountInfo derives the typed percent and the summary segment from the same usedPercent, so they are always present or absent together and the fallback is unreachable through the only producer. The lookup was still wrong by construction. Adds a regression test that a monthly-window account is ranked by its real headroom, which guards the primary/secondary mapping this refactor touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfmQjR1jh1pb6n4YPiHDsJ
@ndycode
ndycode merged commit 87d646a into main Jul 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Accounts with monthly quotas (Codex Business) is shown as 5h in login

1 participant