feat: global failover + dashboard model config section

- Failover is now global (account-level, not per-channel)
- Dashboard shows role model config (Owner/Reviewer/Arbiter + MoA refs)
- Dashboard shows failover status when active
- Auto-clear failover on successful Claude rotation
- Remove per-channel lease writes from failover path
This commit is contained in:
Eyejoker
2026-03-31 00:38:29 +09:00
parent f98dd27712
commit 68901e5da9
7 changed files with 152 additions and 77 deletions

View File

@@ -16,6 +16,7 @@ import {
setChannelOwnerLease,
type ChannelOwnerLeaseRow,
} from './db.js';
import { logger } from './logger.js';
export interface EffectiveChannelLease {
chat_jid: string;
@@ -122,6 +123,18 @@ export function refreshChannelOwnerCache(force = false): void {
export function getEffectiveChannelLease(
chatJid: string,
): EffectiveChannelLease {
// Global failover overrides all per-channel leases
if (globalFailoverActive) {
return {
chat_jid: chatJid,
owner_service_id: CODEX_REVIEW_SERVICE_ID,
reviewer_service_id: CODEX_MAIN_SERVICE_ID,
arbiter_service_id: null,
activated_at: globalFailoverActivatedAt,
reason: globalFailoverReason,
explicit: true,
};
}
refreshChannelOwnerCache();
const row = leaseCache.get(chatJid);
if (row) {
@@ -167,19 +180,48 @@ export function shouldServiceProcessChat(
return true;
}
export function activateCodexFailover(chatJid: string, reason: string): void {
const now = new Date().toISOString();
const row: ChannelOwnerLeaseRow = {
chat_jid: chatJid,
owner_service_id: CODEX_REVIEW_SERVICE_ID,
reviewer_service_id: CODEX_MAIN_SERVICE_ID,
arbiter_service_id: null,
activated_at: now,
reason,
// ── Global failover ──────────────────────────────────────────────
// Claude API limits are account-level, so failover applies to all channels.
let globalFailoverActive = false;
let globalFailoverReason: string | null = null;
let globalFailoverActivatedAt: string | null = null;
export function activateCodexFailover(
_chatJid: string,
reason: string,
): void {
globalFailoverActive = true;
globalFailoverReason = reason;
globalFailoverActivatedAt = new Date().toISOString();
logger.warn(
{ reason, activatedAt: globalFailoverActivatedAt },
'Global failover activated — all channels switching to codex',
);
}
export function isGlobalFailoverActive(): boolean {
return globalFailoverActive;
}
export function getGlobalFailoverInfo(): {
active: boolean;
reason: string | null;
activatedAt: string | null;
} {
return {
active: globalFailoverActive,
reason: globalFailoverReason,
activatedAt: globalFailoverActivatedAt,
};
setChannelOwnerLease(row);
leaseCache.set(chatJid, row);
lastLeaseRefreshAt = Date.now();
}
export function clearGlobalFailover(): void {
if (!globalFailoverActive) return;
globalFailoverActive = false;
globalFailoverReason = null;
globalFailoverActivatedAt = null;
logger.info('Global failover cleared — resuming normal routing');
}
export function restoreDefaultChannelLease(chatJid: string): void {
@@ -194,18 +236,13 @@ export interface ActiveFailoverLease {
}
export function getActiveCodexFailoverLeases(): ActiveFailoverLease[] {
refreshChannelOwnerCache(true);
return [...leaseCache.values()]
.filter(
(row) =>
normalizeServiceId(row.owner_service_id) === CODEX_REVIEW_SERVICE_ID &&
normalizeServiceId(row.reviewer_service_id || '') ===
CODEX_MAIN_SERVICE_ID,
)
.map((row) => ({
chatJid: row.chat_jid,
activatedAt: row.activated_at ?? null,
}));
// Global failover: report as a single pseudo-lease
if (globalFailoverActive) {
return [
{ chatJid: '*', activatedAt: globalFailoverActivatedAt },
];
}
return [];
}
/** @deprecated Use getActiveCodexFailoverLeases() instead */