fix(primer): drop reset-alignment retry; keep Claude binary fix only
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 <noreply@anthropic.com>
This commit is contained in:
@@ -12,13 +12,8 @@ const refreshActiveCodexUsage = vi.fn(async () => {
|
|||||||
});
|
});
|
||||||
const runCodexWarmupCycle = vi.fn(async () => {
|
const runCodexWarmupCycle = vi.fn(async () => {
|
||||||
callOrder.push('warmup');
|
callOrder.push('warmup');
|
||||||
return { status: 'warmed', accountIndex: 0 } as {
|
return { status: 'warmed', accountIndex: 0 };
|
||||||
status: string;
|
|
||||||
accountIndex?: number;
|
|
||||||
reason?: string;
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
const getAllCodexAccounts = vi.fn<() => Array<{ resetAt?: string }>>(() => []);
|
|
||||||
|
|
||||||
vi.mock('./config.js', () => ({
|
vi.mock('./config.js', () => ({
|
||||||
CODEX_WARMUP_CONFIG: {
|
CODEX_WARMUP_CONFIG: {
|
||||||
@@ -58,10 +53,6 @@ vi.mock('./codex-warmup.js', () => ({
|
|||||||
runCodexWarmupCycle,
|
runCodexWarmupCycle,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./codex-token-rotation.js', () => ({
|
|
||||||
getAllCodexAccounts,
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('usage-primer', () => {
|
describe('usage-primer', () => {
|
||||||
it('refreshes Codex usage before primer selection and fires regardless of usage level', async () => {
|
it('refreshes Codex usage before primer selection and fires regardless of usage level', async () => {
|
||||||
callOrder.length = 0;
|
callOrder.length = 0;
|
||||||
@@ -83,31 +74,4 @@ describe('usage-primer', () => {
|
|||||||
{ ignoreZeroUsageWindow: true },
|
{ 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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,24 +8,23 @@ import {
|
|||||||
refreshAllCodexAccountUsage,
|
refreshAllCodexAccountUsage,
|
||||||
} from './codex-usage-collector.js';
|
} from './codex-usage-collector.js';
|
||||||
import { runCodexWarmupCycle } from './codex-warmup.js';
|
import { runCodexWarmupCycle } from './codex-warmup.js';
|
||||||
import { getAllCodexAccounts } from './codex-token-rotation.js';
|
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { getAllTokens } from './token-rotation.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
|
* Fires a tiny inference call at fixed KST slots (08, 13, 18, 23) so each
|
||||||
* inference call after the previous window expires and reset ~5h later. We
|
* provider account is kept warm and any usage-window issues surface in the
|
||||||
* anchor windows to fixed KST times by firing a tiny primer call at 08, 13,
|
* logs at predictable times.
|
||||||
* 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).
|
|
||||||
*
|
*
|
||||||
* Daily reset point ends up being 08:00 KST (start of first slot after gap),
|
* IMPORTANT — what this does NOT do: it does not pin the reset to a fixed
|
||||||
* provided no off-slot Codex usage (e.g. a paired-room reviewer) re-anchors
|
* clock time. Empirically the Codex 5h limit is a *trailing rolling* window:
|
||||||
* the window inside the gap. When a slot lands while the window is still
|
* the reported reset slides forward with continued usage (observed reset
|
||||||
* active/exhausted, runCodexPrimerCycle schedules a one-shot retry right after
|
* moving 18:00 → 18:33 over ~33 min of activity). A single timed message can
|
||||||
* the reported reset so the next window still anchors close to the slot.
|
* 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];
|
const PRIMER_HOURS_KST = [8, 13, 18, 23];
|
||||||
@@ -152,53 +151,7 @@ async function runClaudePrimerCycle(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a primer slot lands while the 5h window is still active (exhausted /
|
export async function runCodexPrimerCycle(): Promise<void> {
|
||||||
// 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<typeof setTimeout> | 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<void> {
|
|
||||||
const { allowRetry = true } = options;
|
|
||||||
try {
|
try {
|
||||||
await refreshAllCodexAccountUsage();
|
await refreshAllCodexAccountUsage();
|
||||||
await refreshActiveCodexUsage();
|
await refreshActiveCodexUsage();
|
||||||
@@ -212,18 +165,12 @@ export async function runCodexPrimerCycle(
|
|||||||
},
|
},
|
||||||
{ ignoreZeroUsageWindow: true },
|
{ 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');
|
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) {
|
} catch (err) {
|
||||||
logger.warn({ err }, 'Codex primer cycle threw');
|
logger.warn({ err }, 'Codex primer cycle threw');
|
||||||
}
|
}
|
||||||
@@ -277,8 +224,4 @@ export function stopUsagePrimer(): void {
|
|||||||
clearTimeout(primerTimer);
|
clearTimeout(primerTimer);
|
||||||
primerTimer = null;
|
primerTimer = null;
|
||||||
}
|
}
|
||||||
if (codexResetRetryTimer) {
|
|
||||||
clearTimeout(codexResetRetryTimer);
|
|
||||||
codexResetRetryTimer = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user