backup current stable ejclaw state

This commit is contained in:
Codex
2026-05-27 10:53:05 +09:00
parent 646bc34372
commit 1509108e04
57 changed files with 7127 additions and 154 deletions

View File

@@ -1,9 +1,80 @@
import {
CLAUDE_EXHAUSTION_THRESHOLD_PCT,
getClaudeUsageExhaustion,
} from './claude-usage.js';
import {
CODEX_EXHAUSTION_THRESHOLD_PCT,
getCodexUsageExhaustion,
} from './codex-usage-collector.js';
import { logger } from './logger.js';
import {
handleSessionCommand,
type SessionCommandDeps,
} from './session-commands.js';
import type { NewMessage, RegisteredGroup } from './types.js';
import type { AgentType, NewMessage, RegisteredGroup } from './types.js';
/**
* Test seam — overridable from unit tests so we don't need a real disk
* cache or rotation state to exercise the exhaustion gate.
*/
export const exhaustionChecks = {
claude: getClaudeUsageExhaustion,
codex: getCodexUsageExhaustion,
};
/** Render an ISO timestamp as "약 N분 뒤" / "약 Nh Nm 뒤" relative to now. */
function formatResetCountdown(iso: string | null): string | null {
if (!iso) return null;
const target = Date.parse(iso);
if (!Number.isFinite(target)) return null;
const diffMs = target - Date.now();
if (diffMs <= 0) return '곧';
const totalMinutes = Math.ceil(diffMs / 60_000);
if (totalMinutes < 60) return `${totalMinutes}분 뒤`;
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
if (hours < 24) {
return minutes > 0
? `${hours}시간 ${minutes}분 뒤`
: `${hours}시간 뒤`;
}
const days = Math.floor(hours / 24);
const remH = hours % 24;
return remH > 0 ? `${days}${remH}시간 뒤` : `${days}일 뒤`;
}
function exhaustionMessageFor(
agentType: AgentType,
nextResetAt: string | null,
): string {
const label = agentType === 'codex' ? 'Codex' : 'Claude';
const pct =
agentType === 'codex'
? CODEX_EXHAUSTION_THRESHOLD_PCT
: CLAUDE_EXHAUSTION_THRESHOLD_PCT;
const countdown = formatResetCountdown(nextResetAt);
const head = `토큰 부족으로 종료됨 (${label} 모든 계정 사용량 ${pct}% 초과)`;
return countdown ? `${head}${countdown} 초기화 예정` : head;
}
// ── Usage-window alignment gap ──
// Bot primer fires at 08/13/18/23 KST to anchor 5h windows. The 04:0008:00
// KST gap before the day's first primer must be free of inference calls,
// otherwise an off-cycle window starts and the daily reset alignment drifts.
// During the gap, send an auto-reply instead of running the model.
// Urgent bypass: any message containing an @-mention (bot's trigger pattern)
// is processed anyway — user explicitly tagged the bot, accept the drift.
const GAP_START_HOUR_KST = 4;
const GAP_END_HOUR_KST = 8;
const KST_OFFSET_MS = 9 * 60 * 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;
}
const GAP_AUTO_REPLY =
'사용량 정렬 시간(04~08 KST)입니다. 08시 이후 다시 보내주세요. 긴급한 경우 @태그하면 처리됩니다.';
export async function handleQueuedRunGates(args: {
chatJid: string;
@@ -31,5 +102,57 @@ export async function handleQueuedRunGates(args: {
return cmdResult;
}
// ── Usage-window alignment gap gate ──
if (isInUsageAlignmentGap()) {
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 turn — usage alignment gap (04-08 KST)',
);
try {
await args.sessionCommandDeps.sendMessage(GAP_AUTO_REPLY);
} catch (err) {
logger.warn({ err }, 'Failed to send alignment gap 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()
: exhaustionChecks.claude();
if (info.exhausted) {
const text = exhaustionMessageFor(agentType, info.nextResetAt);
logger.warn(
{
chatJid: args.chatJid,
groupName: args.group.name,
agentType,
runId: args.runId,
nextResetAt: info.nextResetAt,
},
'Blocking new turn — all accounts exhausted',
);
try {
await args.sessionCommandDeps.sendMessage(text);
} catch (err) {
logger.warn({ err }, 'Failed to send exhaustion notice');
}
return { handled: true, success: true };
}
return { handled: false };
}