fix(usage): render codex usage when rate-limit response has null secondary window

Some Codex plans (e.g. Plus) return only a weekly window in `primary` with
`secondary: null`. applyCodexUsageToAccount dereferenced `secondary.usedPercent`
unconditionally, throwing a TypeError that refreshActiveCodexUsage swallowed at
debug level, so usage never applied and the dashboard row stayed blank (-1).

Guard the null secondary, and when a lone window is weekly (windowDurationMins
> 1440) route it into the 7d slot leaving 5h unknown. Accounts that report both
windows are unchanged. Adds a regression test for the secondary:null case.
This commit is contained in:
Codex
2026-07-24 23:15:12 +09:00
parent a9095d95a8
commit 6ab38ca461
2 changed files with 68 additions and 8 deletions

View File

@@ -14,8 +14,8 @@ import { logger } from './logger.js';
export interface CodexRateLimit {
limitId?: string;
limitName: string | null;
primary: { usedPercent: number; resetsAt: string | number };
secondary: { usedPercent: number; resetsAt: string | number };
primary: { usedPercent: number; resetsAt: string | number; windowDurationMins?: number };
secondary: { usedPercent: number; resetsAt: string | number; windowDurationMins?: number } | null;
}
/**
@@ -291,8 +291,8 @@ export function applyCodexUsageToAccount(
account: accountIndex + 1,
buckets: usage.map((l) => ({
id: l.limitId,
h5: l.primary.usedPercent,
d7: l.secondary.usedPercent,
h5: l.primary?.usedPercent ?? null,
d7: l.secondary?.usedPercent ?? null,
})),
},
`Codex account #${accountIndex + 1}: ${usage.length} rate-limit bucket(s)`,
@@ -312,13 +312,31 @@ export function applyCodexUsageToAccount(
return;
}
const pct = Math.round(effective.primary.usedPercent);
const d7Pct = Math.round(effective.secondary.usedPercent);
// Historical shape: primary = 5h window, secondary = 7d window. Some plans
// (e.g. Plus) return only ONE window in `primary` (a weekly/7d window) with
// `secondary: null`. Guard the null so we don't throw, and route a lone
// weekly window into the 7d slot so usage still renders.
// Store raw ISO timestamps (not pre-formatted strings) so the exhaustion
// gate can compute "minutes until reset" later. The dashboard render path
// formats these at display time via `formatResetRemaining`.
const resetIso = toIsoMaybe(effective.primary.resetsAt);
const resetD7Iso = toIsoMaybe(effective.secondary.resetsAt);
let pct = Math.round(effective.primary.usedPercent);
let resetIso = toIsoMaybe(effective.primary.resetsAt);
let d7Pct = effective.secondary
? Math.round(effective.secondary.usedPercent)
: -1;
let resetD7Iso = effective.secondary
? toIsoMaybe(effective.secondary.resetsAt)
: undefined;
if (
!effective.secondary &&
(effective.primary.windowDurationMins ?? 0) > 1440
) {
// Lone window is weekly (> 1 day) — show it as 7d, leave 5h unknown.
d7Pct = pct;
resetD7Iso = resetIso;
pct = -1;
resetIso = undefined;
}
updateCodexAccountUsage(pct, resetIso, accountIndex, d7Pct, resetD7Iso);
logger.info(
{