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