UNPKG

claude-flow-tbowman01

Version:

Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)

278 lines 10 kB
/** * Fallback Coordinator for MCP * Manages graceful degradation to CLI when MCP connection fails */ import { EventEmitter } from 'node:events'; import { exec } from 'node:child_process'; import { promisify } from 'node:util'; const execAsync = promisify(exec); export class FallbackCoordinator extends EventEmitter { logger; operationQueue = []; state; notificationTimer; processingQueue = false; defaultConfig = { enableFallback: true, maxQueueSize: 100, queueTimeout: 300000, // 5 minutes cliPath: 'npx ruv-swarm', fallbackNotificationInterval: 30000, // 30 seconds }; constructor(logger, config) { super(); this.logger = logger; this.config = { ...this.defaultConfig, ...config }; this.state = { isFallbackActive: false, queuedOperations: 0, failedOperations: 0, successfulOperations: 0, }; } config; /** * Check if MCP is available */ async isMCPAvailable() { try { // Try to execute a simple MCP command const { stdout } = await execAsync(`${this.config.cliPath} status --json`); const status = JSON.parse(stdout); return status.connected === true; } catch (error) { this.logger.debug('MCP availability check failed', error); return false; } } /** * Enable CLI fallback mode */ enableCLIFallback() { if (this.state.isFallbackActive) { this.logger.debug('Fallback already active'); return; } this.logger.warn('Enabling CLI fallback mode'); this.state.isFallbackActive = true; this.state.lastFallbackActivation = new Date(); // Start notification timer this.startNotificationTimer(); this.emit('fallbackEnabled', this.state); } /** * Disable CLI fallback mode */ disableCLIFallback() { if (!this.state.isFallbackActive) { return; } this.logger.info('Disabling CLI fallback mode'); this.state.isFallbackActive = false; // Stop notification timer this.stopNotificationTimer(); this.emit('fallbackDisabled', this.state); // Process any queued operations if (this.operationQueue.length > 0) { this.processQueue().catch((error) => { this.logger.error('Error processing queue after fallback disabled', error); }); } } /** * Queue an operation for later execution */ queueOperation(operation) { if (!this.config.enableFallback) { this.logger.debug('Fallback disabled, operation not queued'); return; } if (this.operationQueue.length >= this.config.maxQueueSize) { this.logger.warn('Operation queue full, removing oldest operation'); this.operationQueue.shift(); this.state.failedOperations++; } const queuedOp = { ...operation, id: this.generateOperationId(), timestamp: new Date(), }; this.operationQueue.push(queuedOp); this.state.queuedOperations = this.operationQueue.length; this.logger.debug('Operation queued', { id: queuedOp.id, type: queuedOp.type, method: queuedOp.method, queueSize: this.operationQueue.length, }); this.emit('operationQueued', queuedOp); // If in fallback mode, try to execute via CLI if (this.state.isFallbackActive && !this.processingQueue) { this.executeViaCliFallback(queuedOp).catch((error) => { this.logger.error('CLI fallback execution failed', { operation: queuedOp, error }); }); } } /** * Process all queued operations */ async processQueue() { if (this.processingQueue || this.operationQueue.length === 0) { return; } this.processingQueue = true; this.logger.info('Processing operation queue', { queueSize: this.operationQueue.length, }); this.emit('queueProcessingStart', this.operationQueue.length); const results = { successful: 0, failed: 0, }; // Process operations in order while (this.operationQueue.length > 0) { const operation = this.operationQueue.shift(); // Check if operation has expired if (this.isOperationExpired(operation)) { this.logger.warn('Operation expired', { id: operation.id }); results.failed++; continue; } try { await this.replayOperation(operation); results.successful++; this.state.successfulOperations++; } catch (error) { this.logger.error('Failed to replay operation', { operation, error, }); results.failed++; this.state.failedOperations++; // Re-queue if retryable if (operation.retryable) { this.operationQueue.push(operation); } } } this.state.queuedOperations = this.operationQueue.length; this.processingQueue = false; this.logger.info('Queue processing complete', results); this.emit('queueProcessingComplete', results); } /** * Get current fallback state */ getState() { return { ...this.state }; } /** * Get queued operations */ getQueuedOperations() { return [...this.operationQueue]; } /** * Clear operation queue */ clearQueue() { const clearedCount = this.operationQueue.length; this.operationQueue = []; this.state.queuedOperations = 0; this.logger.info('Operation queue cleared', { clearedCount }); this.emit('queueCleared', clearedCount); } async executeViaCliFallback(operation) { this.logger.debug('Executing operation via CLI fallback', { id: operation.id, method: operation.method, }); try { // Map MCP operations to CLI commands const cliCommand = this.mapOperationToCli(operation); if (!cliCommand) { throw new Error(`No CLI mapping for operation: ${operation.method}`); } const { stdout, stderr } = await execAsync(cliCommand); if (stderr) { this.logger.warn('CLI command stderr', { stderr }); } this.logger.debug('CLI fallback execution successful', { id: operation.id, stdout: stdout.substring(0, 200), // Log first 200 chars }); this.state.successfulOperations++; this.emit('fallbackExecutionSuccess', { operation, result: stdout }); } catch (error) { this.logger.error('CLI fallback execution failed', { operation, error, }); this.state.failedOperations++; this.emit('fallbackExecutionFailed', { operation, error }); // Re-queue if retryable if (operation.retryable) { this.queueOperation(operation); } } } async replayOperation(operation) { // This would typically use the MCP client to replay the operation // For now, we'll log it this.logger.info('Replaying operation', { id: operation.id, method: operation.method, }); // Emit event for handling by the MCP client this.emit('replayOperation', operation); } mapOperationToCli(operation) { // Map common MCP operations to CLI commands const mappings = { // Tool operations 'tools/list': () => `${this.config.cliPath} tools list`, 'tools/call': (params) => `${this.config.cliPath} tools call ${params.name} '${JSON.stringify(params.arguments)}'`, // Resource operations 'resources/list': () => `${this.config.cliPath} resources list`, 'resources/read': (params) => `${this.config.cliPath} resources read ${params.uri}`, // Session operations initialize: () => `${this.config.cliPath} session init`, shutdown: () => `${this.config.cliPath} session shutdown`, // Custom operations heartbeat: () => `${this.config.cliPath} health check`, }; const mapper = mappings[operation.method]; return mapper ? mapper(operation.params) : null; } isOperationExpired(operation) { const age = Date.now() - operation.timestamp.getTime(); return age > this.config.queueTimeout; } generateOperationId() { return `op-${Date.now()}-${Math.random().toString(36).slice(2)}`; } startNotificationTimer() { if (this.notificationTimer) { return; } this.notificationTimer = setInterval(() => { if (this.state.isFallbackActive && this.operationQueue.length > 0) { this.logger.info('Fallback mode active', { queuedOperations: this.operationQueue.length, duration: Date.now() - (this.state.lastFallbackActivation?.getTime() || 0), }); this.emit('fallbackStatus', this.state); } }, this.config.fallbackNotificationInterval); } stopNotificationTimer() { if (this.notificationTimer) { clearInterval(this.notificationTimer); this.notificationTimer = undefined; } } } //# sourceMappingURL=fallback-coordinator.js.map