fix(codex): restore five-hour usage window

This commit is contained in:
Brennan Benson
2026-07-29 15:27:41 -07:00
parent 857f28554b
commit 1dca7c7142
8 changed files with 207 additions and 45 deletions
@@ -97,6 +97,46 @@ describe('Codex backend rate-limit requests', () => {
)
})
it('classifies a sole seven-day backend primary window as weekly', async () => {
readFileMock.mockResolvedValue(
JSON.stringify({
tokens: { access_token: 'access-token', account_id: 'account-id' }
})
)
vi.mocked(fetch)
.mockResolvedValueOnce({
ok: true,
json: async () => ({
plan_type: 'plus',
rate_limit: {
primary_window: {
used_percent: 37,
limit_window_seconds: 7 * 24 * 60 * 60,
reset_at: 1_800_000_000
},
secondary_window: null
}
})
} as Response)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ available_count: 0, credits: [] })
} as Response)
await expect(
fetchCodexRateLimits({
codexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\account\\home'
})
).resolves.toMatchObject({
session: null,
weekly: { usedPercent: 37, windowMinutes: 10_080, resetsAt: 1_800_000_000_000 },
status: 'ok'
})
expect(childSpawnMock).not.toHaveBeenCalled()
expect(ptySpawnMock).not.toHaveBeenCalled()
})
it('aborts callers while sharing one stalled backend auth read', async () => {
let resolveRead!: (content: string) => void
readFileMock.mockImplementation(
@@ -385,6 +385,53 @@ describe('fetchCodexRateLimits', () => {
})
})
it('fills a restored backend 5-hour window when RPC only reports weekly usage', async () => {
const rpcChild = makeRpcChild()
childSpawnMock.mockReturnValue(rpcChild)
respondToRpcRateLimitRead(rpcChild, {
primary: { usedPercent: 22, windowDurationMins: 10080 },
secondary: null
})
readFileMock.mockResolvedValue(
JSON.stringify({
tokens: { access_token: 'access-token', account_id: 'account-id' }
})
)
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({
plan_type: 'plus',
rate_limit: {
primary_window: {
used_percent: 7,
limit_window_seconds: 5 * 60 * 60,
reset_at: 1_800_000_000
},
secondary_window: {
used_percent: 23,
limit_window_seconds: 7 * 24 * 60 * 60,
reset_at: 1_800_100_000
}
},
rate_limit_reset_credits: {
available_count: 1,
credits: [{ status: 'available', expires_at: '2027-01-15T12:00:00Z' }]
}
})
} as Response)
const resultPromise = fetchCodexRateLimits()
await vi.advanceTimersByTimeAsync(1)
await vi.advanceTimersByTimeAsync(1)
await expect(resultPromise).resolves.toMatchObject({
session: { usedPercent: 7, windowMinutes: 300, resetsAt: 1_800_000_000_000 },
weekly: { usedPercent: 23, windowMinutes: 10080, resetsAt: 1_800_100_000_000 },
status: 'ok'
})
expect(fetch).toHaveBeenCalledTimes(1)
})
it('does not map a duplicate session-duration window into the weekly slot', async () => {
const rpcChild = makeRpcChild()
childSpawnMock.mockReturnValue(rpcChild)
+67 -25
View File
@@ -15,8 +15,8 @@ import {
classifyCodexRateLimitWindows,
CODEX_SESSION_WINDOW_MINUTES,
CODEX_WEEKLY_WINDOW_MINUTES,
type CodexRpcRateLimits,
type CodexRpcRateWindow
type CodexRateLimitWindowsSnapshot,
type CodexRateWindowSnapshot
} from './codex-rate-limit-window-classification'
import { resolveCodexCommand } from '../codex-cli/command'
import { withMacTailscaleDnsHint } from '../network/macos-tailscale-dns-diagnostic'
@@ -83,7 +83,7 @@ type RateLimitResetCredits = {
// Why: the Codex app-server wraps rate limit data as { rateLimits: { primary, secondary, ... } }.
type RpcRateLimitsResponse = {
rateLimits?: CodexRpcRateLimits | null
rateLimits?: CodexRateLimitWindowsSnapshot | null
rateLimitResetCredits?: {
availableCount?: number
totalEarnedCount?: number
@@ -454,7 +454,7 @@ export async function consumeCodexRateLimitResetCredit(options: {
}
function mapRpcWindow(
raw: CodexRpcRateWindow | null | undefined,
raw: CodexRateWindowSnapshot | null | undefined,
expectedWindowMinutes: number
): RateLimitWindow | null {
if (!raw || typeof raw.usedPercent !== 'number' || !Number.isFinite(raw.usedPercent)) {
@@ -489,27 +489,30 @@ function mapRpcWindow(
}
}
function mapBackendUsageWindow(
raw: BackendRateLimitWindow | null | undefined,
fallbackWindowMinutes: number
): RateLimitWindow | null {
const limitWindowSeconds = raw?.limit_window_seconds
// Why: match Codex backend-client's window_minutes_from_seconds — actual bucket duration, rounding partial minutes up.
const windowMinutes =
function backendWindowToSnapshot(
raw: BackendRateLimitWindow | null | undefined
): CodexRateWindowSnapshot | null {
if (!raw) {
return null
}
const limitWindowSeconds = raw.limit_window_seconds
const windowDurationMins =
typeof limitWindowSeconds === 'number' &&
Number.isFinite(limitWindowSeconds) &&
limitWindowSeconds > 0
? Math.ceil(limitWindowSeconds / 60)
: fallbackWindowMinutes
return mapRpcWindow(
raw
? {
usedPercent: raw.used_percent,
resetsAt: raw.reset_at
}
: undefined,
windowMinutes
)
: undefined
return { usedPercent: raw.used_percent, windowDurationMins, resetsAt: raw.reset_at }
}
function snapshotWindowMinutes(
snapshot: CodexRateWindowSnapshot | null,
fallbackWindowMinutes: number
): number {
const duration = snapshot?.windowDurationMins
return typeof duration === 'number' && Number.isFinite(duration) && duration > 0
? duration
: fallbackWindowMinutes
}
async function fetchViaBackend(
@@ -534,10 +537,20 @@ async function fetchViaBackend(
if (typeof payload.plan_type !== 'string') {
return null
}
const classified = classifyCodexRateLimitWindows({
primary: backendWindowToSnapshot(payload.rate_limit?.primary_window),
secondary: backendWindowToSnapshot(payload.rate_limit?.secondary_window)
})
return {
provider: 'codex',
session: mapBackendUsageWindow(payload.rate_limit?.primary_window, 300),
weekly: mapBackendUsageWindow(payload.rate_limit?.secondary_window, 10080),
session: mapRpcWindow(
classified.session,
snapshotWindowMinutes(classified.session, CODEX_SESSION_WINDOW_MINUTES)
),
weekly: mapRpcWindow(
classified.weekly,
snapshotWindowMinutes(classified.weekly, CODEX_WEEKLY_WINDOW_MINUTES)
),
// Surfaced for the status-bar Usage row (e.g. "Codex · Plus").
planType: payload.plan_type,
...(payload.rate_limit_reset_credits !== undefined
@@ -552,6 +565,33 @@ async function fetchViaBackend(
}
}
async function withBackendSessionWindow(
limits: ProviderRateLimits,
options?: FetchCodexRateLimitsOptions
): Promise<ProviderRateLimits> {
if (options?.signal?.aborted || limits.session || !limits.weekly) {
return limits
}
try {
const backend = await fetchViaBackend(options)
if (!backend?.session) {
return limits
}
return {
...limits,
session: backend.session,
weekly: backend.weekly ?? limits.weekly,
planType: backend.planType ?? limits.planType,
...(backend.rateLimitResetCredits !== undefined
? { rateLimitResetCredits: backend.rateLimitResetCredits }
: {}),
updatedAt: backend.updatedAt
}
} catch {
return limits
}
}
// ---------------------------------------------------------------------------
// RPC fetch — spawn `codex -s read-only -a untrusted app-server`
// ---------------------------------------------------------------------------
@@ -1133,7 +1173,8 @@ export async function fetchCodexRateLimits(
return abortedCodexRateLimitResult()
}
if (rpcResult.status === 'ok' || rpcResult.status === 'unavailable') {
const withResetCredits = await withBackendRateLimitResetCredits(rpcResult, options)
const withSession = await withBackendSessionWindow(rpcResult, options)
const withResetCredits = await withBackendRateLimitResetCredits(withSession, options)
return options?.signal?.aborted ? abortedCodexRateLimitResult() : withResetCredits
}
if (isCodexAuthError(rpcResult.error)) {
@@ -1169,7 +1210,8 @@ export async function fetchCodexRateLimits(
if (options?.signal?.aborted) {
return abortedCodexRateLimitResult()
}
const withResetCredits = await withBackendRateLimitResetCredits(ptyResult, options)
const withSession = await withBackendSessionWindow(ptyResult, options)
const withResetCredits = await withBackendRateLimitResetCredits(withSession, options)
return options?.signal?.aborted ? abortedCodexRateLimitResult() : withResetCredits
} catch (err) {
if (options?.signal?.aborted) {
@@ -1,10 +1,10 @@
import { describe, expect, it } from 'vitest'
import {
classifyCodexRateLimitWindows,
type CodexRpcRateLimits
type CodexRateLimitWindowsSnapshot
} from './codex-rate-limit-window-classification'
function usedPercentByWindow(result: CodexRpcRateLimits | null): {
function usedPercentByWindow(result: CodexRateLimitWindowsSnapshot | null): {
session: number | null
weekly: number | null
} {
@@ -4,27 +4,27 @@ export const CODEX_WEEKLY_WINDOW_MINUTES = 10080
// Why: tolerate the one-minute drift seen in older Codex bucket lengths without absorbing other durations.
const CODEX_WINDOW_DURATION_TOLERANCE_MINUTES = 1
export type CodexRpcRateWindow = {
export type CodexRateWindowSnapshot = {
usedPercent?: unknown
windowDurationMins?: unknown
resetsAt?: unknown
}
export type CodexRpcRateLimits = {
primary?: CodexRpcRateWindow | null
secondary?: CodexRpcRateWindow | null
export type CodexRateLimitWindowsSnapshot = {
primary?: CodexRateWindowSnapshot | null
secondary?: CodexRateWindowSnapshot | null
}
type MappableCodexRpcRateWindow = CodexRpcRateWindow & { usedPercent: number }
type MappableCodexRateWindowSnapshot = CodexRateWindowSnapshot & { usedPercent: number }
type CodexRateLimitWindowKind = 'session' | 'weekly' | null
function isMappableCodexRpcRateWindow(
raw: CodexRpcRateWindow | null | undefined
): raw is MappableCodexRpcRateWindow {
function isMappableCodexRateWindowSnapshot(
raw: CodexRateWindowSnapshot | null | undefined
): raw is MappableCodexRateWindowSnapshot {
return typeof raw?.usedPercent === 'number' && Number.isFinite(raw.usedPercent)
}
function classifyWindowDuration(raw: MappableCodexRpcRateWindow): CodexRateLimitWindowKind {
function classifyWindowDuration(raw: MappableCodexRateWindowSnapshot): CodexRateLimitWindowKind {
const duration = raw.windowDurationMins
if (typeof duration !== 'number' || !Number.isFinite(duration)) {
return null
@@ -40,14 +40,16 @@ function classifyWindowDuration(raw: MappableCodexRpcRateWindow): CodexRateLimit
return null
}
export function classifyCodexRateLimitWindows(result: CodexRpcRateLimits | null | undefined): {
session: MappableCodexRpcRateWindow | null
weekly: MappableCodexRpcRateWindow | null
export function classifyCodexRateLimitWindows(
result: CodexRateLimitWindowsSnapshot | null | undefined
): {
session: MappableCodexRateWindowSnapshot | null
weekly: MappableCodexRateWindowSnapshot | null
} {
const primary = isMappableCodexRpcRateWindow(result?.primary) ? result.primary : null
const secondary = isMappableCodexRpcRateWindow(result?.secondary) ? result.secondary : null
let session: MappableCodexRpcRateWindow | null = null
let weekly: MappableCodexRpcRateWindow | null = null
const primary = isMappableCodexRateWindowSnapshot(result?.primary) ? result.primary : null
const secondary = isMappableCodexRateWindowSnapshot(result?.secondary) ? result.secondary : null
let session: MappableCodexRateWindowSnapshot | null = null
let weekly: MappableCodexRateWindowSnapshot | null = null
for (const window of [primary, secondary]) {
if (!window) {
@@ -1232,10 +1232,12 @@ function VerboseProviderUsage({
return window !== null
})
const meterWindow = p.session ?? p.weekly
return (
<>
{p.session && !compact ? (
<MiniBar usedPct={clampUsedPercent(p.session.usedPercent)} display={display} />
{meterWindow && !compact ? (
<MiniBar usedPct={clampUsedPercent(meterWindow.usedPercent)} display={display} />
) : null}
{visibleWindows.map((window, index) => (
<React.Fragment key={window.key}>
@@ -129,6 +129,10 @@ describe('UsageRow', () => {
expect(mocks.useResetCountdownClock).toHaveBeenCalledOnce()
expect(mocks.useResetCountdownClock).toHaveBeenCalledWith([sessionReset, weeklyReset])
expect(markup).toContain('Resets in 2m')
expect(markup).toContain('5h')
expect(markup).toContain('25%')
expect(markup).toContain('wk')
expect(markup).toContain('10%')
})
})
@@ -99,6 +99,31 @@ describe('InlineUsageBars', () => {
expect(markup).toContain('32% used now')
})
it('keeps the footer meter for weekly-only Codex usage', async () => {
const { ProviderSegment } = await import('./StatusBar')
const limits: ProviderRateLimits = {
provider: 'codex',
session: null,
weekly: {
usedPercent: 37,
windowMinutes: 10_080,
resetsAt: null,
resetDescription: null
},
updatedAt: Date.now(),
error: null,
status: 'ok'
}
const markup = renderToStaticMarkup(
<ProviderSegment p={limits} compact={false} display="used" />
)
expect(markup).toContain('w-[48px] h-[6px]')
expect(markup).toContain('width:37%')
expect(markup).toContain('37% used wk')
})
it('shows remaining copy and remaining meter fill', async () => {
mocks.usagePercentageDisplay = 'remaining'
const { InlineUsageBars } = await import('./StatusBar')