/** * 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(), }; }