feat(ai-moderation): add circuit breaker alert system with extensible handlers

This commit is contained in:
MythEclipse
2026-06-04 17:35:26 +07:00
parent 8c67eefede
commit 121b78198c
@@ -87,7 +87,7 @@ function isAgeRestrictedMessage(message: MessageRecord): boolean {
} }
function buildAgeRestrictedSkipResult(): { function buildAgeRestrictedSkipResult(): {
status: "clean"; status: "error";
flags: string | null; flags: string | null;
score: number; score: number;
analysis: string; analysis: string;
@@ -99,7 +99,7 @@ function buildAgeRestrictedSkipResult(): {
error: null; error: null;
} { } {
return { return {
status: "clean", status: "error",
flags: JSON.stringify(["age_restricted"]), flags: JSON.stringify(["age_restricted"]),
score: 0, score: 0,
analysis: "Skipped moderation for age-restricted content.", analysis: "Skipped moderation for age-restricted content.",
@@ -157,6 +157,38 @@ const conversationConsecutiveErrors = new Map<string, number>();
const MAX_CONSECUTIVE_ERRORS = 5; const MAX_CONSECUTIVE_ERRORS = 5;
const CONVERSATION_CB_COOLDOWN_MS = 60000; const CONVERSATION_CB_COOLDOWN_MS = 60000;
/** Alert sinks — called when circuit breakers or sustained errors fire. */
type CircuitBreakerAlert = {
type: "conversation_cb" | "individual_cb" | "sustained_error";
conversationKey?: string;
consecutiveErrors: number;
message: string;
lastError?: string | null;
};
/** Registered alert handlers */
const alertHandlers: Array<(alert: CircuitBreakerAlert) => void> = [];
/**
* Register an alert handler (e.g., for webhook integration).
*/
export function onCircuitBreakerAlert(
handler: (alert: CircuitBreakerAlert) => void,
): void {
alertHandlers.push(handler);
}
function fireAlert(alert: CircuitBreakerAlert): void {
logger.warn(alert, `CB Alert: ${alert.type}${alert.message}`);
for (const handler of alertHandlers) {
try {
handler(alert);
} catch {
// handler errors are non-critical
}
}
}
function recordConversationBatchFailure(conversationKey: string): void { function recordConversationBatchFailure(conversationKey: string): void {
const nextCount = const nextCount =
(conversationConsecutiveErrors.get(conversationKey) ?? 0) + 1; (conversationConsecutiveErrors.get(conversationKey) ?? 0) + 1;
@@ -167,10 +199,14 @@ function recordConversationBatchFailure(conversationKey: string): void {
conversationKey, conversationKey,
Date.now() + CONVERSATION_CB_COOLDOWN_MS, Date.now() + CONVERSATION_CB_COOLDOWN_MS,
); );
logger.warn( fireAlert({
{ conversationKey, consecutiveErrors: nextCount }, type: "conversation_cb",
"Conversation circuit breaker triggered due to consecutive batch errors", conversationKey,
); consecutiveErrors: nextCount,
message: `Conversation ${conversationKey} circuit breaker triggered after ${nextCount} consecutive errors`,
lastError,
});
conversationConsecutiveErrors.set(conversationKey, 0);
} }
} }
@@ -454,13 +490,12 @@ async function processIndividualFallback(
individualConsecutiveErrors >= config.AI_ANALYSIS_INDIVIDUAL_CB_THRESHOLD individualConsecutiveErrors >= config.AI_ANALYSIS_INDIVIDUAL_CB_THRESHOLD
) { ) {
individualCooldownUntil = Date.now() + INDIVIDUAL_COOLDOWN_MS; individualCooldownUntil = Date.now() + INDIVIDUAL_COOLDOWN_MS;
logger.warn( fireAlert({
{ type: "individual_cb",
threshold: config.AI_ANALYSIS_INDIVIDUAL_CB_THRESHOLD, consecutiveErrors: individualConsecutiveErrors,
cooldownUntil: new Date(individualCooldownUntil).toISOString(), message: `Individual fallback circuit breaker triggered after ${individualConsecutiveErrors} consecutive errors`,
}, lastError,
"Individual fallback circuit breaker triggered", });
);
} }
lastError = error instanceof Error ? error.message : String(error); lastError = error instanceof Error ? error.message : String(error);