UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

491 lines 19.1 kB
/** * Circuit Breaker Manager for AI-Debug Tools * * Implements circuit breaker pattern to handle failures gracefully and provide * reset capabilities for AI-Debug tools that get stuck in failure states. * * Features: * - Per-tool circuit breakers with configurable thresholds * - Automatic failure detection and circuit opening * - Manual and automatic reset capabilities * - Health monitoring and recovery strategies * - Graceful degradation and fallback mechanisms */ import { UserFriendlyLogger } from './user-friendly-logger.js'; import { PhoenixCircuitBreakerConfig } from './phoenix-circuit-breaker-config.js'; export class CircuitBreakerManager { logger; circuitBreakers = new Map(); configs = new Map(); events = []; healthMetrics = new Map(); monitoringInterval = null; constructor() { this.logger = new UserFriendlyLogger('CircuitBreaker'); this.initializeDefaultConfigs(); this.startHealthMonitoring(); } /** * Initialize default circuit breaker configurations for AI-Debug tools */ initializeDefaultConfigs() { const defaultConfigs = { // Core debugging tools 'inject_debugging': { failureThreshold: 3, recoveryTimeoutMs: 30000, // 30 seconds halfOpenMaxAttempts: 2, successThreshold: 2, monitoringWindowMs: 300000 // 5 minutes }, 'monitor_realtime': { failureThreshold: 5, recoveryTimeoutMs: 15000, halfOpenMaxAttempts: 3, successThreshold: 3, monitoringWindowMs: 180000 }, 'simulate_user_action': { failureThreshold: 4, recoveryTimeoutMs: 20000, halfOpenMaxAttempts: 2, successThreshold: 2, monitoringWindowMs: 240000 }, 'take_screenshot': { failureThreshold: 3, recoveryTimeoutMs: 10000, halfOpenMaxAttempts: 1, successThreshold: 1, monitoringWindowMs: 120000 }, 'run_audit': { failureThreshold: 2, recoveryTimeoutMs: 45000, // Audits take longer halfOpenMaxAttempts: 1, successThreshold: 1, monitoringWindowMs: 600000 // 10 minutes }, // Framework-specific tools 'flutter_diagnostics': { failureThreshold: 4, recoveryTimeoutMs: 25000, halfOpenMaxAttempts: 2, successThreshold: 2, monitoringWindowMs: 300000 }, 'nextjs_bundle_analyze': { failureThreshold: 2, recoveryTimeoutMs: 60000, // Bundle analysis is slow halfOpenMaxAttempts: 1, successThreshold: 1, monitoringWindowMs: 900000 // 15 minutes }, 'phoenix_live_dashboard': { failureThreshold: 3, recoveryTimeoutMs: 20000, halfOpenMaxAttempts: 2, successThreshold: 2, monitoringWindowMs: 300000 }, // Backend tools 'debug_liveview_connection': { failureThreshold: 5, recoveryTimeoutMs: 15000, halfOpenMaxAttempts: 3, successThreshold: 2, monitoringWindowMs: 180000 }, 'ecto_trace_queries': { failureThreshold: 3, recoveryTimeoutMs: 30000, halfOpenMaxAttempts: 2, successThreshold: 2, monitoringWindowMs: 300000 } }; for (const [toolName, config] of Object.entries(defaultConfigs)) { this.configs.set(toolName, config); this.initializeCircuitBreaker(toolName); } } /** * Initialize circuit breaker state for a tool */ initializeCircuitBreaker(toolName) { const state = { status: 'closed', failureCount: 0, successCount: 0, lastFailureTime: 0, lastSuccessTime: 0, openedAt: 0, nextAttemptTime: 0, totalAttempts: 0, recentFailures: [] }; this.circuitBreakers.set(toolName, state); // Ensure config exists before updating health metrics if (!this.configs.has(toolName)) { this.configs.set(toolName, { failureThreshold: 3, recoveryTimeoutMs: 30000, halfOpenMaxAttempts: 2, successThreshold: 2, monitoringWindowMs: 300000 }); } this.updateHealthMetrics(toolName); } /** * Record a successful execution */ recordSuccess(toolName, responseTime = 0, context = '') { const state = this.getOrCreateCircuitBreaker(toolName); const now = Date.now(); state.successCount++; state.lastSuccessTime = now; state.totalAttempts++; // Handle state transitions if (state.status === 'half-open') { const config = this.configs.get(toolName); if (state.successCount >= config.successThreshold) { this.closeCircuit(toolName, 'Success threshold reached in half-open state'); } } else if (state.status === 'open') { // Shouldn't happen, but handle gracefully this.logger.warn(`Success recorded for open circuit: ${toolName}`); } this.recordEvent(toolName, 'success', context, { responseTime }); this.updateHealthMetrics(toolName); } /** * Record a failed execution */ recordFailure(toolName, error, context = '') { const state = this.getOrCreateCircuitBreaker(toolName); const config = this.configs.get(toolName); const now = Date.now(); // Check if this error should be counted (Phoenix-specific filtering) const errorObj = new Error(error); if (config.customSettings && !PhoenixCircuitBreakerConfig.shouldCountError(errorObj, config)) { this.logger.info(`🔮 Ignoring Phoenix-expected error for ${toolName}: ${error}`); this.recordEvent(toolName, 'failure', context, { error, ignored: true }); return; } state.failureCount++; state.lastFailureTime = now; state.totalAttempts++; // Add to recent failures (keep last 10) state.recentFailures.push({ timestamp: now, error: error.substring(0, 200), // Truncate long errors context }); if (state.recentFailures.length > 10) { state.recentFailures = state.recentFailures.slice(-10); } // Check if we should open the circuit const recentFailureCount = this.getRecentFailureCount(toolName); if (state.status === 'closed' && recentFailureCount >= config.failureThreshold) { this.openCircuit(toolName, `Failure threshold exceeded: ${recentFailureCount}/${config.failureThreshold}`); } else if (state.status === 'half-open') { this.openCircuit(toolName, 'Failure in half-open state'); } this.recordEvent(toolName, 'failure', context, { error }); this.updateHealthMetrics(toolName); } /** * Check if a tool can be executed (circuit is not open) */ canExecute(toolName) { const state = this.getOrCreateCircuitBreaker(toolName); const now = Date.now(); switch (state.status) { case 'closed': return { allowed: true }; case 'open': if (now >= state.nextAttemptTime) { this.setHalfOpen(toolName, 'Recovery timeout elapsed'); return { allowed: true }; } return { allowed: false, reason: `Circuit open until ${new Date(state.nextAttemptTime).toLocaleTimeString()}` }; case 'half-open': const config = this.configs.get(toolName); if (state.successCount < config.halfOpenMaxAttempts) { return { allowed: true }; } return { allowed: false, reason: 'Half-open state max attempts reached' }; default: return { allowed: false, reason: 'Unknown circuit state' }; } } /** * Manually reset a circuit breaker */ resetCircuit(toolName, reason = 'Manual reset') { const state = this.circuitBreakers.get(toolName); if (!state) { this.logger.warn(`Cannot reset unknown circuit breaker: ${toolName}`); return false; } this.logger.info(`🔄 Resetting circuit breaker for ${toolName}: ${reason}`); // Reset state state.status = 'closed'; state.failureCount = 0; state.successCount = 0; state.openedAt = 0; state.nextAttemptTime = 0; state.recentFailures = []; this.recordEvent(toolName, 'reset', reason); this.updateHealthMetrics(toolName); return true; } /** * Reset all circuit breakers (emergency recovery) */ resetAllCircuits(reason = 'Emergency reset') { let resetCount = 0; for (const toolName of this.circuitBreakers.keys()) { if (this.resetCircuit(toolName, reason)) { resetCount++; } } this.logger.info(`🚨 Emergency reset: ${resetCount} circuit breakers reset`); return resetCount; } /** * Get health status for a specific tool */ getToolHealth(toolName) { return this.healthMetrics.get(toolName) || null; } /** * Get health status for all tools */ getAllToolHealth() { return new Map(this.healthMetrics); } /** * Get tools that are currently failing or degraded */ getUnhealthyTools() { return Array.from(this.healthMetrics.values()) .filter(metrics => metrics.healthScore < 70 || metrics.state.status !== 'closed') .sort((a, b) => a.healthScore - b.healthScore); } /** * Get circuit breaker statistics */ getStatistics() { const tools = Array.from(this.healthMetrics.values()); const healthy = tools.filter(t => t.healthScore >= 80 && t.state.status === 'closed').length; const degraded = tools.filter(t => t.healthScore >= 50 && t.healthScore < 80).length; const failed = tools.filter(t => t.healthScore < 50 || t.state.status === 'open').length; return { totalTools: tools.length, healthyTools: healthy, degradedTools: degraded, failedTools: failed, totalEvents: this.events.length, recentEvents: this.events.slice(-20) }; } /** * Update configuration for a specific tool */ updateConfig(toolName, config) { const currentConfig = this.configs.get(toolName); if (!currentConfig) { this.logger.warn(`Cannot update config for unknown tool: ${toolName}`); return; } const newConfig = { ...currentConfig, ...config }; this.configs.set(toolName, newConfig); this.logger.info(`🔧 Updated circuit breaker config for ${toolName}`); } /** * Update circuit breaker configuration for a specific tool */ updateCircuitBreakerConfig(toolName, config) { this.configs.set(toolName, config); // Re-initialize the circuit breaker state if it exists if (this.circuitBreakers.has(toolName)) { const currentState = this.circuitBreakers.get(toolName); // Preserve current state but update with new config timing currentState.nextAttemptTime = currentState.openedAt > 0 ? currentState.openedAt + config.recoveryTimeoutMs : 0; } } // Private helper methods getOrCreateCircuitBreaker(toolName) { let state = this.circuitBreakers.get(toolName); if (!state) { this.initializeCircuitBreaker(toolName); state = this.circuitBreakers.get(toolName); } return state; } /** * Apply Phoenix-specific circuit breaker configuration if appropriate */ applyPhoenixConfiguration(context) { if (PhoenixCircuitBreakerConfig.shouldUsePhoenixConfig(context)) { this.logger.info('🔮 Applying Phoenix-optimized circuit breaker settings'); const phoenixConfig = PhoenixCircuitBreakerConfig.getPhoenixConfig(); // Update all existing circuit breaker configs for (const [toolName, _] of this.configs) { this.updateCircuitBreakerConfig(toolName, phoenixConfig); } } } openCircuit(toolName, reason) { const state = this.circuitBreakers.get(toolName); const config = this.configs.get(toolName); const now = Date.now(); state.status = 'open'; state.openedAt = now; state.nextAttemptTime = now + config.recoveryTimeoutMs; state.successCount = 0; // Reset success count this.logger.warn(`🔴 Circuit OPENED for ${toolName}: ${reason}`); this.recordEvent(toolName, 'open', reason); } closeCircuit(toolName, reason) { const state = this.circuitBreakers.get(toolName); state.status = 'closed'; state.failureCount = 0; // Reset failure count state.successCount = 0; // Reset success count state.openedAt = 0; state.nextAttemptTime = 0; this.logger.success(`🟢 Circuit CLOSED for ${toolName}: ${reason}`); this.recordEvent(toolName, 'close', reason); } setHalfOpen(toolName, reason) { const state = this.circuitBreakers.get(toolName); state.status = 'half-open'; state.successCount = 0; // Reset for half-open tracking this.logger.info(`🟡 Circuit HALF-OPEN for ${toolName}: ${reason}`); this.recordEvent(toolName, 'half-open', reason); } getRecentFailureCount(toolName) { const state = this.circuitBreakers.get(toolName); const config = this.configs.get(toolName); const cutoff = Date.now() - config.monitoringWindowMs; return state.recentFailures.filter(f => f.timestamp > cutoff).length; } recordEvent(toolName, event, context, metadata) { this.events.push({ timestamp: Date.now(), toolName, event, context, metadata }); // Keep only recent events (last 1000) if (this.events.length > 1000) { this.events = this.events.slice(-500); } } updateHealthMetrics(toolName) { const state = this.circuitBreakers.get(toolName); const config = this.configs.get(toolName); const now = Date.now(); // Calculate health score (0-100) let healthScore = 100; // Penalize based on circuit state if (state.status === 'open') { healthScore = 0; } else if (state.status === 'half-open') { healthScore = 30; } else { // Calculate based on recent failure rate const recentFailures = this.getRecentFailureCount(toolName); const failureRate = recentFailures / config.failureThreshold; healthScore = Math.max(0, 100 - (failureRate * 70)); } // Calculate availability rate const totalRecent = state.recentFailures.length + state.successCount; const availabilityRate = totalRecent > 0 ? (state.successCount / totalRecent) * 100 : 100; // Generate recommendations const recommendations = this.generateRecommendations(toolName, state); this.healthMetrics.set(toolName, { toolName, state: { ...state }, // Clone state healthScore, availabilityRate, averageResponseTime: 0, // Would be calculated from metrics lastHealthCheck: now, recommendations }); } generateRecommendations(toolName, state) { const recommendations = []; if (state.status === 'open') { recommendations.push('🔴 Tool is currently unavailable due to repeated failures'); recommendations.push(`⏰ Next retry available at ${new Date(state.nextAttemptTime).toLocaleTimeString()}`); recommendations.push('🔧 Consider manual reset if underlying issue is resolved'); } else if (state.status === 'half-open') { recommendations.push('🟡 Tool is in recovery mode - use cautiously'); recommendations.push('📊 Monitor next few executions closely'); } if (state.recentFailures.length > 0) { const recentError = state.recentFailures[state.recentFailures.length - 1]; recommendations.push(`🐛 Recent error: ${recentError.error}`); // Tool-specific recommendations if (toolName.includes('flutter') && recentError.error.includes('Flutter')) { recommendations.push('🦋 Check Flutter Web build and dependencies'); } else if (toolName.includes('phoenix') && recentError.error.includes('connection')) { recommendations.push('🔥 Verify Phoenix server is running and accessible'); } else if (toolName.includes('browser') || recentError.error.includes('browser')) { recommendations.push('🌐 Check browser installation and permissions'); } } return recommendations; } startHealthMonitoring() { this.monitoringInterval = setInterval(() => { this.performHealthCheck(); }, 60000); // Check every minute } performHealthCheck() { const now = Date.now(); let degradedCount = 0; let failedCount = 0; for (const [toolName, metrics] of this.healthMetrics) { if (metrics.healthScore < 50) { failedCount++; } else if (metrics.healthScore < 80) { degradedCount++; } } if (failedCount > 0 || degradedCount > 3) { this.logger.warn(`⚠️ Health check: ${failedCount} failed, ${degradedCount} degraded tools`); } } /** * Cleanup resources */ destroy() { if (this.monitoringInterval) { clearInterval(this.monitoringInterval); this.monitoringInterval = null; } } } //# sourceMappingURL=circuit-breaker-manager.js.map