From efc8c00380b7a4cebb573040974f9e82ec964636 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 13:40:35 +0900 Subject: [PATCH] fix(primer): drop reset-alignment retry; keep Claude binary fix only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live evidence shows the Codex 5h limit is a trailing rolling window whose reported reset slides forward with continued usage (observed 18:00 → 18:33 over ~33 min). A timed primer therefore cannot pin the reset to a fixed clock time, and the previous "retry at reported reset" logic was both based on a wrong fixed-window model and buggy under sliding resets (stale reset target, single-shot). Revert it. Keep the verified Claude primer binary-path fix (resolve repo-root node_modules) and document the real rolling-window behavior so the primer is correctly framed as a best-effort warm-up + success/failure record. Co-Authored-By: Claude Opus 4.7 --- src/usage-primer.test.ts | 38 +---------------- src/usage-primer.ts | 91 ++++++++-------------------------------- 2 files changed, 18 insertions(+), 111 deletions(-) diff --git a/src/usage-primer.test.ts b/src/usage-primer.test.ts index effb9b7..18e649b 100644 --- a/src/usage-primer.test.ts +++ b/src/usage-primer.test.ts @@ -12,13 +12,8 @@ const refreshActiveCodexUsage = vi.fn(async () => { }); const runCodexWarmupCycle = vi.fn(async () => { callOrder.push('warmup'); - return { status: 'warmed', accountIndex: 0 } as { - status: string; - accountIndex?: number; - reason?: string; - }; + return { status: 'warmed', accountIndex: 0 }; }); -const getAllCodexAccounts = vi.fn<() => Array<{ resetAt?: string }>>(() => []); vi.mock('./config.js', () => ({ CODEX_WARMUP_CONFIG: { @@ -58,10 +53,6 @@ vi.mock('./codex-warmup.js', () => ({ runCodexWarmupCycle, })); -vi.mock('./codex-token-rotation.js', () => ({ - getAllCodexAccounts, -})); - describe('usage-primer', () => { it('refreshes Codex usage before primer selection and fires regardless of usage level', async () => { callOrder.length = 0; @@ -83,31 +74,4 @@ describe('usage-primer', () => { { ignoreZeroUsageWindow: true }, ); }); - - it('reschedules a retry at the window reset when the slot is rate-limited', async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-06-09T00:00:00.000Z')); - runCodexWarmupCycle.mockClear(); - // First slot: the 5h window is still active/exhausted → command fails. - runCodexWarmupCycle.mockResolvedValueOnce({ - status: 'failed', - accountIndex: 0, - reason: 'exit_1', - }); - // The account's window resets 60s from now. - getAllCodexAccounts.mockReturnValue([ - { resetAt: '2026-06-09T00:01:00.000Z' }, - ]); - - const { runCodexPrimerCycle } = await import('./usage-primer.js'); - await runCodexPrimerCycle(); - expect(runCodexWarmupCycle).toHaveBeenCalledTimes(1); - - // Retry is scheduled at reset (+60s) plus a 30s buffer = +90s. - await vi.advanceTimersByTimeAsync(91_000); - expect(runCodexWarmupCycle).toHaveBeenCalledTimes(2); - - getAllCodexAccounts.mockReturnValue([]); - vi.useRealTimers(); - }); }); diff --git a/src/usage-primer.ts b/src/usage-primer.ts index 4a3aeed..b2e80c4 100644 --- a/src/usage-primer.ts +++ b/src/usage-primer.ts @@ -8,24 +8,23 @@ import { refreshAllCodexAccountUsage, } from './codex-usage-collector.js'; import { runCodexWarmupCycle } from './codex-warmup.js'; -import { getAllCodexAccounts } from './codex-token-rotation.js'; import { logger } from './logger.js'; import { getAllTokens } from './token-rotation.js'; /** - * Usage-window alignment primer. + * Usage-window warm-up primer. * - * Anthropic/OpenAI enforce 5h rolling usage windows that start on the first - * inference call after the previous window expires and reset ~5h later. We - * anchor windows to fixed KST times by firing a tiny primer call at 08, 13, - * 18, 23 KST (4 windows × 5h = 20h, leaving a 04:00–08:00 quiet gap with no - * scheduled primer so 08:00 can re-anchor the day). + * Fires a tiny inference call at fixed KST slots (08, 13, 18, 23) so each + * provider account is kept warm and any usage-window issues surface in the + * logs at predictable times. * - * Daily reset point ends up being 08:00 KST (start of first slot after gap), - * provided no off-slot Codex usage (e.g. a paired-room reviewer) re-anchors - * the window inside the gap. When a slot lands while the window is still - * active/exhausted, runCodexPrimerCycle schedules a one-shot retry right after - * the reported reset so the next window still anchors close to the slot. + * IMPORTANT — what this does NOT do: it does not pin the reset to a fixed + * clock time. Empirically the Codex 5h limit is a *trailing rolling* window: + * the reported reset slides forward with continued usage (observed reset + * moving 18:00 → 18:33 over ~33 min of activity). A single timed message can + * therefore not anchor the reset to a fixed time while Codex keeps being used + * (e.g. by a paired-room reviewer on the same account). The slots are a + * best-effort warm-up + a success/failure record, not a reset-alignment lever. */ const PRIMER_HOURS_KST = [8, 13, 18, 23]; @@ -152,53 +151,7 @@ async function runClaudePrimerCycle(): Promise { } } -// When a primer slot lands while the 5h window is still active (exhausted / -// rate-limited), the warm-up call fails and the window only re-anchors at its -// natural reset. Rather than leaving Codex un-primed until the next 5h slot, -// we schedule a single retry right after the reported window reset so the next -// window is anchored as close to the reset boundary as possible. -const CODEX_RETRY_BUFFER_MS = 30_000; -const CODEX_RETRY_MAX_DELAY_MS = 5.5 * 60 * 60 * 1000; -let codexResetRetryTimer: ReturnType | null = null; - -function soonestCodexResetMs(nowMs: number): number | null { - let soonest: number | null = null; - for (const account of getAllCodexAccounts()) { - const resetMs = account.resetAt ? Date.parse(account.resetAt) : NaN; - if (Number.isFinite(resetMs) && resetMs > nowMs) { - if (soonest === null || resetMs < soonest) soonest = resetMs; - } - } - return soonest; -} - -function scheduleCodexResetRetry(): void { - if (codexResetRetryTimer) return; // at most one outstanding retry - const nowMs = Date.now(); - const resetMs = soonestCodexResetMs(nowMs); - if (resetMs === null) { - logger.info('Codex primer retry not scheduled: no known window reset time'); - return; - } - const delay = resetMs - nowMs + CODEX_RETRY_BUFFER_MS; - if (delay <= 0 || delay > CODEX_RETRY_MAX_DELAY_MS) { - return; - } - logger.info( - { fireAt: new Date(nowMs + delay).toISOString() }, - 'Scheduling Codex primer retry at window reset', - ); - codexResetRetryTimer = setTimeout(() => { - codexResetRetryTimer = null; - void runCodexPrimerCycle({ allowRetry: false }); - }, delay); - codexResetRetryTimer.unref?.(); -} - -export async function runCodexPrimerCycle( - options: { allowRetry?: boolean } = {}, -): Promise { - const { allowRetry = true } = options; +export async function runCodexPrimerCycle(): Promise { try { await refreshAllCodexAccountUsage(); await refreshActiveCodexUsage(); @@ -212,18 +165,12 @@ export async function runCodexPrimerCycle( }, { ignoreZeroUsageWindow: true }, ); + // Best-effort: a slot may land while the trailing 5h window is exhausted, + // in which case this records `failed`. We deliberately do NOT try to + // re-anchor to the reported reset time: the 5h limit is a trailing rolling + // window whose reset slides forward with continued Codex usage, so no + // single timed call can pin the reset to a fixed clock time. logger.info({ result }, 'Codex primer cycle finished'); - // The window was still active at this slot (exhausted or all accounts - // rate-limited). Re-anchor right after the window resets instead of - // waiting for the next 5h slot. - const windowBlocked = - result.status === 'failed' || - (result.status === 'skipped' && - (result.reason === 'no_eligible_accounts' || - result.reason === 'disabled_cooldown')); - if (allowRetry && windowBlocked) { - scheduleCodexResetRetry(); - } } catch (err) { logger.warn({ err }, 'Codex primer cycle threw'); } @@ -277,8 +224,4 @@ export function stopUsagePrimer(): void { clearTimeout(primerTimer); primerTimer = null; } - if (codexResetRetryTimer) { - clearTimeout(codexResetRetryTimer); - codexResetRetryTimer = null; - } }