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
JavaScript
/**
* 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