fix(primer): force a Codex command at every fixed slot (bypass warm-up skip gates)

The scheduled primer reused the opportunistic warm-up gates, so the 18:00 slot
(exactly 5h after 13:00, but firing a few hundred ms later) fell just under
minIntervalMs and was silently skipped as no_eligible_accounts — the primer did
not actually fire at every slot. Add a forceAttempt path: the fixed-slot primer
bypasses stagger, post-failure cooldown, min-interval, and per-account
usage/rate-limit filters, attempting a real Codex command on the active account
at every 08/13/18/23 slot and recording success/failure.

Reviewer/arbiter holding stays removed (per user). This does not pin the reset
clock — a trailing window still slides with later usage — but it guarantees the
"simple task at every fixed slot" the user asked for.

Verified: typecheck, build, bundle-smoke; new regression tests (13:00 success
then sub-5h 18:00 still fires; control without forceAttempt is skipped;
forceAttempt bypasses cooldown/stagger) + usage-primer/codex-warmup suites pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-09 15:17:59 +09:00
parent b3a927b241
commit 4ddaa88f8a
4 changed files with 145 additions and 4 deletions

View File

@@ -36,6 +36,15 @@ interface CodexWarmupRuntimeOptions {
statePath?: string;
shouldSkip?: () => boolean;
ignoreZeroUsageWindow?: boolean;
/**
* Fixed-slot primer mode: bypass the opportunistic warm-up skip gates
* (stagger, post-failure cooldown, min-interval, per-account usage/rate-limit
* filters) so a real Codex command is attempted at every 08/13/18/23 slot and
* its success/failure recorded. Without this, two slots exactly 5h apart fall
* just under minIntervalMs (the primer fires a few hundred ms after the slot)
* and the later slot is silently skipped as `no_eligible_accounts`.
*/
forceAttempt?: boolean;
}
export type CodexWarmupCycleResult =
@@ -162,15 +171,20 @@ function selectWarmupCandidate(
config: CodexWarmupConfig,
state: CodexWarmupState,
nowMs: number,
options: { ignoreZeroUsageWindow?: boolean } = {},
options: { ignoreZeroUsageWindow?: boolean; forceAttempt?: boolean } = {},
): { accountIndex: number; zeroUsageWarmupUntil: string } | { reason: string } {
const disabledUntilMs = parseTimestamp(state.disabledUntil);
if (disabledUntilMs != null && disabledUntilMs > nowMs) {
if (
!options.forceAttempt &&
disabledUntilMs != null &&
disabledUntilMs > nowMs
) {
return { reason: 'disabled_cooldown' };
}
const lastGlobalWarmupMs = parseTimestamp(state.lastWarmupAt);
if (
!options.forceAttempt &&
lastGlobalWarmupMs != null &&
config.staggerMs > 0 &&
nowMs - lastGlobalWarmupMs < config.staggerMs
@@ -181,6 +195,16 @@ function selectWarmupCandidate(
const accounts = getAllCodexAccounts();
if (accounts.length === 0) return { reason: 'no_accounts' };
// Fixed-slot primer: attempt a real command on the active account every slot,
// bypassing the per-account usage/rate-limit/min-interval filters below.
if (options.forceAttempt) {
const active = accounts.find((a) => a.isActive) ?? accounts[0];
return {
accountIndex: active.index,
zeroUsageWarmupUntil: new Date(nowMs).toISOString(),
};
}
for (const account of accounts) {
if (account.isRateLimited) continue;
if (typeof account.cachedUsagePct !== 'number') continue;
@@ -322,6 +346,7 @@ export async function runCodexWarmupCycle(
const state = readWarmupState(statePath);
const selected = selectWarmupCandidate(config, state, nowMs, {
ignoreZeroUsageWindow: runtime.ignoreZeroUsageWindow,
forceAttempt: runtime.forceAttempt,
});
if ('reason' in selected) {
return { status: 'skipped', reason: selected.reason };