feat(primer): broaden Codex alignment hold to all real consumers; pre+post slot window
Address review gaps in the primer-alignment hold so the fixed-slot primer reliably wins the first-request race for the shared Codex window: - Extract the time-based hold into a dependency-free leaf module (codex-primer-alignment.ts) so every Codex path can share it without import cycles. - Add a pre-slot hold window (2 min) in addition to the post-slot (5 min) and the 04-08 KST dawn gap, closing the "request a beat before the slot" gap. - Hold the dashboard's periodic Codex warm-up too: runCodexWarmupCycle now skips during the hold unless called with isPrimer (the scheduled primer sets it), so the warm-up can't anchor the window ahead of the primer. - Remove the urgent @-mention bypass on the reviewer/arbiter and owner holds — the reset is anchored unconditionally as requested. - Reviewer/arbiter hold resumes exactly once after the window via the existing per-revision claim (poll re-detect; no double run). Note: the unified lease boundary (syncHostCodexSessionFiles) is synchronous and throws a terminal "Codex unavailable", so it can't host a clean defer+resume; gating stays at the async turn-scheduling layer that can re-queue. Honest caveat unchanged: making the primer first is necessary but not sufficient to pin a trailing reset. Verified: typecheck, build, bundle-smoke; full suite 1482 passing with only the 9 pre-existing env-config baseline failures (service-routing/paired-context/ migrate-room owner=claude vs codex-main); new alignment + warmup-hold tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { msUntilNextPrimerSlotKST } from './codex-primer-alignment.js';
|
||||
import { CODEX_WARMUP_CONFIG } from './config.js';
|
||||
import {
|
||||
refreshActiveCodexUsage,
|
||||
@@ -23,14 +24,13 @@ import { getAllTokens } from './token-rotation.js';
|
||||
* 18:00 → 18:33 over ~33 min of activity), so any off-slot Codex call by a
|
||||
* paired-room reviewer/arbiter on the same account would anchor the window
|
||||
* early. To make the slot the *anchor*, the primer is paired with a hold gate
|
||||
* (`shouldHoldCodexForPrimerAlignment` in message-runtime-gating.ts) that
|
||||
* keeps non-primer Codex turns quiet during the dawn gap (04–08 KST) and for a
|
||||
* few minutes after each slot, so the primer wins the first-request race. The
|
||||
* primer is also a best-effort warm-up + success/failure record.
|
||||
* (`shouldHoldCodexForPrimerAlignment` in codex-primer-alignment.ts) that keeps
|
||||
* non-primer Codex turns/warm-ups quiet during the dawn gap (04–08 KST) and the
|
||||
* minutes around each slot, so the primer wins the first-request race. The
|
||||
* primer itself passes `isPrimer` so it is never held. The primer is also a
|
||||
* best-effort warm-up + success/failure record.
|
||||
*/
|
||||
|
||||
export const PRIMER_HOURS_KST = [8, 13, 18, 23];
|
||||
export 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;
|
||||
// Fire the Codex primer unconditionally at every slot, mirroring the Claude
|
||||
@@ -165,13 +165,14 @@ export async function runCodexPrimerCycle(): Promise<void> {
|
||||
maxUsagePct: CODEX_PRIMER_MAX_USAGE_PCT,
|
||||
maxD7UsagePct: CODEX_PRIMER_MAX_D7_USAGE_PCT,
|
||||
},
|
||||
{ ignoreZeroUsageWindow: true },
|
||||
{ ignoreZeroUsageWindow: true, isPrimer: 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.
|
||||
// `isPrimer` exempts this call from the alignment hold (the hold blocks
|
||||
// every *other* Codex path during the slot windows) so the primer is the
|
||||
// first Codex request and anchors the window at the slot. A slot may still
|
||||
// land while the trailing 5h window is exhausted, in which case this records
|
||||
// `failed`; we do not chase the reported reset time, since a trailing window
|
||||
// can keep sliding with later usage.
|
||||
logger.info({ result }, 'Codex primer cycle finished');
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Codex primer cycle threw');
|
||||
@@ -183,26 +184,6 @@ async function runPrimerCycle(): Promise<void> {
|
||||
await Promise.allSettled([runClaudePrimerCycle(), runCodexPrimerCycle()]);
|
||||
}
|
||||
|
||||
export function msUntilNextPrimerSlotKST(nowMs: number = Date.now()): number {
|
||||
// KST = UTC+9 (no DST). Shift so KST clock can be read via getUTC*.
|
||||
const kstNowShifted = nowMs + KST_OFFSET_MS;
|
||||
const kstDate = new Date(kstNowShifted);
|
||||
const kstH = kstDate.getUTCHours();
|
||||
|
||||
let nextH = PRIMER_HOURS_KST.find((h) => h > kstH);
|
||||
let dayOffset = 0;
|
||||
if (nextH === undefined) {
|
||||
nextH = PRIMER_HOURS_KST[0];
|
||||
dayOffset = 1;
|
||||
}
|
||||
|
||||
const target = new Date(kstNowShifted);
|
||||
target.setUTCHours(nextH, 0, 0, 0);
|
||||
if (dayOffset === 1) target.setUTCDate(target.getUTCDate() + 1);
|
||||
|
||||
return target.getTime() - kstNowShifted;
|
||||
}
|
||||
|
||||
let primerTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
export function startUsagePrimer(): void {
|
||||
|
||||
Reference in New Issue
Block a user