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
JavaScript
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');
}
}