zelin-bot / src /runtime-healer.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* runtime-healer.js β€” Supervisor Auto-Curativo del Proceso
* ===========================================================
* Basado en: VIGIL framework (2025) β€” "reflective runtime that supervises
* a sibling agent and performs autonomous maintenance"
*
* FUNCIONES:
* 1. Capturar excepciones no manejadas y diagnosticarlas con IA
* 2. Detectar degradaciΓ³n de rendimiento (lentitud, alta RAM)
* 3. Proponer fixes al owner (NUNCA auto-aplicar cΓ³digo)
* 4. Reiniciar mΓ³dulos especΓ­ficos sin matar el proceso
* 5. Log estructurado de todos los eventos para auditorΓ­a
*
* LÍMITE CRÍTICO (de la investigación):
* "Self-correcting agents without hard limits are just very determined
* systems that can make the same bad decision many times in a row"
* β†’ El healer NUNCA escribe cΓ³digo directamente. Solo diagnostica y sugiere.
*/
import { callAIBackground } from './ai.js';
import * as db from './db.js';
import { readConfig } from './utils.js';
const config = readConfig();
// ── Estado del healer ─────────────────────────────────────────────────────────
const _state = {
errors : [], // ΓΊltimos 50 errores
healingAttempts : 0,
lastHealed : null,
degraded : false,
client : null,
};
const MAX_ERRORS_STORED = 50;
const MAX_HEALING_PER_HOUR = 5; // cap por hora para evitar loops
let _healingThisHour = 0;
let _hourReset = Date.now();
function canHeal() {
const now = Date.now();
if (now - _hourReset > 3_600_000) { _healingThisHour = 0; _hourReset = now; }
return _healingThisHour < MAX_HEALING_PER_HOUR;
}
// ── Registrar error ───────────────────────────────────────────────────────────
function recordError(type, message, stack = '', context = '') {
const entry = {
type, message: message.slice(0, 500), stack: stack.slice(0, 1000),
context, ts: Date.now(),
};
_state.errors.push(entry);
if (_state.errors.length > MAX_ERRORS_STORED) _state.errors.shift();
// Persistir en DB para diagnΓ³stico posterior
db.memSet(`runtime.error.${Date.now()}`, entry, 'runtime_errors').catch(() => {});
return entry;
}
// ── DiagnΓ³stico con IA ────────────────────────────────────────────────────────
async function diagnoseWithAI(error) {
if (!canHeal()) {
console.warn('[Healer] Cap de healing/hora alcanzado β€” no diagnosticando');
return null;
}
_healingThisHour++;
// Incluir los ΓΊltimos 3 errores para contexto (pattern detection)
const recentErrors = _state.errors.slice(-3).map(e =>
`[${new Date(e.ts).toISOString()}] ${e.type}: ${e.message}`
).join('\n');
try {
const raw = await callAIBackground([
{
role : 'system',
content: `Eres un experto en Node.js y Discord.js diagnosticando errores de Zelin, un bot de Discord.
Analiza el error y propón una solución ESPECÍFICA y SEGURA.
Reglas:
- NO sugieras reiniciar el servidor completo como primera opciΓ³n
- SÍ sugiere qué módulo específico podría recargarse
- SÍ identifica si es un error transitorio (red, API) o estructural (bug en código)
- SÍ indica si hay un pattern de errores repetidos
Responde SOLO JSON:
{
"type": "transient|structural|resource|unknown",
"root_cause": "causa raΓ­z en 1 frase",
"severity": "low|medium|high|critical",
"is_pattern": true/false,
"safe_actions": ["acciΓ³n 1 que Zelin puede hacer sola sin riesgo"],
"owner_suggestion": "quΓ© deberΓ­a hacer tomatitoo manualmente (si aplica)",
"auto_recoverable": true/false
}`,
},
{
role : 'user',
content: `Error actual:\n${error.type}: ${error.message}\n${error.stack ? `Stack:\n${error.stack}` : ''}\n\nErrores recientes:\n${recentErrors}`,
},
], 'reasoning', 400);
return JSON.parse(raw.replace(/```json|```/g, '').trim());
} catch {
return null;
}
}
// ── Acciones de recuperaciΓ³n automΓ‘tica (solo las seguras) ────────────────────
async function attemptAutoRecovery(diagnosis) {
if (!diagnosis?.auto_recoverable) return false;
for (const action of diagnosis.safe_actions ?? []) {
const actionLower = action.toLowerCase();
if (actionLower.includes('clear cache') || actionLower.includes('limpiar cache')) {
const { clearCache } = await import('./ai.js');
clearCache();
console.log('[Healer] βœ… Cache limpiada automΓ‘ticamente');
} else if (actionLower.includes('gc') || actionLower.includes('garbage')) {
if (global.gc) { global.gc(); console.log('[Healer] βœ… GC ejecutado'); }
} else if (actionLower.includes('reintentar') || actionLower.includes('wait')) {
// Errores transitorios de red β€” simplemente esperar
await new Promise(r => setTimeout(r, 5000));
console.log('[Healer] βœ… Esperado 5s para recuperaciΓ³n de error transitorio');
}
}
return true;
}
// ── Notificar al owner ────────────────────────────────────────────────────────
async function notifyOwner(error, diagnosis) {
if (!_state.client) return;
try {
const owner = await _state.client.users.fetch(config.admin.userId);
const dm = await owner.createDM();
const severity = diagnosis?.severity ?? 'unknown';
const icon = { low: '🟑', medium: '🟠', high: 'πŸ”΄', critical: 'πŸ’€' }[severity] ?? 'βšͺ';
const msg = [
`${icon} **Error en Zelin** [${severity.toUpperCase()}]`,
`**Tipo:** ${error.type}`,
`**Causa raΓ­z:** ${diagnosis?.root_cause ?? error.message.slice(0, 200)}`,
diagnosis?.is_pattern ? `⚠️ **PatrΓ³n detectado** β€” este error se repite` : '',
diagnosis?.owner_suggestion ? `**Sugerencia para ti:** ${diagnosis.owner_suggestion}` : '',
`*Errores en el ΓΊltimo ciclo: ${_state.errors.filter(e => Date.now()-e.ts < 3_600_000).length}*`,
].filter(Boolean).join('\n');
await dm.send(msg.slice(0, 1900));
} catch {}
}
// ── Monitor de rendimiento ────────────────────────────────────────────────────
// NOTA: NO usamos heapUsed/heapTotal β€” esa mΓ©trica es engaΓ±osa en Node.js con
// mΓ³dulos nativos (node-llama-cpp carga modelos fuera del heap V8).
// heapTotal puede ser 200MB mientras RSS es 1.2GB β†’ heapPct = 94% FALSO POSITIVO.
// MΓ©trica correcta: RSS vs RAM total real del sistema.
import { freemem, totalmem } from 'os';
function checkPerformance() {
const mem = process.memoryUsage();
const totalRamMB = totalmem() / 1024 / 1024;
const freeRamMB = freemem() / 1024 / 1024;
const usedRamMB = totalRamMB - freeRamMB;
const rssMB = mem.rss / 1024 / 1024;
const ramPct = usedRamMB / totalRamMB; // % de RAM del SISTEMA (no del heap V8)
// Warning real: RAM del sistema > 88% (eso sΓ­ es preocupante)
const WARN_THRESHOLD = 0.88;
const CRIT_THRESHOLD = 0.94;
if (ramPct > CRIT_THRESHOLD) {
console.warn(`[Healer] πŸ”΄ RAM crΓ­tica: ${usedRamMB.toFixed(0)}MB / ${totalRamMB.toFixed(0)}MB (${(ramPct*100).toFixed(0)}%)`);
_state.degraded = true;
if (global.gc) { global.gc(); console.log('[Healer] GC ejecutado'); }
} else if (ramPct > WARN_THRESHOLD) {
console.warn(`[Healer] ⚠️ RAM alta: ${usedRamMB.toFixed(0)}MB / ${totalRamMB.toFixed(0)}MB (${(ramPct*100).toFixed(0)}%)`);
_state.degraded = false; // warning pero no degraded
} else {
_state.degraded = false;
}
return {
heapUsedMB : (mem.heapUsed / 1024 / 1024).toFixed(1),
heapTotalMB : (mem.heapTotal / 1024 / 1024).toFixed(1),
rssMB : rssMB.toFixed(1),
ramUsedMB : usedRamMB.toFixed(1),
ramTotalMB : totalRamMB.toFixed(1),
ramPct : (ramPct * 100).toFixed(0) + '%',
uptime : Math.round(process.uptime()) + 's',
degraded : _state.degraded,
};
}
// ── Inicializar el healer ─────────────────────────────────────────────────────
export function initRuntimeHealer(client) {
_state.client = client;
// 1. Capturar excepciones no manejadas
process.on('uncaughtException', async (err) => {
// NUNCA dejar que el proceso muera silenciosamente
const recorded = recordError('uncaughtException', err.message, err.stack);
console.error('[Healer] 🚨 uncaughtException:', err.message);
const diagnosis = await diagnoseWithAI(recorded);
if (diagnosis) {
await attemptAutoRecovery(diagnosis);
// Solo notificar al owner si es medium o peor
if (['medium','high','critical'].includes(diagnosis.severity)) {
await notifyOwner(recorded, diagnosis);
}
}
// NO hacer process.exit() β€” intentar seguir corriendo
});
// 2. Capturar promesas rechazadas no manejadas
process.on('unhandledRejection', async (reason) => {
const msg = reason instanceof Error ? reason.message : String(reason);
const stack = reason instanceof Error ? reason.stack : '';
const recorded = recordError('unhandledRejection', msg, stack);
console.error('[Healer] 🚨 unhandledRejection:', msg.slice(0, 200));
// Solo diagnosticar si es un error frecuente (3+ en 5 minutos)
const recentSimilar = _state.errors.filter(e =>
e.type === 'unhandledRejection' &&
Date.now() - e.ts < 5 * 60_000 &&
e.message.slice(0, 50) === msg.slice(0, 50)
).length;
if (recentSimilar >= 3) {
const diagnosis = await diagnoseWithAI(recorded);
if (diagnosis) {
await attemptAutoRecovery(diagnosis);
await notifyOwner(recorded, diagnosis);
}
}
});
// 3. Monitor de rendimiento cada 5 minutos
setInterval(() => {
const perf = checkPerformance();
if (_state.degraded) {
console.warn(`[Healer] ⚠️ Rendimiento degradado: ${perf.ramUsedMB}MB / ${perf.ramTotalMB}MB RAM sistema (${perf.ramPct}), RSS proceso: ${perf.rssMB}MB`);
}
}, 5 * 60_000);
console.log('[Healer] βœ… Runtime healer activo β€” monitoreando proceso');
}
export function getHealerStats() {
return {
errors_stored : _state.errors.length,
errors_last_hour : _state.errors.filter(e => Date.now()-e.ts < 3_600_000).length,
healing_this_hour : _healingThisHour,
healing_cap : MAX_HEALING_PER_HOUR,
degraded : _state.degraded,
performance : checkPerformance(),
};
}