fix(primer): refresh Codex usage before primer and allow 1% fresh accounts
Codex primer was skipping with no_eligible_accounts because it read a stale usage cache and required exactly 0% usage. Re-query usage right before the primer call and treat freshly-reset 0~1% accounts as eligible so the 5h window can be anchored at the fixed KST slot. Also hold new Codex-owner turns at fresh usage until the next primer slot. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -66,16 +67,65 @@ 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;
|
||||
@@ -125,11 +175,35 @@ 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
|
||||
// every account at ≥95% on either window (or rate-limited). In-flight
|
||||
// runs are not affected; this only stops *new* turns from starting.
|
||||
const agentType: AgentType = args.group.agentType ?? 'claude-code';
|
||||
const info =
|
||||
agentType === 'codex'
|
||||
? exhaustionChecks.codex()
|
||||
|
||||
Reference in New Issue
Block a user