fix(codex): align primer warmup with fixed slots
This commit is contained in:
@@ -225,6 +225,71 @@ describe('Codex warm-up scheduler', () => {
|
||||
expect(childProcess.spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('lets the fixed-slot primer warm an account again when 5h usage is fresh but weekly usage is nonzero', async () => {
|
||||
const childProcess = await import('child_process');
|
||||
const rotation = await import('./codex-token-rotation.js');
|
||||
const { runCodexWarmupCycle } = await import('./codex-warmup.js');
|
||||
const now = new Date('2026-04-24T14:00:00Z').getTime();
|
||||
|
||||
fs.writeFileSync(
|
||||
statePath,
|
||||
JSON.stringify({
|
||||
lastWarmupAt: '2026-04-24T09:00:00.000Z',
|
||||
consecutiveFailures: 0,
|
||||
accounts: {
|
||||
'0': {
|
||||
lastWarmupAt: '2026-04-24T09:00:00.000Z',
|
||||
zeroUsageWarmupUntil: '2026-05-01T09:00:00.000Z',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mocked(rotation.getAllCodexAccounts).mockReturnValue([
|
||||
{
|
||||
index: 0,
|
||||
accountId: 'same-account-next-slot',
|
||||
planType: 'pro',
|
||||
isActive: true,
|
||||
isRateLimited: false,
|
||||
cachedUsagePct: 0,
|
||||
cachedUsageD7Pct: 37,
|
||||
resetAt: '2026-04-24T19:00:00.000Z',
|
||||
resetD7At: '2026-05-01T09:00:00.000Z',
|
||||
},
|
||||
]);
|
||||
vi.mocked(rotation.getCodexAuthPath).mockImplementation(
|
||||
(accountIndex = 0) => authPathFor(tempHome, accountIndex),
|
||||
);
|
||||
vi.mocked(childProcess.spawn).mockImplementation(
|
||||
() => createFakeCodexProcess(0) as never,
|
||||
);
|
||||
|
||||
const result = await runCodexWarmupCycle(
|
||||
{
|
||||
enabled: true,
|
||||
prompt: 'Reply exactly OK. Do not run tools.',
|
||||
model: 'gpt-5.5',
|
||||
intervalMs: 300_000,
|
||||
minIntervalMs: 18_000_000,
|
||||
staggerMs: 0,
|
||||
maxUsagePct: 0,
|
||||
maxD7UsagePct: 100,
|
||||
commandTimeoutMs: 120_000,
|
||||
failureCooldownMs: 21_600_000,
|
||||
maxConsecutiveFailures: 2,
|
||||
},
|
||||
{ nowMs: now, statePath, ignoreZeroUsageWindow: true },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ status: 'warmed', accountIndex: 0 });
|
||||
expect(childProcess.spawn).toHaveBeenCalledTimes(1);
|
||||
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
expect(state.accounts['0'].lastWarmupAt).toBe('2026-04-24T14:00:00.000Z');
|
||||
expect(state.accounts['0'].zeroUsageWarmupUntil).toBe(
|
||||
'2026-04-24T14:00:00.000Z',
|
||||
);
|
||||
});
|
||||
|
||||
it('auto-backs off after repeated codex exec failures so OpenAI-side blocking does not hammer accounts', async () => {
|
||||
const childProcess = await import('child_process');
|
||||
const rotation = await import('./codex-token-rotation.js');
|
||||
|
||||
@@ -33,6 +33,7 @@ interface CodexWarmupRuntimeOptions {
|
||||
nowMs?: number;
|
||||
statePath?: string;
|
||||
shouldSkip?: () => boolean;
|
||||
ignoreZeroUsageWindow?: boolean;
|
||||
}
|
||||
|
||||
export type CodexWarmupCycleResult =
|
||||
@@ -105,6 +106,7 @@ function selectWarmupCandidate(
|
||||
config: CodexWarmupConfig,
|
||||
state: CodexWarmupState,
|
||||
nowMs: number,
|
||||
options: { ignoreZeroUsageWindow?: boolean } = {},
|
||||
): { accountIndex: number; zeroUsageWarmupUntil: string } | { reason: string } {
|
||||
const disabledUntilMs = parseTimestamp(state.disabledUntil);
|
||||
if (disabledUntilMs != null && disabledUntilMs > nowMs) {
|
||||
@@ -134,7 +136,11 @@ function selectWarmupCandidate(
|
||||
const zeroUsageWarmupUntilMs = parseTimestamp(
|
||||
accountState?.zeroUsageWarmupUntil,
|
||||
);
|
||||
if (zeroUsageWarmupUntilMs != null && zeroUsageWarmupUntilMs > nowMs) {
|
||||
if (
|
||||
!options.ignoreZeroUsageWindow &&
|
||||
zeroUsageWarmupUntilMs != null &&
|
||||
zeroUsageWarmupUntilMs > nowMs
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -144,10 +150,13 @@ function selectWarmupCandidate(
|
||||
}
|
||||
|
||||
const resetD7Ms = parseTimestamp(account.resetD7At);
|
||||
const zeroUsageWarmupUntilMsForState =
|
||||
resetD7Ms != null && resetD7Ms > nowMs
|
||||
? resetD7Ms
|
||||
: nowMs + DEFAULT_ZERO_USAGE_WARMUP_WINDOW_MS;
|
||||
let zeroUsageWarmupUntilMsForState = nowMs;
|
||||
if (!options.ignoreZeroUsageWindow) {
|
||||
zeroUsageWarmupUntilMsForState =
|
||||
resetD7Ms != null && resetD7Ms > nowMs
|
||||
? resetD7Ms
|
||||
: nowMs + DEFAULT_ZERO_USAGE_WARMUP_WINDOW_MS;
|
||||
}
|
||||
|
||||
return {
|
||||
accountIndex: account.index,
|
||||
@@ -254,7 +263,9 @@ export async function runCodexWarmupCycle(
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const statePath = runtime.statePath ?? DEFAULT_STATE_FILE;
|
||||
const state = readWarmupState(statePath);
|
||||
const selected = selectWarmupCandidate(config, state, nowMs);
|
||||
const selected = selectWarmupCandidate(config, state, nowMs, {
|
||||
ignoreZeroUsageWindow: runtime.ignoreZeroUsageWindow,
|
||||
});
|
||||
if ('reason' in selected) {
|
||||
return { status: 'skipped', reason: selected.reason };
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ import { getAllTokens } from './token-rotation.js';
|
||||
const PRIMER_HOURS_KST = [8, 13, 18, 23];
|
||||
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
const CLAUDE_PRIMER_TIMEOUT_MS = 60_000;
|
||||
const CODEX_PRIMER_MIN_INTERVAL_MS = 5 * 60 * 60 * 1000;
|
||||
const CODEX_PRIMER_MAX_USAGE_PCT = 0;
|
||||
const CODEX_PRIMER_MAX_D7_USAGE_PCT = 100;
|
||||
|
||||
function resolveClaudeBinary(): string {
|
||||
const candidates = [
|
||||
@@ -130,7 +133,16 @@ async function runClaudePrimerCycle(): Promise<void> {
|
||||
|
||||
async function runCodexPrimerCycle(): Promise<void> {
|
||||
try {
|
||||
const result = await runCodexWarmupCycle(CODEX_WARMUP_CONFIG);
|
||||
const result = await runCodexWarmupCycle(
|
||||
{
|
||||
...CODEX_WARMUP_CONFIG,
|
||||
enabled: true,
|
||||
minIntervalMs: CODEX_PRIMER_MIN_INTERVAL_MS,
|
||||
maxUsagePct: CODEX_PRIMER_MAX_USAGE_PCT,
|
||||
maxD7UsagePct: CODEX_PRIMER_MAX_D7_USAGE_PCT,
|
||||
},
|
||||
{ ignoreZeroUsageWindow: true },
|
||||
);
|
||||
logger.info({ result }, 'Codex primer cycle finished');
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Codex primer cycle threw');
|
||||
|
||||
Reference in New Issue
Block a user