fix(gating): drop Codex 'wait until next primer' hold so Codex runs like Claude

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 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-08 15:48:04 +09:00
parent 80bc078a24
commit 8883cd166d
2 changed files with 8 additions and 127 deletions

View File

@@ -1,15 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { RegisteredGroup } from './types.js'; import type { RegisteredGroup } from './types.js';
const { getAllCodexAccountsMock, handleSessionCommandMock } = vi.hoisted( const { handleSessionCommandMock } = vi.hoisted(() => ({
() => ({
getAllCodexAccountsMock: vi.fn(),
handleSessionCommandMock: vi.fn(), handleSessionCommandMock: vi.fn(),
}),
);
vi.mock('./codex-token-rotation.js', () => ({
getAllCodexAccounts: getAllCodexAccountsMock,
})); }));
vi.mock('./session-commands.js', () => ({ vi.mock('./session-commands.js', () => ({
@@ -19,7 +12,6 @@ vi.mock('./session-commands.js', () => ({
import { import {
exhaustionChecks, exhaustionChecks,
handleQueuedRunGates, handleQueuedRunGates,
shouldHoldFreshCodexUntilPrimer,
} from './message-runtime-gating.js'; } from './message-runtime-gating.js';
describe('message-runtime-gating', () => { describe('message-runtime-gating', () => {
@@ -46,8 +38,6 @@ describe('message-runtime-gating', () => {
beforeEach(() => { beforeEach(() => {
handleSessionCommandMock.mockReset(); handleSessionCommandMock.mockReset();
sendMessage.mockReset(); sendMessage.mockReset();
getAllCodexAccountsMock.mockReset();
getAllCodexAccountsMock.mockReturnValue([]);
exhaustionChecks.claude = () => ({ exhausted: false, nextResetAt: null }); exhaustionChecks.claude = () => ({ exhausted: false, nextResetAt: null });
exhaustionChecks.codex = () => ({ exhausted: false, nextResetAt: null }); exhaustionChecks.codex = () => ({ exhausted: false, nextResetAt: null });
}); });
@@ -141,46 +131,12 @@ describe('message-runtime-gating', () => {
expect(sendMessage).not.toHaveBeenCalled(); expect(sendMessage).not.toHaveBeenCalled();
}); });
it('holds a fresh Codex account until the next fixed primer slot', async () => { it('does not hold a fresh Codex turn until a primer slot — Codex runs like Claude', 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 () => {
handleSessionCommandMock.mockResolvedValue({ handled: false }); handleSessionCommandMock.mockResolvedValue({ handled: false });
getAllCodexAccountsMock.mockReturnValue([ // 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.
index: 0,
accountId: 'codex-1',
planType: 'pro',
isActive: true,
isRateLimited: false,
cachedUsagePct: 1,
cachedUsageD7Pct: 22,
},
]);
vi.spyOn(Date, 'now').mockReturnValue( 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({ const result = await handleQueuedRunGates({
@@ -188,9 +144,7 @@ describe('message-runtime-gating', () => {
group: { ...baseArgs.group, agentType: 'codex' }, group: { ...baseArgs.group, agentType: 'codex' },
}); });
expect(result).toEqual({ handled: true, success: true }); expect(result).toEqual({ handled: false });
expect(sendMessage).toHaveBeenCalledWith( expect(sendMessage).not.toHaveBeenCalled();
expect.stringContaining('18:00 KST'),
);
}); });
}); });

View File

@@ -6,7 +6,6 @@ import {
CODEX_EXHAUSTION_THRESHOLD_PCT, CODEX_EXHAUSTION_THRESHOLD_PCT,
getCodexUsageExhaustion, getCodexUsageExhaustion,
} from './codex-usage-collector.js'; } from './codex-usage-collector.js';
import { getAllCodexAccounts } from './codex-token-rotation.js';
import { logger } from './logger.js'; import { logger } from './logger.js';
import { import {
handleSessionCommand, handleSessionCommand,
@@ -67,65 +66,16 @@ function exhaustionMessageFor(
// is processed anyway — user explicitly tagged the bot, accept the drift. // is processed anyway — user explicitly tagged the bot, accept the drift.
const GAP_START_HOUR_KST = 4; const GAP_START_HOUR_KST = 4;
const GAP_END_HOUR_KST = 8; const GAP_END_HOUR_KST = 8;
const PRIMER_HOURS_KST = [8, 13, 18, 23];
const KST_OFFSET_MS = 9 * 60 * 60 * 1000; 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 { export function isInUsageAlignmentGap(nowMs: number = Date.now()): boolean {
const kstHour = new Date(nowMs + KST_OFFSET_MS).getUTCHours(); const kstHour = new Date(nowMs + KST_OFFSET_MS).getUTCHours();
return kstHour >= GAP_START_HOUR_KST && kstHour < GAP_END_HOUR_KST; 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 = const GAP_AUTO_REPLY =
'사용량 정렬 시간(04~08 KST)입니다. 08시 이후 다시 보내주세요. 긴급한 경우 @태그하면 처리됩니다.'; '사용량 정렬 시간(04~08 KST)입니다. 08시 이후 다시 보내주세요. 긴급한 경우 @태그하면 처리됩니다.';
const CODEX_ALIGNMENT_AUTO_REPLY = (nowMs: number) =>
`Codex 사용량 정렬 대기 중입니다. 다음 프라이머 시각(${formatPrimerHourKST(
nowMs,
)}) 이후 다시 보내주세요. 긴급한 경우 @태그하면 처리됩니다.`;
export async function handleQueuedRunGates(args: { export async function handleQueuedRunGates(args: {
chatJid: string; chatJid: string;
group: RegisteredGroup; group: RegisteredGroup;
@@ -176,29 +126,6 @@ export async function handleQueuedRunGates(args: {
} }
const agentType: AgentType = args.group.agentType ?? 'claude-code'; 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 ── // ── Usage-exhaustion gate ──
// Block new turns when the agent provider that would handle this room has // Block new turns when the agent provider that would handle this room has