backup current stable ejclaw state
This commit is contained in:
179
src/auto-paired-mode.ts
Normal file
179
src/auto-paired-mode.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Auto paired-mode degrade/restore.
|
||||
*
|
||||
* When all configured Claude accounts are at/over the 95% exhaustion threshold,
|
||||
* paired (tribunal) rooms cannot meaningfully run their reviewer/arbiter
|
||||
* because the gate blocks Claude turns. To keep the user-facing rooms
|
||||
* responsive, we temporarily flip selected rooms to `single` mode and notify
|
||||
* each affected channel. When usage recovers we flip them back to `tribunal`.
|
||||
*
|
||||
* Selection (Option B agreed with the user):
|
||||
* - The most-recently-active room (per chats.last_message_time)
|
||||
* - All rooms that are currently in-flight (queue status === 'processing')
|
||||
* - Deduplicated; rooms whose owner is `claude-code` are skipped because
|
||||
* degrading them does not help — the 95% gate blocks them either way.
|
||||
*
|
||||
* Restore behavior:
|
||||
* - Only restore a room if it is still in 'single' (don't override a manual
|
||||
* user change made during the degrade window).
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { getClaudeUsageExhaustion } from './claude-usage.js';
|
||||
import { DATA_DIR } from './config.js';
|
||||
import {
|
||||
getAllChats,
|
||||
getAllRoomBindings,
|
||||
getEffectiveRoomMode,
|
||||
getStoredRoomSettings,
|
||||
setExplicitRoomMode,
|
||||
} from './db.js';
|
||||
import { logger } from './logger.js';
|
||||
import { readJsonFile, writeJsonFile } from './utils.js';
|
||||
|
||||
const STATE_PATH = path.join(DATA_DIR, 'auto-paired-mode-state.json');
|
||||
|
||||
const DEGRADE_NOTICE =
|
||||
'클로드 사용량이 95%를 넘어 잠시 single 모드로 전환했어요. 사용량이 회복되면 paired 모드로 다시 켤게요.';
|
||||
const RESTORE_NOTICE = '클로드 사용량이 회복돼서 paired 모드로 다시 켰어요.';
|
||||
|
||||
interface DowngradeRecord {
|
||||
chatJid: string;
|
||||
atIso: string;
|
||||
}
|
||||
|
||||
interface AutoPairedModeState {
|
||||
exhausted: boolean;
|
||||
downgraded: DowngradeRecord[];
|
||||
}
|
||||
|
||||
export interface AutoPairedModeDeps {
|
||||
queue?: {
|
||||
getStatuses(jids: string[]): Array<{ jid: string; status: string }>;
|
||||
};
|
||||
sendNotice: (chatJid: string, text: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function loadState(): AutoPairedModeState {
|
||||
const raw = readJsonFile<AutoPairedModeState>(STATE_PATH);
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return { exhausted: false, downgraded: [] };
|
||||
}
|
||||
return {
|
||||
exhausted: Boolean(raw.exhausted),
|
||||
downgraded: Array.isArray(raw.downgraded) ? raw.downgraded : [],
|
||||
};
|
||||
}
|
||||
|
||||
function saveState(state: AutoPairedModeState): void {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
|
||||
writeJsonFile(STATE_PATH, state);
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'auto-paired-mode: failed to persist state');
|
||||
}
|
||||
}
|
||||
|
||||
function pickCandidates(deps: AutoPairedModeDeps): string[] {
|
||||
const bindings = getAllRoomBindings();
|
||||
const allJids = Object.keys(bindings);
|
||||
if (allJids.length === 0) return [];
|
||||
|
||||
const candidates = new Set<string>();
|
||||
|
||||
// Most-recently-active room first (chats ordered DESC by last_message_time).
|
||||
const chats = getAllChats();
|
||||
const recent = chats.find((c) => allJids.includes(c.jid));
|
||||
if (recent) candidates.add(recent.jid);
|
||||
|
||||
// Currently in-flight rooms.
|
||||
if (deps.queue) {
|
||||
for (const status of deps.queue.getStatuses(allJids)) {
|
||||
if (status.status === 'processing') candidates.add(status.jid);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip claude-owned rooms (Option B).
|
||||
const result: string[] = [];
|
||||
for (const jid of candidates) {
|
||||
const settings = getStoredRoomSettings(jid);
|
||||
if (settings?.ownerAgentType === 'claude-code') continue;
|
||||
if (getEffectiveRoomMode(jid) !== 'tribunal') continue;
|
||||
result.push(jid);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function evaluateAndApplyAutoPairedMode(
|
||||
deps: AutoPairedModeDeps,
|
||||
): Promise<void> {
|
||||
const exhaustion = getClaudeUsageExhaustion();
|
||||
const state = loadState();
|
||||
|
||||
if (exhaustion.exhausted && !state.exhausted) {
|
||||
// Transition: healthy → exhausted. Downgrade selected rooms.
|
||||
const targets = pickCandidates(deps);
|
||||
const downgraded: DowngradeRecord[] = [];
|
||||
for (const jid of targets) {
|
||||
try {
|
||||
setExplicitRoomMode(jid, 'single');
|
||||
downgraded.push({ chatJid: jid, atIso: new Date().toISOString() });
|
||||
logger.info(
|
||||
{ jid },
|
||||
'auto-paired-mode: downgraded room to single (claude exhausted)',
|
||||
);
|
||||
try {
|
||||
await deps.sendNotice(jid, DEGRADE_NOTICE);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err, jid },
|
||||
'auto-paired-mode: failed to send degrade notice',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn({ err, jid }, 'auto-paired-mode: failed to downgrade room');
|
||||
}
|
||||
}
|
||||
saveState({ exhausted: true, downgraded });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!exhaustion.exhausted && state.exhausted) {
|
||||
// Transition: exhausted → healthy. Restore previously-downgraded rooms.
|
||||
for (const record of state.downgraded) {
|
||||
try {
|
||||
if (getEffectiveRoomMode(record.chatJid) !== 'single') {
|
||||
// Respect manual user changes during the degrade window.
|
||||
logger.info(
|
||||
{ jid: record.chatJid },
|
||||
'auto-paired-mode: skip restore (room is no longer single)',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
setExplicitRoomMode(record.chatJid, 'tribunal');
|
||||
logger.info(
|
||||
{ jid: record.chatJid },
|
||||
'auto-paired-mode: restored room to tribunal',
|
||||
);
|
||||
try {
|
||||
await deps.sendNotice(record.chatJid, RESTORE_NOTICE);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err, jid: record.chatJid },
|
||||
'auto-paired-mode: failed to send restore notice',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err, jid: record.chatJid },
|
||||
'auto-paired-mode: failed to restore room',
|
||||
);
|
||||
}
|
||||
}
|
||||
saveState({ exhausted: false, downgraded: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Steady state — nothing to do.
|
||||
}
|
||||
Reference in New Issue
Block a user