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:
Codex
2026-06-09 15:01:21 +09:00
parent edeeed9476
commit 0ca24debfd
10 changed files with 270 additions and 170 deletions

View File

@@ -9,45 +9,7 @@ vi.mock('./session-commands.js', () => ({
handleSessionCommand: handleSessionCommandMock,
}));
import {
handleQueuedRunGates,
msSincePreviousPrimerSlotKST,
shouldHoldCodexForPrimerAlignment,
} from './message-runtime-gating.js';
// nowMs such that the shifted KST clock reads the given hour/minute on a day.
const KST_OFFSET = 9 * 60 * 60 * 1000;
const atKst = (hour: number, minute = 0): number =>
Date.UTC(2026, 5, 9, hour, minute) - KST_OFFSET;
describe('primer-alignment hold timing', () => {
it('measures ms since the most recent fixed slot', () => {
expect(msSincePreviousPrimerSlotKST(atKst(8, 0))).toBe(0);
expect(msSincePreviousPrimerSlotKST(atKst(13, 30))).toBe(30 * 60 * 1000);
// Before the day's first slot → previous slot was yesterday's 23:00.
expect(msSincePreviousPrimerSlotKST(atKst(2, 0))).toBe(3 * 60 * 60 * 1000);
});
it('holds during the 0408 KST dawn gap', () => {
expect(shouldHoldCodexForPrimerAlignment(atKst(4, 0))).toBe(true);
expect(shouldHoldCodexForPrimerAlignment(atKst(5, 0))).toBe(true);
expect(shouldHoldCodexForPrimerAlignment(atKst(7, 59))).toBe(true);
expect(shouldHoldCodexForPrimerAlignment(atKst(3, 59))).toBe(false);
});
it('holds briefly right after each fixed slot, then releases', () => {
expect(shouldHoldCodexForPrimerAlignment(atKst(8, 0))).toBe(true);
expect(shouldHoldCodexForPrimerAlignment(atKst(13, 2))).toBe(true);
expect(shouldHoldCodexForPrimerAlignment(atKst(13, 6))).toBe(false);
expect(shouldHoldCodexForPrimerAlignment(atKst(18, 4))).toBe(true);
expect(shouldHoldCodexForPrimerAlignment(atKst(23, 30))).toBe(false);
});
it('does not hold mid-window away from slots and the dawn gap', () => {
expect(shouldHoldCodexForPrimerAlignment(atKst(15, 0))).toBe(false);
expect(shouldHoldCodexForPrimerAlignment(atKst(2, 0))).toBe(false);
});
});
import { handleQueuedRunGates } from './message-runtime-gating.js';
describe('message-runtime-gating', () => {
const baseArgs = {