fix(menu): label quota windows by duration, not position (#635) - #636
Conversation
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
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Warning Review limit reached
Next review available in: 5 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughquota window durations now propagate from cached account data into login menu rendering. the ui derives labels such as ChangesQuota window metadata
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
lib/cli.tslib/codex-manager/login-menu-data.tslib/ui/auth-menu-builder.tstest/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.tstest/auth-menu-builder.test.tslib/codex-manager/login-menu-data.tslib/ui/auth-menu-builder.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Do not use
as any,@ts-ignore, or@ts-expect-errorin TypeScript files
Files:
lib/cli.tstest/auth-menu-builder.test.tslib/codex-manager/login-menu-data.tslib/ui/auth-menu-builder.ts
{lib,scripts}/**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Windows filesystem safety: retry transient
EBUSY/EPERM/ENOTEMPTYcleanup and write failures where tests cover Windows locks
Files:
lib/cli.tslib/codex-manager/login-menu-data.tslib/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 globalcodexbinary; preserve the official OpenAI installation as the owner of thecodexcommand.
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 withbackground: truemust use statefulstore=true, while default stateless routing usesstore=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.tstest/auth-menu-builder.test.tslib/codex-manager/login-menu-data.tslib/ui/auth-menu-builder.ts
lib/**/*.ts
📄 CodeRabbit inference engine (lib/AGENTS.md)
lib/**/*.ts: Route all public exports throughlib/index.tsor documented package subpaths.
Keep module dependencies acyclic and preserve the layeringtypes/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 usingnormalizeEmailKey(), which trims and lowercases the email.
Use classes for state requiring multiple independent instances or dependency injection, includingAccountManager,CircuitBreaker,SessionAffinityStore, and theCodexErrorhierarchy. Reserve module-level state for genuinely process-global concerns and provide a test reset helper for such state.
Never import fromdist/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.tslib/codex-manager/login-menu-data.tslib/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.tslib/codex-manager/login-menu-data.tslib/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 iscodex-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!
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
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) and7d(secondary), so a monthly window rendered as:The
18%andreset 28d 11hvalues were already correct — the5hlabel 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-probealready parseswindowMinutesper window and the quota cache persists it. ButtoExistingAccountInfo()projected the parsed snapshot into the view model by position (primary → quota5h*,secondary → quota7d*) and droppedwindowMinutes, andauth-menu-builder'sformatQuotaSummary()then hardcoded the"5h"/"7d"labels. The compact quota summary string (formatAccountQuotaSummary) already labels by duration, so the two surfaces disagreed.Fix
windowMinutesthrough theExistingAccountInfo/AccountInfoview models asquotaPrimaryWindowMinutes/quotaSecondaryWindowMinutes.toExistingAccountInfo()from the (already-cached)entry.primary/secondary.windowMinutes.auth-menu-builder, matching theNd/Nh/Nmscheme the summary string already uses. A 30d window now renders30d.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 (
compareReadyFirstAccountset al.) keys off the primary/secondary percent fields, which are correct numbers independent of the label, so no sort changes were needed.Tests
auth-menu-builder.test.ts: a 30d monthly primary window renders30d(and no longer5h), and the unknown-duration fallback still yields5h/7d.🤖 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
windowMinutesthrough the view model and derives the label from duration, with a positional fallback for old cache entries.quotaPrimaryWindowMinutes/quotaSecondaryWindowMinutestoExistingAccountInfoandAccountInfo, populated intoExistingAccountInfo()from the already-cachedwindowMinutesfields.resolveQuotaWindowLabel()inauth-menu-builder.ts(exported) to convert minutes →Nd/Nh/Nmlabels, and applies it in bothformatQuotaSummaryandreadQuotaLeftPercent(login-menu-data sort helpers) so the summary-string parse path uses the real label rather than the hardcoded positional one.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
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 --> NReviews (3): Last reviewed commit: "fix(menu): rank rows by window duration ..." | Re-trigger Greptile