Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/main/rate-limits/codex-fetcher-backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,47 @@ 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
}
},
rate_limit_reset_credits: { available_count: 0 }
})
} as Response)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ available_count: 0, credits: [] })
} as Response)

await expect(
fetchCodexRateLimits({
codexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex'
})
).resolves.toMatchObject({
session: null,
weekly: {
usedPercent: 37,
windowMinutes: 10_080,
resetsAt: 1_800_000_000_000
},
status: 'ok'
})
})

it('aborts callers while sharing one stalled backend auth read', async () => {
let resolveRead!: (content: string) => void
readFileMock.mockImplementation(
Expand Down
46 changes: 45 additions & 1 deletion src/main/rate-limits/codex-fetcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ describe('fetchCodexRateLimits', () => {
expect(ptySpawnMock).not.toHaveBeenCalled()
})

it('normalizes Codex RPC remaining-minute windows to fixed display durations', async () => {
it('normalizes floor-rounded Codex RPC window durations to canonical display durations', async () => {
const rpcChild = makeRpcChild()
childSpawnMock.mockReturnValue(rpcChild)
rpcChild.stdin.write.mockImplementation((line: string) => {
Expand Down Expand Up @@ -368,6 +368,50 @@ describe('fetchCodexRateLimits', () => {
expect(result.weekly?.windowMinutes).toBe(10080)
})

it('classifies a sole RPC weekly primary window as weekly', async () => {
const rpcChild = makeRpcChild()
childSpawnMock.mockReturnValue(rpcChild)
rpcChild.stdin.write.mockImplementation((line: string) => {
const msg = JSON.parse(line) as { id?: number; method?: string }
if (msg.method === 'initialize') {
setTimeout(() => {
rpcChild.stdout.emit(
'data',
Buffer.from(`${JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: {} })}\n`)
)
}, 0)
}
if (msg.method === 'account/rateLimits/read') {
setTimeout(() => {
rpcChild.stdout.emit(
'data',
Buffer.from(
`${JSON.stringify({
jsonrpc: '2.0',
id: msg.id,
result: {
rateLimits: {
primary: { usedPercent: 37, windowDurationMins: 10_079 }
}
}
})}\n`
)
)
}, 0)
}
})

const resultPromise = fetchCodexRateLimits()
await vi.advanceTimersByTimeAsync(1)
await vi.advanceTimersByTimeAsync(1)

await expect(resultPromise).resolves.toMatchObject({
session: null,
weekly: { usedPercent: 37, windowMinutes: 10_080 },
status: 'ok'
})
})

it('fills reset-credit count from the backend when the installed app-server omits it', async () => {
const rpcChild = makeRpcChild()
childSpawnMock.mockReturnValue(rpcChild)
Expand Down
27 changes: 22 additions & 5 deletions src/main/rate-limits/codex-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ import {
createAuthFilesystemOperation,
type SharedAuthFilesystemOperation
} from './auth-filesystem-operation'
import {
classifyCodexRateLimitWindows,
CODEX_SESSION_WINDOW_MINUTES,
CODEX_WEEKLY_WINDOW_MINUTES,
isCodexWeeklyWindowDuration
} from './codex-rate-limit-window-classification'

const RPC_TIMEOUT_MS = 10_000
const WSL_RPC_TIMEOUT_MS = 25_000
Expand Down Expand Up @@ -476,7 +482,7 @@ function mapRpcWindow(

return {
usedPercent: Math.min(100, Math.max(0, raw.usedPercent)),
// Why: windowDurationMins reports remaining minutes, but the UI needs the fixed bucket duration for "5h"/"wk" labels.
// Why: windowDurationMins is a fixed per-window bucket size that legacy Codex paths floor-round (e.g. 299, 10079); normalize to the canonical duration for stable "5h"/"wk" labels.
windowMinutes: expectedWindowMinutes,
resetsAt,
resetDescription
Expand Down Expand Up @@ -528,10 +534,14 @@ async function fetchViaBackend(
if (typeof payload.plan_type !== 'string') {
return null
}
const { session, weekly } = classifyCodexRateLimitWindows(
mapBackendUsageWindow(payload.rate_limit?.primary_window, CODEX_SESSION_WINDOW_MINUTES),
mapBackendUsageWindow(payload.rate_limit?.secondary_window, CODEX_WEEKLY_WINDOW_MINUTES)
)
return {
provider: 'codex',
session: mapBackendUsageWindow(payload.rate_limit?.primary_window, 300),
weekly: mapBackendUsageWindow(payload.rate_limit?.secondary_window, 10080),
session,
weekly,
// Surfaced for the status-bar Usage row (e.g. "Codex · Plus").
planType: payload.plan_type,
...(payload.rate_limit_reset_credits !== undefined
Expand Down Expand Up @@ -701,8 +711,15 @@ async function fetchViaRpc(options?: FetchCodexRateLimitsOptions): Promise<Provi

const wrapper = msg.result as RpcRateLimitsResponse | undefined
const result = wrapper?.rateLimits
const session = mapRpcWindow(result?.primary, 300)
const weekly = mapRpcWindow(result?.secondary, 10080)
const primaryWindowMinutes = isCodexWeeklyWindowDuration(
result?.primary?.windowDurationMins
)
? CODEX_WEEKLY_WINDOW_MINUTES
: CODEX_SESSION_WINDOW_MINUTES
const { session, weekly } = classifyCodexRateLimitWindows(
mapRpcWindow(result?.primary, primaryWindowMinutes),
mapRpcWindow(result?.secondary, CODEX_WEEKLY_WINDOW_MINUTES)
)
const rateLimitResetCredits = mapRpcRateLimitResetCredits(
wrapper?.rateLimitResetCredits
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import type { RateLimitWindow } from '../../shared/rate-limit-types'
import {
classifyCodexRateLimitWindows,
isCodexWeeklyWindowDuration
} from './codex-rate-limit-window-classification'

function window(windowMinutes: number): RateLimitWindow {
return {
usedPercent: 25,
windowMinutes,
resetsAt: null,
resetDescription: null
}
}

describe('Codex rate-limit window classification', () => {
it('recognizes exact and RPC-rounded weekly durations', () => {
expect(isCodexWeeklyWindowDuration(10_080)).toBe(true)
expect(isCodexWeeklyWindowDuration(10_079)).toBe(true)
expect(isCodexWeeklyWindowDuration(300)).toBe(false)
})

it('classifies a sole weekly primary window as weekly', () => {
const weekly = window(10_080)

expect(classifyCodexRateLimitWindows(weekly, null)).toEqual({
session: null,
weekly
})
})

it('keeps a sole five-hour primary window as the session limit', () => {
const session = window(300)

expect(classifyCodexRateLimitWindows(session, null)).toEqual({
session,
weekly: null
})
})

it('keeps the traditional primary and secondary positions when both exist', () => {
const session = window(300)
const weekly = window(10_080)

expect(classifyCodexRateLimitWindows(session, weekly)).toEqual({ session, weekly })
})
})
28 changes: 28 additions & 0 deletions src/main/rate-limits/codex-rate-limit-window-classification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { RateLimitWindow } from '../../shared/rate-limit-types'

export const CODEX_SESSION_WINDOW_MINUTES = 300
export const CODEX_WEEKLY_WINDOW_MINUTES = 10_080

const CODEX_RPC_WINDOW_ROUNDING_TOLERANCE_MINUTES = 1

export function isCodexWeeklyWindowDuration(windowMinutes: number | undefined): boolean {
return (
typeof windowMinutes === 'number' &&
Number.isFinite(windowMinutes) &&
Math.abs(windowMinutes - CODEX_WEEKLY_WINDOW_MINUTES) <=
CODEX_RPC_WINDOW_ROUNDING_TOLERANCE_MINUTES
)
}

export function classifyCodexRateLimitWindows(
primary: RateLimitWindow | null,
secondary: RateLimitWindow | null
): { session: RateLimitWindow | null; weekly: RateLimitWindow | null } {
// Why: newer Codex plans can expose their sole weekly quota as the primary
// window, so field position alone no longer identifies a five-hour session.
if (primary && !secondary && isCodexWeeklyWindowDuration(primary.windowMinutes)) {
return { session: null, weekly: primary }
}

return { session: primary, weekly: secondary }
}
7 changes: 5 additions & 2 deletions src/renderer/src/components/status-bar/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1238,10 +1238,13 @@ function VerboseProviderUsage({
return window !== null
})

// Why: weekly-only plans have no session window; the mini meter follows the weekly quota instead of vanishing.
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}>
Expand Down
53 changes: 53 additions & 0 deletions src/renderer/src/components/status-bar/inline-usage-bars.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,59 @@ describe('InlineUsageBars', () => {
expect(markup).toContain('32% used now')
})

it('labels a sole Codex weekly window as weekly, not a 5h session', async () => {
const { InlineUsageBars } = await import('./StatusBar')
// Shape the classifier emits for a weekly-only plan: session cleared, weekly set.
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(<InlineUsageBars limits={limits} isFetching={false} />)

expect(markup).toContain('37% used wk')
expect(markup).not.toContain('37% used 5h')
})

it('keeps the mini meter when Codex exposes only a weekly window', async () => {
const { ProviderDetailsMenu } = 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(
<ProviderDetailsMenu
provider={limits}
compact={false}
iconOnly={false}
ariaLabel="Open Codex usage details"
/>
)

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')
Expand Down