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:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user