diff --git a/src/codex-primer-alignment.test.ts b/src/codex-primer-alignment.test.ts deleted file mode 100644 index 59dc90e..0000000 --- a/src/codex-primer-alignment.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { afterEach, describe, expect, it } from 'vitest'; - -import { - ALIGNMENT_NOTICE_KEY, - beginPrimerAnchor, - endPrimerAnchor, - isAlignmentSlot, - isPrimerAnchoring, - shouldHoldCodexForPrimerAlignment, -} from './codex-primer-alignment.js'; - -const KST_OFFSET_MS = 9 * 60 * 60 * 1000; - -/** Build an epoch-ms value that reads as the given KST wall-clock time. */ -function atKst(hour: number, minute = 0, second = 0): number { - // shouldHold reads KST via `new Date(ms + KST_OFFSET_MS)`, so picking a UTC - // instant equal to the desired KST clock and subtracting the offset - // reproduces that KST time exactly. - const kstShifted = Date.UTC(2026, 5, 10, hour, minute, second); - return kstShifted - KST_OFFSET_MS; -} - -afterEach(() => { - // The anchor flag is module-global; never leak it across tests. - endPrimerAnchor(); -}); - -describe('shouldHoldCodexForPrimerAlignment — dawn window', () => { - it('holds across the full 03:00–08:00 KST dawn window', () => { - expect(shouldHoldCodexForPrimerAlignment(atKst(3, 0))).toBe(true); - expect(shouldHoldCodexForPrimerAlignment(atKst(5, 30))).toBe(true); - expect(shouldHoldCodexForPrimerAlignment(atKst(7, 59, 59))).toBe(true); - }); - - it('keeps holding through the post-08:00 grace, then releases', () => { - // Grace bridges the gap between the 08:00 boundary lapsing and the primer - // setting its anchor flag, so a consumer poll at 08:00:00 cannot slip in. - expect(shouldHoldCodexForPrimerAlignment(atKst(8, 0, 0))).toBe(true); - expect(shouldHoldCodexForPrimerAlignment(atKst(8, 2, 59))).toBe(true); - expect(shouldHoldCodexForPrimerAlignment(atKst(8, 3, 1))).toBe(false); - expect(shouldHoldCodexForPrimerAlignment(atKst(9, 0))).toBe(false); - }); - - it('does not hold before the dawn window', () => { - expect(shouldHoldCodexForPrimerAlignment(atKst(2, 59, 59))).toBe(false); - expect(shouldHoldCodexForPrimerAlignment(atKst(0, 0))).toBe(false); - }); - - it('leaves the other primer slots and daytime hours untouched', () => { - for (const h of [9, 12, 13, 15, 18, 20, 23]) { - expect(shouldHoldCodexForPrimerAlignment(atKst(h, 0))).toBe(false); - } - }); -}); - -describe('shouldHoldCodexForPrimerAlignment — anchor lock', () => { - it('holds while the primer is anchoring, even outside the dawn window', () => { - // Closes the 08:00 race: once the primer begins, all other Codex consumers - // are held until it finishes — regardless of the clock. - const noon = atKst(13, 0); - expect(shouldHoldCodexForPrimerAlignment(noon)).toBe(false); - beginPrimerAnchor(); - expect(isPrimerAnchoring()).toBe(true); - expect(shouldHoldCodexForPrimerAlignment(noon)).toBe(true); - endPrimerAnchor(); - expect(isPrimerAnchoring()).toBe(false); - expect(shouldHoldCodexForPrimerAlignment(noon)).toBe(false); - }); -}); - -describe('isAlignmentSlot', () => { - it('is true only at the 08:00 KST hour', () => { - expect(isAlignmentSlot(atKst(8, 0))).toBe(true); - expect(isAlignmentSlot(atKst(8, 59))).toBe(true); - expect(isAlignmentSlot(atKst(7, 59))).toBe(false); - expect(isAlignmentSlot(atKst(13, 0))).toBe(false); - }); -}); - -describe('ALIGNMENT_NOTICE_KEY', () => { - it('is stable within a KST day and changes across days', () => { - expect(ALIGNMENT_NOTICE_KEY(atKst(3, 0))).toBe( - ALIGNMENT_NOTICE_KEY(atKst(7, 59)), - ); - const dayTwo = atKst(3, 0) + 24 * 60 * 60 * 1000; - expect(ALIGNMENT_NOTICE_KEY(dayTwo)).not.toBe( - ALIGNMENT_NOTICE_KEY(atKst(3, 0)), - ); - }); -}); diff --git a/src/codex-primer-alignment.ts b/src/codex-primer-alignment.ts deleted file mode 100644 index 6acc27c..0000000 --- a/src/codex-primer-alignment.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Codex usage-primer alignment — narrow pre-08:00 quiet window + anchor lock. - * - * The Codex 5h usage limit is a FIXED window: it opens on the first Codex - * request after the previous window expires and resets exactly 5h later - * (verified live — usedPercent climbs while resetsAt stays put). To pin the - * daily reset to 13:00 KST, the 08:00 KST primer must be the *first* Codex - * request of a fresh window. Codex is otherwise used almost continuously - * (reviewers/arbiters every paired room, codex-owner rooms, warm-up), so 08:00 - * normally lands mid-window and the reset drifts (observed 15:47). - * - * Two mechanisms, both time-based / in-process (no I/O, no reliance on the - * unreliable live usage figures): - * - * 1. Dawn quiet window [03:00, 08:00) KST. One Codex window is at most 5h, so - * anything opened before 03:00 has expired by 08:00 — held requests can't - * keep a stray window alive into the slot. - * - * 2. Anchor lock. The 08:00 primer sets an in-process flag for the duration of - * its own Codex call (beginPrimerAnchor/endPrimerAnchor). Held consumers - * stay held while the flag is set, so the primer wins the first-request race - * even though the dawn window's clock boundary lapses at exactly 08:00. A - * short post-slot grace bridges the few ms between the 08:00 boundary lapse - * and the primer firing, so a consumer poll in that gap can't slip in first. - * - * Narrow by design (the broad all-slots version was previously reverted): only - * the 08:00 alignment slot holds; the other slots (13/18/23) and all daytime - * hours pass through untouched. - */ - -const KST_OFFSET_MS = 9 * 60 * 60 * 1000; -const HOUR_MS = 60 * 60 * 1000; - -// Dawn quiet window before the 08:00 KST primer slot. 5h = one full Codex -// window, so any window opened before 03:00 is guaranteed expired by 08:00. -export const DAWN_QUIET_START_MS = 3 * HOUR_MS; -export const DAWN_QUIET_END_MS = 8 * HOUR_MS; - -// Grace after the 08:00 boundary during which consumers stay held even though -// the dawn window technically lapsed — covers the gap between the boundary and -// the primer setting its anchor flag (incl. event-loop lag at the slot). -export const PRIMER_SLOT_GRACE_MS = 3 * 60 * 1000; - -/** The KST clock hour the primer aligns to (08:00). */ -export const ALIGNMENT_SLOT_HOUR_KST = 8; - -let primerAnchorActive = false; - -/** Mark the 08:00 alignment primer as in-flight so consumers stay held. */ -export function beginPrimerAnchor(): void { - primerAnchorActive = true; -} - -/** Clear the anchor flag once the alignment primer's Codex call has finished. */ -export function endPrimerAnchor(): void { - primerAnchorActive = false; -} - -/** Whether the 08:00 alignment primer is currently anchoring the window. */ -export function isPrimerAnchoring(): boolean { - return primerAnchorActive; -} - -/** Milliseconds since KST midnight for the given instant. */ -function kstMsOfDay(nowMs: number): number { - const shifted = new Date(nowMs + KST_OFFSET_MS); - return ( - ((shifted.getUTCHours() * 60 + shifted.getUTCMinutes()) * 60 + - shifted.getUTCSeconds()) * - 1000 + - shifted.getUTCMilliseconds() - ); -} - -/** Whether the given instant is the 08:00 KST alignment slot (hour granularity). */ -export function isAlignmentSlot(nowMs: number = Date.now()): boolean { - return ( - new Date(nowMs + KST_OFFSET_MS).getUTCHours() === ALIGNMENT_SLOT_HOUR_KST - ); -} - -/** - * A stable key identifying the current dawn hold window (its KST calendar date), - * used to dedupe the "please wait" notice to at most once per room per window. - */ -export function ALIGNMENT_NOTICE_KEY(nowMs: number = Date.now()): string { - return new Date(nowMs + KST_OFFSET_MS).toISOString().slice(0, 10); -} - -/** - * Whether non-primer Codex execution should be held right now so the 08:00 - * fixed-slot primer is the window-anchoring first request. Holds while the - * alignment primer is anchoring, and during [03:00, 08:00 + grace) KST; every - * other hour returns false. - */ -export function shouldHoldCodexForPrimerAlignment( - nowMs: number = Date.now(), -): boolean { - if (primerAnchorActive) return true; - const msOfDay = kstMsOfDay(nowMs); - return ( - msOfDay >= DAWN_QUIET_START_MS && - msOfDay < DAWN_QUIET_END_MS + PRIMER_SLOT_GRACE_MS - ); -} diff --git a/src/codex-warmup.ts b/src/codex-warmup.ts index 9e09388..8c0f491 100644 --- a/src/codex-warmup.ts +++ b/src/codex-warmup.ts @@ -7,7 +7,6 @@ import { fileURLToPath } from 'url'; import { DATA_DIR } from './config.js'; import type { AppConfig } from './config/schema.js'; -import { shouldHoldCodexForPrimerAlignment } from './codex-primer-alignment.js'; import { getAllCodexAccounts, getCodexAuthPath, @@ -342,13 +341,6 @@ export async function runCodexWarmupCycle( return { status: 'skipped', reason: 'runtime_busy' }; const nowMs = runtime.nowMs ?? Date.now(); - // Hold every non-primer warm-up during the pre-08:00 dawn window so it can't - // anchor the shared Codex 5h window ahead of the scheduled primer (which - // passes forceAttempt). Skipping is safe — an opportunistic warm-up is - // best-effort. The fixed-slot primer itself (forceAttempt) is never held. - if (!runtime.forceAttempt && shouldHoldCodexForPrimerAlignment(nowMs)) { - return { status: 'skipped', reason: 'primer_alignment_hold' }; - } const nowIso = new Date(nowMs).toISOString(); const statePath = runtime.statePath ?? DEFAULT_STATE_FILE; const state = readWarmupState(statePath); diff --git a/src/message-agent-executor-paired.test.ts b/src/message-agent-executor-paired.test.ts index 07e0c8c..e1019c8 100644 --- a/src/message-agent-executor-paired.test.ts +++ b/src/message-agent-executor-paired.test.ts @@ -26,7 +26,11 @@ vi.mock('./message-runtime-follow-up.js', () => ({ import type { AgentOutput } from './agent-runner.js'; import * as db from './db.js'; import * as pairedExecutionContextModule from './paired-execution-context.js'; -import { createPairedExecutionLifecycle } from './message-agent-executor-paired.js'; +import { + createPairedExecutionLifecycle, + resolveOwnerNextStepNotice, +} from './message-agent-executor-paired.js'; +import type { PairedTask } from './types.js'; const log = { info: vi.fn(), @@ -325,3 +329,42 @@ describe('createPairedExecutionLifecycle completion handling', () => { expect(enqueueMessageCheck).not.toHaveBeenCalled(); }); }); + +describe('resolveOwnerNextStepNotice routing indicator', () => { + const task = (over: Partial): PairedTask => + ({ + status: 'active', + reviewer_agent_type: 'claude-code', + completion_reason: null, + ...over, + }) as PairedTask; + + it('announces reviewer request while waiting', () => { + expect(resolveOwnerNextStepNotice(task({ status: 'review_ready' }))).toBe( + '🔁 리뷰어 검토 요청됨 — 리뷰어 응답을 기다리는 중입니다.', + ); + }); + + it('announces arbiter call when arbiter is requested', () => { + expect(resolveOwnerNextStepNotice(task({ status: 'arbiter_requested' }))).toBe( + '⚖️ 의견 차이로 중재자 호출됨 — 중재 판정을 기다리는 중입니다.', + ); + }); + + it('stays silent when the task finishes (no redundant done line)', () => { + expect( + resolveOwnerNextStepNotice( + task({ status: 'completed', completion_reason: 'done' }), + ), + ).toBeNull(); + expect( + resolveOwnerNextStepNotice( + task({ status: 'completed', completion_reason: 'escalated' }), + ), + ).toBeNull(); + }); + + it('stays silent for in-progress owner work', () => { + expect(resolveOwnerNextStepNotice(task({ status: 'active' }))).toBeNull(); + }); +}); diff --git a/src/message-agent-executor-paired.ts b/src/message-agent-executor-paired.ts index 018340b..f508cae 100644 --- a/src/message-agent-executor-paired.ts +++ b/src/message-agent-executor-paired.ts @@ -64,25 +64,63 @@ function completeStoredExecution( }); } +/** + * Routing indicator shown to the user after an owner turn when the turn does + * NOT end the task — i.e. when more is still pending — so a same-looking status + * line (e.g. TASK_DONE) no longer reads as inconsistent: the user can always + * tell whether the reviewer was requested or the arbiter was called, vs. the + * task simply finishing (no line — the owner's final message is the ending). + * Purely additive — it reflects the routing the state machine already chose; it + * does not change any routing, and it never duplicates the completion message. + */ +export function resolveOwnerNextStepNotice( + task: PairedTaskRecord, +): string | null { + switch (task.status) { + case 'review_ready': + case 'in_review': + return '🔁 리뷰어 검토 요청됨 — 리뷰어 응답을 기다리는 중입니다.'; + case 'arbiter_requested': + case 'in_arbitration': + return '⚖️ 의견 차이로 중재자 호출됨 — 중재 판정을 기다리는 중입니다.'; + default: + // 'completed' included: the owner's final message is itself the ending, + // so no extra done/escalation line is appended here (escalation has its + // own dedicated notice above). + return null; + } +} + async function notifyPairedCompletionIfNeeded(args: { task: PairedTaskRecord | null | undefined; chatJid: string; + completedRole: PairedRoomRole; onOutput?: (output: AgentOutput) => Promise; }): Promise { - if (args.task?.status !== 'completed' || !args.task.completion_reason) return; - const sender = getLastHumanMessageSender(args.chatJid); - const mention = sender ? `<@${sender}>` : ''; - const notifications: Record = { - escalated: `${mention} ⚠️ 자동 해결 불가 — 확인이 필요합니다.`, - }; - const message = notifications[args.task.completion_reason]; - if (!message) return; - await args.onOutput?.({ - status: 'success', - result: message, - output: { visibility: 'public', text: message }, - phase: 'final', - }); + const task = args.task; + if (!task) return; + + const emit = (text: string): Promise | undefined => + args.onOutput?.({ + status: 'success', + result: text, + output: { visibility: 'public', text }, + phase: 'final', + }); + + // Escalation notice fires for any role's completion that escalated to the user. + if (task.status === 'completed' && task.completion_reason === 'escalated') { + const sender = getLastHumanMessageSender(args.chatJid); + const mention = sender ? `<@${sender}> ` : ''; + await emit(`${mention}⚠️ 자동 해결 불가 — 확인이 필요합니다.`); + return; + } + + // Owner turn: surface the next routing step so the ending is unambiguous. + if (args.completedRole !== 'owner') return; + const notice = resolveOwnerNextStepNotice(task); + if (!notice) return; + await emit(notice); } export interface PairedExecutionLifecycle { @@ -581,7 +619,8 @@ class PairedExecutionLifecycleController implements PairedExecutionLifecycle { private async notifyCompletionAndQueueFollowUp( state: FinalizeState, ): Promise { - const { chatJid, onOutput, pairedExecutionContext } = this.args; + const { chatJid, completedRole, onOutput, pairedExecutionContext } = + this.args; if (!pairedExecutionContext || state.interruptedByHumanMessage) { return; } @@ -589,6 +628,7 @@ class PairedExecutionLifecycleController implements PairedExecutionLifecycle { await notifyPairedCompletionIfNeeded({ task: finishedTask, chatJid, + completedRole, onOutput, }); this.queueFollowUpIfNeeded(state, finishedTask); diff --git a/src/message-runtime-gating.test.ts b/src/message-runtime-gating.test.ts index 95f71ca..af2cbe8 100644 --- a/src/message-runtime-gating.test.ts +++ b/src/message-runtime-gating.test.ts @@ -1,25 +1,14 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { RegisteredGroup } from './types.js'; -const { handleSessionCommandMock, getEffectiveChannelLeaseMock } = vi.hoisted( - () => ({ - handleSessionCommandMock: vi.fn(), - getEffectiveChannelLeaseMock: vi.fn(), - }), -); +const { handleSessionCommandMock } = vi.hoisted(() => ({ + handleSessionCommandMock: vi.fn(), +})); vi.mock('./session-commands.js', () => ({ handleSessionCommand: handleSessionCommandMock, })); -vi.mock('./service-routing.js', () => ({ - getEffectiveChannelLease: getEffectiveChannelLeaseMock, -})); - -import { - beginPrimerAnchor, - endPrimerAnchor, -} from './codex-primer-alignment.js'; import { handleQueuedRunGates } from './message-runtime-gating.js'; function makeArgs(overrides: { @@ -49,16 +38,6 @@ function makeArgs(overrides: { describe('message-runtime-gating', () => { beforeEach(() => { handleSessionCommandMock.mockReset(); - getEffectiveChannelLeaseMock.mockReset(); - // Default: claude owner, no failover → no alignment hold. - getEffectiveChannelLeaseMock.mockReturnValue({ - owner_agent_type: 'claude-code', - owner_failover_active: false, - }); - }); - - afterEach(() => { - endPrimerAnchor(); }); it('returns session-command results directly when a command is handled', async () => { @@ -72,49 +51,11 @@ describe('message-runtime-gating', () => { expect(result).toEqual({ handled: true, success: false }); }); - it('falls through when no command and the room owner is not on Codex', async () => { + it('falls through when no session command matched', async () => { handleSessionCommandMock.mockResolvedValue({ handled: false }); - beginPrimerAnchor(); // force the alignment window on, isolate provider check const result = await handleQueuedRunGates(makeArgs({})); expect(result).toEqual({ handled: false }); }); - - it('holds a codex-owner turn during the alignment window and notifies once', async () => { - handleSessionCommandMock.mockResolvedValue({ handled: false }); - getEffectiveChannelLeaseMock.mockReturnValue({ - owner_agent_type: 'codex', - owner_failover_active: false, - }); - beginPrimerAnchor(); - const sendMessage = vi.fn(async () => {}); - - const first = await handleQueuedRunGates( - makeArgs({ chatJid: 'room-codex', sendMessage }), - ); - const second = await handleQueuedRunGates( - makeArgs({ chatJid: 'room-codex', sendMessage }), - ); - - expect(first).toEqual({ handled: true, success: true }); - expect(second).toEqual({ handled: true, success: true }); - // Notice deduped: sent once across repeated holds in the same window. - expect(sendMessage).toHaveBeenCalledTimes(1); - }); - - it('holds a claude-owner room when global failover routes the owner to Codex', async () => { - handleSessionCommandMock.mockResolvedValue({ handled: false }); - getEffectiveChannelLeaseMock.mockReturnValue({ - owner_agent_type: 'claude-code', - owner_failover_active: true, - }); - beginPrimerAnchor(); - - const result = await handleQueuedRunGates( - makeArgs({ chatJid: 'room-failover' }), - ); - - expect(result).toEqual({ handled: true, success: true }); - }); }); diff --git a/src/message-runtime-gating.ts b/src/message-runtime-gating.ts index 5a2f56f..2a0c1c9 100644 --- a/src/message-runtime-gating.ts +++ b/src/message-runtime-gating.ts @@ -1,29 +1,9 @@ -import { - ALIGNMENT_NOTICE_KEY, - shouldHoldCodexForPrimerAlignment, -} from './codex-primer-alignment.js'; -import { logger } from './logger.js'; -import { getEffectiveChannelLease } from './service-routing.js'; import { handleSessionCommand, type SessionCommandDeps, } from './session-commands.js'; import type { NewMessage, RegisteredGroup } from './types.js'; -const CODEX_ALIGNMENT_AUTO_REPLY = - '코덱스 5시간 한도 리셋을 매일 오후 1시에 고정하기 위해, 새벽 03~08시 구간은 정렬용으로 잠시 대기합니다. 08시 이후 다시 보내주세요.'; - -// Per-room dedupe so a held owner room gets the wait notice at most once per -// dawn hold window, instead of on every re-poll while it stays held. -const lastAlignmentNoticeKeyByChat = new Map(); - -function shouldSendAlignmentNotice(chatJid: string): boolean { - const key = ALIGNMENT_NOTICE_KEY(); - if (lastAlignmentNoticeKeyByChat.get(chatJid) === key) return false; - lastAlignmentNoticeKeyByChat.set(chatJid, key); - return true; -} - export async function handleQueuedRunGates(args: { chatJid: string; group: RegisteredGroup; @@ -50,36 +30,5 @@ export async function handleQueuedRunGates(args: { return cmdResult; } - // ── Codex primer-alignment hold (codex-owner rooms) ── - // Reviewer/arbiter Codex turns are held at their own dispatch site; here we - // cover rooms whose *owner* effectively runs on Codex, so a fresh human turn - // doesn't anchor the shared window before the 08:00 fixed-slot primer. Keyed - // on the EFFECTIVE owner backend — configured codex owner OR a global-failover - // override to Codex — not just the static group agent type. Only fires inside - // the dawn hold window, so daytime turns are never delayed. Returning handled - // without advancing the message cursor lets the turn resume on the next poll - // exactly once after the window clears. - const lease = getEffectiveChannelLease(args.chatJid); - const ownerRunsCodex = - lease.owner_agent_type === 'codex' || lease.owner_failover_active === true; - if (ownerRunsCodex && shouldHoldCodexForPrimerAlignment()) { - logger.info( - { - chatJid: args.chatJid, - groupName: args.group.name, - runId: args.runId, - }, - 'Holding fresh Codex owner turn — keeping 08:00 primer slot as first request', - ); - if (shouldSendAlignmentNotice(args.chatJid)) { - try { - await args.sessionCommandDeps.sendMessage(CODEX_ALIGNMENT_AUTO_REPLY); - } catch (err) { - logger.warn({ err }, 'Failed to send Codex alignment notice'); - } - } - return { handled: true, success: true }; - } - return { handled: false }; } diff --git a/src/message-runtime-queue.ts b/src/message-runtime-queue.ts index d89f150..a2ac4d7 100644 --- a/src/message-runtime-queue.ts +++ b/src/message-runtime-queue.ts @@ -12,8 +12,6 @@ import { } from './message-runtime-flow.js'; import { buildPairedTurnIdentity } from './paired-turn-identity.js'; import { resolveStoredVisibleVerdict } from './paired-verdict.js'; -import { shouldHoldCodexForPrimerAlignment } from './codex-primer-alignment.js'; -import { getEffectiveChannelLease } from './service-routing.js'; import { advanceLastAgentCursor, finalizeQueuedRunCursor, @@ -64,23 +62,6 @@ function timestampMs(value: string | null | undefined): number | null { return Number.isFinite(parsed) ? parsed : null; } -/** - * If the pending paired turn is a Codex reviewer/arbiter turn that must wait for - * the pre-08:00 primer-alignment window, return its role; otherwise null. - */ -function codexRoleHeldForPrimerAlignment( - chatJid: string, - taskStatus: string | null | undefined, -): 'reviewer' | 'arbiter' | null { - const role = resolveActiveRole(taskStatus); - if (role !== 'reviewer' && role !== 'arbiter') return null; - if (!shouldHoldCodexForPrimerAlignment()) return null; - const lease = getEffectiveChannelLease(chatJid); - const agentType = - role === 'reviewer' ? lease.reviewer_agent_type : lease.arbiter_agent_type; - return agentType === 'codex' ? role : null; -} - function isHumanMessageFreshForTask( task: PairedTask | null | undefined, message: NewMessage, @@ -238,21 +219,6 @@ export async function runPendingPairedTurnIfNeeded(args: { return null; } - // ── Codex primer-alignment hold (reviewer/arbiter turns) ── - // Reviewer/arbiter turns are the off-slot Codex usage that anchors the shared - // 5h window early and drifts the reset. Inside the pre-08:00 dawn window we - // defer them so the fixed-slot primer stays the first request. The open task - // is re-detected on the next poll and the per-revision claim guarantees it - // resumes exactly once after the window clears (no work lost, no double run). - const heldRole = codexRoleHeldForPrimerAlignment(chatJid, task.status); - if (heldRole) { - args.log.info( - { chatJid, taskId: task.id, taskStatus: task.status, role: heldRole }, - 'Holding Codex reviewer/arbiter turn — keeping 08:00 primer slot as first request', - ); - return true; - } - if (pendingTurn.channel) { const claimed = claimPairedTurnExecution({ chatJid, diff --git a/src/usage-primer.ts b/src/usage-primer.ts index aa34431..5b0236b 100644 --- a/src/usage-primer.ts +++ b/src/usage-primer.ts @@ -2,11 +2,6 @@ import { spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; -import { - beginPrimerAnchor, - endPrimerAnchor, - isAlignmentSlot, -} from './codex-primer-alignment.js'; import { CODEX_WARMUP_CONFIG } from './config.js'; import { refreshActiveCodexUsage } from './codex-usage-collector.js'; import { runCodexWarmupCycle } from './codex-warmup.js'; @@ -154,12 +149,6 @@ async function runClaudePrimerCycle(): Promise { } export async function runCodexPrimerCycle(): Promise { - // At the 08:00 alignment slot, hold all other Codex consumers (via the shared - // shouldHoldCodexForPrimerAlignment flag) for the duration of this primer's - // Codex call, so the primer wins the first-request race that opens the fresh - // 5h window. Other slots run without the anchor lock. - const anchoring = isAlignmentSlot(); - if (anchoring) beginPrimerAnchor(); try { // Fire immediately at the slot. There is NO pre-call usage refresh on // purpose: forceAttempt fires regardless of usage, so reading usage first @@ -181,12 +170,10 @@ export async function runCodexPrimerCycle(): Promise { ); logger.info({ result }, 'Codex primer cycle finished'); // Observability only — runs AFTER the call so it never delays it: refresh - // so the newly-anchored reset window shows up in usage data/logs. + // so the new reset window shows up in usage data/logs. await refreshActiveCodexUsage(); } catch (err) { logger.warn({ err }, 'Codex primer cycle threw'); - } finally { - if (anchoring) endPrimerAnchor(); } }