UNPKG

@calmhive/calmhive-cli

Version:

🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry

396 lines (340 loc) 11.4 kB
/** * Claude API Circuit Breaker Integration * * Demonstrates how to integrate circuit breaker pattern with Claude API calls * and other external dependencies in the Calmhive system. */ const { globalCircuitBreakerManager } = require('./circuit-breaker'); const { APIError, UsageLimitError, NetworkTimeoutError, ProcessError, ProcessSpawnError } = require('../errors/base-error'); class ClaudeAPICircuitBreaker { constructor() { // Create circuit breaker for Claude API calls this.claudeAPI = globalCircuitBreakerManager.getCircuitBreaker('claude-api', { failureThreshold: 3, successThreshold: 2, timeout: 30000, // 30 seconds before retry slowRequestThreshold: 10000, // 10 seconds is considered slow isRetryableError: this.isClaudeRetryableError, onStateChange: this.onClaudeStateChange, onFailure: this.onClaudeFailure }); // Create circuit breaker for process spawning this.processSpawn = globalCircuitBreakerManager.getCircuitBreaker('process-spawn', { failureThreshold: 5, successThreshold: 3, timeout: 60000, // 1 minute before retry isRetryableError: this.isProcessRetryableError, onStateChange: this.onProcessStateChange }); // Create circuit breaker for file system operations this.fileSystem = globalCircuitBreakerManager.getCircuitBreaker('file-system', { failureThreshold: 10, successThreshold: 5, timeout: 10000, // 10 seconds before retry isRetryableError: this.isFileSystemRetryableError }); } /** * Execute Claude API call with circuit breaker protection */ async executeClaudeCommand(command, args, options = {}) { return this.claudeAPI.execute(async () => { console.log(`🚀 Executing Claude command: ${command} ${args.join(' ')}`); // Simulate Claude API call with proper error handling const { spawn } = require('child_process'); return new Promise((resolve, reject) => { const process = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], cwd: options.workingDir || process.cwd(), env: { ...process.env, ...options.env } }); let stdout = ''; let stderr = ''; let hasTimedOut = false; // Set up timeout const timeout = setTimeout(() => { hasTimedOut = true; process.kill('SIGKILL'); reject(new NetworkTimeoutError('claude-api', options.timeout || 30000)); }, options.timeout || 30000); process.stdout.on('data', (data) => { stdout += data.toString(); }); process.stderr.on('data', (data) => { stderr += data.toString(); // Check for usage limit patterns if (this.isUsageLimitError(stderr)) { clearTimeout(timeout); process.kill('SIGTERM'); reject(new UsageLimitError(this.extractResetTime(stderr))); } }); process.on('close', (code) => { clearTimeout(timeout); if (hasTimedOut) {return;} // Already handled by timeout if (code === 0) { resolve({ stdout, stderr, exitCode: code }); } else { // Classify the error based on exit code and stderr const error = this.classifyClaudeError(code, stderr); reject(error); } }); process.on('error', (error) => { clearTimeout(timeout); reject(new ProcessSpawnError(command, args, error.message)); }); }); }); } /** * Execute process spawn with circuit breaker protection */ async executeProcessSpawn(command, args, options = {}) { return this.processSpawn.execute(async () => { const { spawn } = require('child_process'); console.log(`🔧 Spawning process: ${command} ${args.join(' ')}`); return new Promise((resolve, reject) => { const process = spawn(command, args, options); process.on('close', (code) => { if (code === 0) { resolve({ exitCode: code, pid: process.pid }); } else { reject(new ProcessError( `Process ${command} exited with code ${code}`, process.pid, { command, args, exitCode: code } )); } }); process.on('error', (error) => { reject(new ProcessSpawnError(command, args, error.message)); }); }); }); } /** * Execute file system operation with circuit breaker protection */ async executeFileSystemOperation(operation, ...args) { return this.fileSystem.execute(async () => { const fs = require('fs').promises; try { return await fs[operation](...args); } catch (error) { // Wrap in appropriate error type throw new Error(`File system operation '${operation}' failed: ${error.message}`); } }); } /** * Check if Claude API error is retryable */ isClaudeRetryableError(error) { // Usage limits are retryable (with backoff) if (error instanceof UsageLimitError) { return true; } // Network timeouts are retryable if (error instanceof NetworkTimeoutError) { return true; } // API errors with specific codes are retryable if (error instanceof APIError) { return [429, 500, 502, 503, 504].includes(error.context?.statusCode); } // Process spawn failures might be retryable if (error instanceof ProcessSpawnError) { return true; } return false; } /** * Check if process error is retryable */ isProcessRetryableError(error) { if (error instanceof ProcessSpawnError) { // ENOENT is usually not retryable (command not found) return error.context?.cause !== 'ENOENT'; } if (error instanceof ProcessError) { // Exit codes that might be retryable const retryableExitCodes = [1, 130, 143]; // General errors, SIGINT, SIGTERM return retryableExitCodes.includes(error.context?.exitCode); } return false; } /** * Check if file system error is retryable */ isFileSystemRetryableError(error) { // Temporary failures that might resolve const retryableCodes = ['EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY']; return retryableCodes.includes(error.code); } /** * Check if stderr indicates usage limit */ isUsageLimitError(stderr) { const usageLimitPatterns = [ 'usage limit', 'rate limit', 'quota exceeded', 'too many requests', 'limit exceeded' ]; const lowerStderr = stderr.toLowerCase(); return usageLimitPatterns.some(pattern => lowerStderr.includes(pattern)); } /** * Extract reset time from usage limit error */ extractResetTime(stderr) { // Try to extract reset time from error message const resetMatch = stderr.match(/reset in (\d+) (seconds?|minutes?|hours?)/i); if (resetMatch) { const value = parseInt(resetMatch[1]); const unit = resetMatch[2].toLowerCase(); let milliseconds = value * 1000; // Default to seconds if (unit.startsWith('minute')) { milliseconds = value * 60 * 1000; } else if (unit.startsWith('hour')) { milliseconds = value * 60 * 60 * 1000; } return Date.now() + milliseconds; } // Default to 15 minutes if we can't parse return Date.now() + (15 * 60 * 1000); } /** * Classify Claude API errors based on exit code and stderr */ classifyClaudeError(exitCode, stderr) { const lowerStderr = stderr.toLowerCase(); // Usage limit detection if (this.isUsageLimitError(stderr)) { return new UsageLimitError(this.extractResetTime(stderr)); } // Network/API errors if (lowerStderr.includes('network') || lowerStderr.includes('connection')) { return new APIError( 'Network error during Claude API call', 'claude-api', null, { exitCode, stderr } ); } // Authentication errors if (lowerStderr.includes('auth') || lowerStderr.includes('permission')) { return new APIError( 'Authentication error', 'claude-api', 401, { exitCode, stderr } ); } // Generic API error return new APIError( `Claude API call failed with exit code ${exitCode}`, 'claude-api', exitCode, { stderr } ); } /** * Handle Claude API state changes */ onClaudeStateChange(oldState, newState, stats) { console.log(`🔄 Claude API circuit breaker: ${oldState}${newState}`); if (newState === 'OPEN') { console.warn(`⚠️ Claude API circuit breaker is OPEN - ${stats.failureCount} failures detected`); console.warn(`⏰ Next attempt allowed at: ${new Date(stats.nextAttemptTime).toISOString()}`); } else if (newState === 'HALF_OPEN') { console.log('🔍 Claude API circuit breaker testing recovery...'); } else if (newState === 'CLOSED') { console.log('✅ Claude API circuit breaker recovered'); } } /** * Handle Claude API failures */ onClaudeFailure({ error, responseTime, stats }) { console.error(`❌ Claude API failure: ${error.message} (${responseTime}ms)`); if (error instanceof UsageLimitError) { console.warn(`⏰ Usage limit hit, reset time: ${new Date(error.resetTime).toISOString()}`); } if (stats.recentFailureRate > 50) { console.warn(`⚠️ High Claude API failure rate: ${stats.recentFailureRate}%`); } } /** * Handle process state changes */ onProcessStateChange(oldState, newState, stats) { console.log(`🔄 Process spawn circuit breaker: ${oldState}${newState}`); if (newState === 'OPEN') { console.warn(`⚠️ Process spawning is failing - ${stats.failureCount} failures`); } } /** * Get comprehensive health status */ getHealthStatus() { return { claudeAPI: this.claudeAPI.isHealthy(), processSpawn: this.processSpawn.isHealthy(), fileSystem: this.fileSystem.isHealthy(), overall: globalCircuitBreakerManager.getSystemHealth() }; } /** * Get detailed statistics */ getDetailedStats() { return { claudeAPI: this.claudeAPI.getStats(), processSpawn: this.processSpawn.getStats(), fileSystem: this.fileSystem.getStats(), global: globalCircuitBreakerManager.getAllStats() }; } /** * Force recovery for manual intervention */ forceRecovery(component, reason = 'manual intervention') { switch (component) { case 'claude-api': this.claudeAPI.forceState('CLOSED', reason); break; case 'process-spawn': this.processSpawn.forceState('CLOSED', reason); break; case 'file-system': this.fileSystem.forceState('CLOSED', reason); break; case 'all': this.claudeAPI.forceState('CLOSED', reason); this.processSpawn.forceState('CLOSED', reason); this.fileSystem.forceState('CLOSED', reason); break; default: throw new Error(`Unknown component: ${component}`); } } } // Export singleton instance const claudeAPICircuitBreaker = new ClaudeAPICircuitBreaker(); module.exports = { ClaudeAPICircuitBreaker, claudeAPICircuitBreaker };