diff --git a/src/codex/quota-auto-refresh.ts b/src/codex/quota-auto-refresh.ts index ae8470a877..f4bebebdad 100644 --- a/src/codex/quota-auto-refresh.ts +++ b/src/codex/quota-auto-refresh.ts @@ -1,6 +1,7 @@ import { mutatePersistedConfig } from "../config"; import { registerStateSweepAfterTick } from "../lib/state-store-sweeper"; import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers"; +import { normalizeResetAt } from "../providers/quota-wire"; import { providerCodexAccountMode } from "../providers/registry"; import type { OcxConfig } from "../types"; import { isSelectableCodexPoolAccount } from "./account-id"; @@ -18,6 +19,7 @@ export const FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60; const RETRY_MS = 5 * 60_000; const CONCURRENCY = 4; +/** Completed/due markers use epoch milliseconds; persisted legacy markers may use seconds. */ export type CodexQuotaAutoRefreshWindows = { fiveHour?: number; weekly?: number }; export interface CodexQuotaAutoRefreshStatus { @@ -41,11 +43,6 @@ let inFlight: Promise | null = null; const completedByAccount = new Map(); const retryAfterByAccount = new Map(); -function resetAtMs(resetAt: number): number { - // WHAM reports seconds; quota parsed from response headers may already be milliseconds. - return resetAt < 100_000_000_000 ? resetAt * 1000 : resetAt; -} - export function codexQuotaAutoRefreshStatus( config: OcxConfig, accountId: string, @@ -71,20 +68,22 @@ export function dueCodexQuotaAutoRefreshWindows( if (!quota) return null; const saved = config.codexQuotaAutoRefresh?.[accountId]; const due: CodexQuotaAutoRefreshWindows = {}; + const shortResetAt = normalizeResetAt(quota.shortResetAt); + const weeklyResetAt = normalizeResetAt(quota.weeklyResetAt); if (saved?.fiveHour === true && quota.shortWindowSeconds === FIVE_HOUR_WINDOW_SECONDS - && typeof quota.shortResetAt === "number" - && resetAtMs(quota.shortResetAt) <= now - && saved.lastFiveHourResetAt !== quota.shortResetAt - && completed?.fiveHour !== quota.shortResetAt) { - due.fiveHour = quota.shortResetAt; + && shortResetAt !== undefined + && shortResetAt <= now + && normalizeResetAt(saved.lastFiveHourResetAt) !== shortResetAt + && normalizeResetAt(completed?.fiveHour) !== shortResetAt) { + due.fiveHour = shortResetAt; } if (saved?.weekly === true - && typeof quota.weeklyResetAt === "number" - && resetAtMs(quota.weeklyResetAt) <= now - && saved.lastWeeklyResetAt !== quota.weeklyResetAt - && completed?.weekly !== quota.weeklyResetAt) { - due.weekly = quota.weeklyResetAt; + && weeklyResetAt !== undefined + && weeklyResetAt <= now + && normalizeResetAt(saved.lastWeeklyResetAt) !== weeklyResetAt + && normalizeResetAt(completed?.weekly) !== weeklyResetAt) { + due.weekly = weeklyResetAt; } return due.fiveHour === undefined && due.weekly === undefined ? null : due; } @@ -139,8 +138,10 @@ function retryPendingMarkers( for (const [accountId, completed] of completedByAccount) { const saved = config.codexQuotaAutoRefresh?.[accountId]; if (!saved) continue; - if ((completed.fiveHour === undefined || saved.lastFiveHourResetAt === completed.fiveHour) - && (completed.weekly === undefined || saved.lastWeeklyResetAt === completed.weekly)) continue; + if ((completed.fiveHour === undefined + || normalizeResetAt(saved.lastFiveHourResetAt) === normalizeResetAt(completed.fiveHour)) + && (completed.weekly === undefined + || normalizeResetAt(saved.lastWeeklyResetAt) === normalizeResetAt(completed.weekly))) continue; persist(config, accountId, completed); } } diff --git a/tests/codex-integration/codex-quota-auto-refresh.test.ts b/tests/codex-integration/codex-quota-auto-refresh.test.ts index aa453ae700..4c46a0080b 100644 --- a/tests/codex-integration/codex-quota-auto-refresh.test.ts +++ b/tests/codex-integration/codex-quota-auto-refresh.test.ts @@ -116,8 +116,8 @@ describe("Codex quota window auto refresh", () => { test("accepts reset timestamps in seconds or milliseconds and ignores completed windows", () => { const cfg = config(); expect(dueCodexQuotaAutoRefreshWindows(cfg, "pool-a", quota(), NOW)).toEqual({ - fiveHour: RESET_SECONDS, - weekly: RESET_SECONDS, + fiveHour: NOW, + weekly: NOW, }); expect(dueCodexQuotaAutoRefreshWindows( cfg, @@ -134,6 +134,62 @@ describe("Codex quota window auto refresh", () => { )).toBeNull(); }); + test.each([ + [RESET_SECONDS, NOW], + [NOW, RESET_SECONDS], + ])("deduplicates saved and completed markers across units (%i -> %i)", (marker, observed) => { + const cfg = config(); + const snapshot = quota({ shortResetAt: observed, weeklyResetAt: observed }); + expect(dueCodexQuotaAutoRefreshWindows(cfg, "pool-a", snapshot, NOW, { + fiveHour: marker, + weekly: marker, + })).toBeNull(); + cfg.codexQuotaAutoRefresh = { + "pool-a": { fiveHour: true, weekly: true, lastFiveHourResetAt: marker, lastWeeklyResetAt: marker }, + }; + expect(dueCodexQuotaAutoRefreshWindows(cfg, "pool-a", snapshot, NOW)).toBeNull(); + expect(dueCodexQuotaAutoRefreshWindows(cfg, "pool-a", quota({ + shortResetAt: RESET_SECONDS + 1, + weeklyResetAt: NOW + 1000, + }), NOW)).toBeNull(); + expect(dueCodexQuotaAutoRefreshWindows(cfg, "pool-a", quota({ + shortResetAt: RESET_SECONDS + 1, + weeklyResetAt: NOW + 1000, + }), NOW + 1000)).toEqual({ fiveHour: NOW + 1000, weekly: NOW + 1000 }); + }); + + test.each([ + [RESET_SECONDS, NOW], + [NOW, RESET_SECONDS], + ])("persists canonical markers and avoids warmup after a unit change and restart (%i -> %i)", async (first, second) => { + const cfg = config(); + writeFileSync(join(testHome, "config.json"), JSON.stringify(cfg)); + let observed = first; + let warmups = 0; + const deps = { + getQuota: (id: string) => id === "pool-a" + ? quota({ shortResetAt: observed, weeklyResetAt: observed }) : null, + warmAccount: async () => { warmups += 1; }, + }; + await runCodexQuotaAutoRefresh(cfg, NOW, deps); + expect(readConfigDiagnostics().config.codexQuotaAutoRefresh?.["pool-a"]).toMatchObject({ + lastFiveHourResetAt: NOW, + lastWeeklyResetAt: NOW, + }); + observed = second; + await runCodexQuotaAutoRefresh(cfg, NOW + 1, deps); + resetCodexQuotaAutoRefreshForTests(); + await runCodexQuotaAutoRefresh(loadConfig(), NOW + 2, deps); + expect(warmups).toBe(1); + observed = RESET_SECONDS + 1; + await runCodexQuotaAutoRefresh(loadConfig(), NOW + 1000, deps); + expect(warmups).toBe(2); + expect(readConfigDiagnostics().config.codexQuotaAutoRefresh?.["pool-a"]).toMatchObject({ + lastFiveHourResetAt: NOW + 1000, + lastWeeklyResetAt: NOW + 1000, + }); + }); + test("coalesces simultaneous windows into one warmup and persists both markers", async () => { const cfg = config(); const warmed: string[] = []; @@ -149,8 +205,8 @@ describe("Codex quota window auto refresh", () => { }); expect(warmed).toEqual(["pool-a"]); expect(cfg.codexQuotaAutoRefresh?.["pool-a"]).toMatchObject({ - lastFiveHourResetAt: RESET_SECONDS, - lastWeeklyResetAt: RESET_SECONDS, + lastFiveHourResetAt: NOW, + lastWeeklyResetAt: NOW, }); }); @@ -170,20 +226,36 @@ describe("Codex quota window auto refresh", () => { const cfg = config(); let warmups = 0; let writes = 0; + let observed = RESET_SECONDS; const persist = (target: OcxConfig, id: string, completed: CodexQuotaAutoRefreshWindows) => { writes += 1; - return writes > 1 ? recordMarkers(target, id, completed) : false; + return writes > 2 ? recordMarkers(target, id, completed) : false; }; const deps = { - getQuota: (id: string) => id === "pool-a" ? quota() : null, + getQuota: (id: string) => id === "pool-a" + ? quota({ shortResetAt: observed, weeklyResetAt: observed }) : null, warmAccount: async () => { warmups += 1; }, persistCompleted: persist, }; await runCodexQuotaAutoRefresh(cfg, NOW, deps); + observed = NOW; await runCodexQuotaAutoRefresh(cfg, NOW + 1, deps); expect(warmups).toBe(1); expect(writes).toBe(2); - expect(cfg.codexQuotaAutoRefresh?.["pool-a"]?.lastWeeklyResetAt).toBe(RESET_SECONDS); + await runCodexQuotaAutoRefresh(cfg, NOW + 2, deps); + expect(warmups).toBe(1); + expect(writes).toBe(3); + expect(cfg.codexQuotaAutoRefresh?.["pool-a"]).toMatchObject({ + lastFiveHourResetAt: NOW, + lastWeeklyResetAt: NOW, + }); + // Equivalent legacy markers must not cause another persistence attempt either. + cfg.codexQuotaAutoRefresh = { + "pool-a": { fiveHour: true, weekly: true, lastFiveHourResetAt: RESET_SECONDS, lastWeeklyResetAt: RESET_SECONDS }, + }; + await runCodexQuotaAutoRefresh(cfg, NOW + 3, deps); + expect(warmups).toBe(1); + expect(writes).toBe(3); }); test("backs a failed main-account claim or warmup off for five minutes", async () => { @@ -202,7 +274,7 @@ describe("Codex quota window auto refresh", () => { await runCodexQuotaAutoRefresh(cfg, NOW + 5 * 60_000 - 1, deps); await runCodexQuotaAutoRefresh(cfg, NOW + 5 * 60_000, deps); expect(attempts).toBe(2); - expect(cfg.codexQuotaAutoRefresh?.__main__?.lastFiveHourResetAt).toBe(RESET_SECONDS); + expect(cfg.codexQuotaAutoRefresh?.__main__?.lastFiveHourResetAt).toBe(NOW); }); test("settings route persists supported toggles and rejects unavailable windows", async () => {