From 8883cd166d2572e620e12d5ab68fc2abf06e0c0e Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 8 Jun 2026 15:48:04 +0900 Subject: [PATCH] fix(gating): drop Codex 'wait until next primer' hold so Codex runs like Claude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Codex-only shouldHoldFreshCodexUntilPrimer gate blocked fresh Codex turns outside the dawn window and replied "Codex 사용량 정렬 대기 중... 다음 프라이머 시각 이후 다시 보내주세요", which Claude rooms never did. The primer (usage-primer.ts) already fires unconditionally for both providers at 08/13/18/23 KST, so this hold only delayed user turns without affecting window alignment. Remove the gate and its now-orphaned helpers (shouldHoldFreshCodexUntilPrimer, CODEX_ALIGNMENT_AUTO_REPLY, msUntilNextPrimerSlotKST, formatPrimerHourKST, and the getAllCodexAccounts import). Keep the 04-08 KST dawn alignment gap block and the usage-exhaustion gate untouched. Co-Authored-By: Claude Opus 4 --- src/message-runtime-gating.test.ts | 62 ++++--------------------- src/message-runtime-gating.ts | 73 ------------------------------ 2 files changed, 8 insertions(+), 127 deletions(-) diff --git a/src/message-runtime-gating.test.ts b/src/message-runtime-gating.test.ts index 63418bb..7285d27 100644 --- a/src/message-runtime-gating.test.ts +++ b/src/message-runtime-gating.test.ts @@ -1,15 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { RegisteredGroup } from './types.js'; -const { getAllCodexAccountsMock, handleSessionCommandMock } = vi.hoisted( - () => ({ - getAllCodexAccountsMock: vi.fn(), - handleSessionCommandMock: vi.fn(), - }), -); - -vi.mock('./codex-token-rotation.js', () => ({ - getAllCodexAccounts: getAllCodexAccountsMock, +const { handleSessionCommandMock } = vi.hoisted(() => ({ + handleSessionCommandMock: vi.fn(), })); vi.mock('./session-commands.js', () => ({ @@ -19,7 +12,6 @@ vi.mock('./session-commands.js', () => ({ import { exhaustionChecks, handleQueuedRunGates, - shouldHoldFreshCodexUntilPrimer, } from './message-runtime-gating.js'; describe('message-runtime-gating', () => { @@ -46,8 +38,6 @@ describe('message-runtime-gating', () => { beforeEach(() => { handleSessionCommandMock.mockReset(); sendMessage.mockReset(); - getAllCodexAccountsMock.mockReset(); - getAllCodexAccountsMock.mockReturnValue([]); exhaustionChecks.claude = () => ({ exhausted: false, nextResetAt: null }); exhaustionChecks.codex = () => ({ exhausted: false, nextResetAt: null }); }); @@ -141,46 +131,12 @@ describe('message-runtime-gating', () => { expect(sendMessage).not.toHaveBeenCalled(); }); - it('holds a fresh Codex account until the next fixed primer slot', async () => { - getAllCodexAccountsMock.mockReturnValue([ - { - index: 0, - accountId: 'codex-1', - planType: 'pro', - isActive: true, - isRateLimited: false, - cachedUsagePct: 1, - cachedUsageD7Pct: 22, - }, - ]); - - expect( - shouldHoldFreshCodexUntilPrimer( - new Date('2026-06-01T06:30:00.000Z').getTime(), // 15:30 KST - ), - ).toBe(true); - expect( - shouldHoldFreshCodexUntilPrimer( - new Date('2026-06-01T09:00:30.000Z').getTime(), // 18:00:30 KST - ), - ).toBe(false); - }); - - it('blocks fresh Codex turns before the next primer slot and notifies the channel', async () => { + it('does not hold a fresh Codex turn until a primer slot — Codex runs like Claude', async () => { handleSessionCommandMock.mockResolvedValue({ handled: false }); - getAllCodexAccountsMock.mockReturnValue([ - { - index: 0, - accountId: 'codex-1', - planType: 'pro', - isActive: true, - isRateLimited: false, - cachedUsagePct: 1, - cachedUsageD7Pct: 22, - }, - ]); + // 15:30 KST: well outside the 04-08 dawn gap. A fresh Codex account used to + // be held back with a "wait until next primer" reply; that behavior is gone. vi.spyOn(Date, 'now').mockReturnValue( - new Date('2026-06-01T06:30:00.000Z').getTime(), // 15:30 KST + new Date('2026-06-01T06:30:00.000Z').getTime(), ); const result = await handleQueuedRunGates({ @@ -188,9 +144,7 @@ describe('message-runtime-gating', () => { group: { ...baseArgs.group, agentType: 'codex' }, }); - expect(result).toEqual({ handled: true, success: true }); - expect(sendMessage).toHaveBeenCalledWith( - expect.stringContaining('18:00 KST'), - ); + expect(result).toEqual({ handled: false }); + expect(sendMessage).not.toHaveBeenCalled(); }); }); diff --git a/src/message-runtime-gating.ts b/src/message-runtime-gating.ts index dfe5ba3..a15cc59 100644 --- a/src/message-runtime-gating.ts +++ b/src/message-runtime-gating.ts @@ -6,7 +6,6 @@ import { CODEX_EXHAUSTION_THRESHOLD_PCT, getCodexUsageExhaustion, } from './codex-usage-collector.js'; -import { getAllCodexAccounts } from './codex-token-rotation.js'; import { logger } from './logger.js'; import { handleSessionCommand, @@ -67,65 +66,16 @@ function exhaustionMessageFor( // is processed anyway — user explicitly tagged the bot, accept the drift. const GAP_START_HOUR_KST = 4; const GAP_END_HOUR_KST = 8; -const PRIMER_HOURS_KST = [8, 13, 18, 23]; const KST_OFFSET_MS = 9 * 60 * 60 * 1000; -const CODEX_ALIGNMENT_FRESH_USAGE_PCT = 1; -const PRIMER_GRACE_MS = 2 * 60 * 1000; export function isInUsageAlignmentGap(nowMs: number = Date.now()): boolean { const kstHour = new Date(nowMs + KST_OFFSET_MS).getUTCHours(); return kstHour >= GAP_START_HOUR_KST && kstHour < GAP_END_HOUR_KST; } -function msUntilNextPrimerSlotKST(nowMs: number): number { - 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; -} - -function formatPrimerHourKST(nowMs: number): string { - const nextMs = nowMs + msUntilNextPrimerSlotKST(nowMs); - const kst = new Date(nextMs + KST_OFFSET_MS); - return `${String(kst.getUTCHours()).padStart(2, '0')}:00 KST`; -} - -export function shouldHoldFreshCodexUntilPrimer( - nowMs: number = Date.now(), -): boolean { - const accounts = getAllCodexAccounts(); - const active = accounts.find((a) => a.isActive) ?? accounts[0]; - if (!active || active.isRateLimited) return false; - const h5 = active.cachedUsagePct; - if (h5 == null || h5 < 0 || h5 > CODEX_ALIGNMENT_FRESH_USAGE_PCT) { - return false; - } - - const elapsedFromPreviousSlot = - (5 * 60 * 60 * 1000 - msUntilNextPrimerSlotKST(nowMs)) % - (5 * 60 * 60 * 1000); - return elapsedFromPreviousSlot > PRIMER_GRACE_MS; -} - const GAP_AUTO_REPLY = '사용량 정렬 시간(04~08 KST)입니다. 08시 이후 다시 보내주세요. 긴급한 경우 @태그하면 처리됩니다.'; -const CODEX_ALIGNMENT_AUTO_REPLY = (nowMs: number) => - `Codex 사용량 정렬 대기 중입니다. 다음 프라이머 시각(${formatPrimerHourKST( - nowMs, - )}) 이후 다시 보내주세요. 긴급한 경우 @태그하면 처리됩니다.`; - export async function handleQueuedRunGates(args: { chatJid: string; group: RegisteredGroup; @@ -176,29 +126,6 @@ export async function handleQueuedRunGates(args: { } const agentType: AgentType = args.group.agentType ?? 'claude-code'; - if (agentType === 'codex' && shouldHoldFreshCodexUntilPrimer()) { - const hasUrgentMention = args.missedMessages.some((m) => - args.triggerPattern.test(m.content), - ); - if (!hasUrgentMention) { - logger.info( - { - chatJid: args.chatJid, - groupName: args.group.name, - runId: args.runId, - }, - 'Blocking new Codex turn — waiting for fixed primer slot', - ); - try { - await args.sessionCommandDeps.sendMessage( - CODEX_ALIGNMENT_AUTO_REPLY(Date.now()), - ); - } catch (err) { - logger.warn({ err }, 'Failed to send Codex alignment notice'); - } - return { handled: true, success: true }; - } - } // ── Usage-exhaustion gate ── // Block new turns when the agent provider that would handle this room has