UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

284 lines (283 loc) 11.3 kB
import { getTimeoutManager } from '../utils/timeout-manager.js'; import logger from '../../../logger.js'; import { EventEmitter } from 'events'; class CancellationTokenImpl { _isCancelled = false; _callbacks = []; get isCancelled() { return this._isCancelled; } cancel() { if (!this._isCancelled) { this._isCancelled = true; this._callbacks.forEach(callback => { try { callback(); } catch (error) { logger.error({ err: error }, 'Error in cancellation callback'); } }); } } onCancelled(callback) { if (this._isCancelled) { callback(); } else { this._callbacks.push(callback); } } } export class AdaptiveTimeoutManager extends EventEmitter { static instance; activeOperations = new Map(); constructor() { super(); } static getInstance() { if (!AdaptiveTimeoutManager.instance) { AdaptiveTimeoutManager.instance = new AdaptiveTimeoutManager(); } return AdaptiveTimeoutManager.instance; } async executeWithTimeout(operationId, operation, config = {}, partialResultExtractor) { const timeoutManager = getTimeoutManager(); const retryConfig = timeoutManager.getRetryConfig(); const operationType = this.inferOperationType(operationId); const baseTimeout = operationType === 'taskDecomposition' ? timeoutManager.getTimeout('taskDecomposition') : timeoutManager.getTimeout('taskExecution'); const fullConfig = { baseTimeoutMs: baseTimeout, maxTimeoutMs: baseTimeout * 2, progressCheckIntervalMs: operationType === 'taskDecomposition' ? 30000 : 10000, exponentialBackoffFactor: retryConfig.backoffMultiplier, maxRetries: retryConfig.maxRetries, partialResultThreshold: 0.3, ...config }; let retryCount = 0; let lastError; while (retryCount <= fullConfig.maxRetries) { const result = await this.attemptOperation(operationId, operation, fullConfig, retryCount, partialResultExtractor); if (result.success || !result.timeoutOccurred) { return result; } if (result.partialResult && result.progressAtTimeout) { const progressRatio = result.progressAtTimeout.completed / result.progressAtTimeout.total; if (progressRatio >= fullConfig.partialResultThreshold) { logger.info({ operationId, progressRatio, retryCount }, 'Accepting partial result due to sufficient progress'); return { ...result, success: true, result: result.partialResult }; } } lastError = result.error; retryCount++; if (retryCount <= fullConfig.maxRetries) { const backoffDelay = this.calculateBackoffDelay(retryCount, fullConfig); logger.info({ operationId, retryCount, backoffDelay, lastError }, 'Retrying operation after timeout'); await this.delay(backoffDelay); } } return { success: false, error: lastError || 'Operation failed after maximum retries', timeoutOccurred: true, retryCount, totalDuration: 0 }; } createCancellationToken() { return new CancellationTokenImpl(); } cancelOperation(operationId) { const operation = this.activeOperations.get(operationId); if (operation) { operation.cancellationToken.cancel(); this.clearOperationTimeouts(operationId); this.activeOperations.delete(operationId); logger.info({ operationId }, 'Operation cancelled'); return true; } return false; } getActiveOperations() { return Array.from(this.activeOperations.keys()); } getOperationProgress(operationId) { return this.activeOperations.get(operationId)?.progress; } async attemptOperation(operationId, operation, config, retryCount, partialResultExtractor) { const startTime = new Date(); const cancellationToken = new CancellationTokenImpl(); let currentProgress; let operationState = {}; const operationInfo = { startTime, config, progress: currentProgress, cancellationToken, timeoutHandle: undefined, progressCheckHandle: undefined }; this.activeOperations.set(operationId, operationInfo); try { const progressCallback = (progress) => { currentProgress = progress; operationInfo.progress = progress; operationState = { ...operationState, progress }; this.adjustTimeoutBasedOnProgress(operationId, progress, config); this.emit('progress', { operationId, progress }); }; const adaptiveTimeout = this.calculateAdaptiveTimeout(config, retryCount); operationInfo.timeoutHandle = setTimeout(() => { this.handleTimeout(operationId); }, adaptiveTimeout); operationInfo.progressCheckHandle = setInterval(() => { this.checkProgressStagnation(operationId); }, config.progressCheckIntervalMs); const result = await operation(cancellationToken, progressCallback); this.clearOperationTimeouts(operationId); const totalDuration = Date.now() - startTime.getTime(); return { success: true, result, timeoutOccurred: false, retryCount, totalDuration }; } catch (error) { this.clearOperationTimeouts(operationId); const totalDuration = Date.now() - startTime.getTime(); const isTimeout = cancellationToken.isCancelled; let partialResult; if (isTimeout && partialResultExtractor) { try { partialResult = partialResultExtractor(operationState); } catch (extractError) { logger.warn({ err: extractError, operationId }, 'Failed to extract partial result'); } } return { success: false, error: error instanceof Error ? error.message : String(error), timeoutOccurred: isTimeout, retryCount, totalDuration, partialResult, progressAtTimeout: currentProgress }; } finally { this.activeOperations.delete(operationId); } } calculateAdaptiveTimeout(config, retryCount) { const baseTimeout = config.baseTimeoutMs * Math.pow(config.exponentialBackoffFactor, retryCount); return Math.min(baseTimeout, config.maxTimeoutMs); } adjustTimeoutBasedOnProgress(operationId, progress, config) { const operation = this.activeOperations.get(operationId); if (!operation || !operation.timeoutHandle) return; const progressRatio = progress.completed / progress.total; const elapsedTime = Date.now() - operation.startTime.getTime(); if (progressRatio > 0.1 && progress.estimatedTimeRemaining) { const newTimeout = Math.min(progress.estimatedTimeRemaining * 1.5, config.maxTimeoutMs - elapsedTime); if (newTimeout > 5000) { clearTimeout(operation.timeoutHandle); operation.timeoutHandle = setTimeout(() => { this.handleTimeout(operationId); }, newTimeout); logger.debug({ operationId, progressRatio, newTimeout, estimatedRemaining: progress.estimatedTimeRemaining }, 'Adjusted timeout based on progress'); } } } handleTimeout(operationId) { const operation = this.activeOperations.get(operationId); if (operation) { logger.warn({ operationId, elapsedTime: Date.now() - operation.startTime.getTime(), progress: operation.progress }, 'Operation timeout triggered'); operation.cancellationToken.cancel(); this.emit('timeout', { operationId, progress: operation.progress }); } } checkProgressStagnation(operationId) { const operation = this.activeOperations.get(operationId); if (!operation?.progress) return; const timeSinceLastUpdate = Date.now() - operation.progress.lastUpdate.getTime(); const multiplier = operation.config.progressCheckIntervalMs >= 30000 ? 2 : 3; const stagnationThreshold = operation.config.progressCheckIntervalMs * multiplier; if (timeSinceLastUpdate > stagnationThreshold) { logger.warn({ operationId, timeSinceLastUpdate, currentStage: operation.progress.stage }, 'Progress stagnation detected'); this.emit('stagnation', { operationId, progress: operation.progress }); } } calculateBackoffDelay(retryCount, config) { return Math.min(1000 * Math.pow(config.exponentialBackoffFactor, retryCount), 30000); } inferOperationType(operationId) { const lowerOperationId = operationId.toLowerCase(); if (lowerOperationId.includes('decomposition') || lowerOperationId.includes('decompose') || lowerOperationId.includes('split') || lowerOperationId.includes('rdd')) { return 'taskDecomposition'; } if (lowerOperationId.includes('execution') || lowerOperationId.includes('execute') || lowerOperationId.includes('run')) { return 'taskExecution'; } return 'other'; } clearOperationTimeouts(operationId) { const operation = this.activeOperations.get(operationId); if (operation) { if (operation.timeoutHandle) { clearTimeout(operation.timeoutHandle); } if (operation.progressCheckHandle) { clearInterval(operation.progressCheckHandle); } } } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } shutdown() { for (const operationId of this.activeOperations.keys()) { this.cancelOperation(operationId); } this.removeAllListeners(); logger.info('Adaptive Timeout Manager shutdown'); } }