Skip to main content
Glama
timeout-manager.ts13.1 kB
/** * Timeout Manager - Centralized timeout and retry management using configurable values * Replaces hardcoded timeout values throughout the codebase */ import { VibeTaskManagerConfig } from './config-loader.js'; import logger from '../../../logger.js'; /** * Timeout operation types */ export type TimeoutOperation = | 'taskExecution' | 'taskDecomposition' | 'recursiveTaskDecomposition' | 'taskRefinement' | 'agentCommunication' | 'llmRequest' | 'fileOperations' | 'databaseOperations' | 'networkOperations'; export type TaskComplexity = 'simple' | 'moderate' | 'complex' | 'critical'; export interface ComplexityTimeoutConfig { simple: number; // 1.0x multiplier moderate: number; // 1.5x multiplier complex: number; // 2.0x multiplier critical: number; // 3.0x multiplier } /** * Timeout result interface */ export interface TimeoutResult<T> { success: boolean; data?: T; error?: string; timedOut: boolean; duration: number; retryCount: number; } /** * Retry configuration for specific operation */ export interface RetryConfig { maxRetries: number; backoffMultiplier: number; initialDelayMs: number; maxDelayMs: number; enableExponentialBackoff: boolean; } /** * Centralized timeout and retry manager */ export class TimeoutManager { private static instance: TimeoutManager | null = null; private config: VibeTaskManagerConfig['taskManager'] | null = null; private constructor() {} static getInstance(): TimeoutManager { if (!TimeoutManager.instance) { TimeoutManager.instance = new TimeoutManager(); } return TimeoutManager.instance; } /** * Check if instance exists and is initialized (without creating instance) */ static isInstanceInitialized(): boolean { return TimeoutManager.instance !== null && TimeoutManager.instance.isInitialized(); } /** * Initialize with configuration */ initialize(config: VibeTaskManagerConfig['taskManager']): void { this.config = config; logger.debug('TimeoutManager initialized with configuration'); } /** * Check if timeout manager has been initialized with configuration */ isInitialized(): boolean { return this.config !== null; } /** * Get timeout value for specific operation */ getTimeout(operation: TimeoutOperation): number { if (!this.config) { // Fallback values matching test expectations const fallbacks: Record<TimeoutOperation, number> = { taskExecution: 300000, // 5 minutes (test expectation) taskDecomposition: 600000, // 10 minutes recursiveTaskDecomposition: 720000, // 12 minutes taskRefinement: 180000, // 3 minutes agentCommunication: 30000, // 30 seconds llmRequest: 60000, // 1 minute (test expectation) fileOperations: 10000, // 10 seconds databaseOperations: 15000, // 15 seconds networkOperations: 20000 // 20 seconds }; logger.warn({ operation }, 'Using fallback timeout value - config not initialized'); return fallbacks[operation]; } return this.config.timeouts[operation]; } /** * Get timeout value adjusted for task complexity */ getComplexityAdjustedTimeout( operation: TimeoutOperation, complexity: TaskComplexity, estimatedHours?: number ): number { const baseTimeout = this.getTimeout(operation); // Complexity multipliers const complexityMultipliers: ComplexityTimeoutConfig = { simple: 1.0, moderate: 1.5, complex: 2.0, critical: 3.0 }; let adjustedTimeout = baseTimeout * complexityMultipliers[complexity]; // Additional adjustment based on estimated hours for task execution if (operation === 'taskExecution' && estimatedHours) { const hourMultiplier = Math.max(1.0, estimatedHours / 2); // Scale with estimated time adjustedTimeout = Math.max(adjustedTimeout, baseTimeout * hourMultiplier); } // Cap maximum timeout to prevent runaway operations const maxTimeout = operation === 'taskExecution' ? 14400000 : baseTimeout * 5; // 4 hours max for tasks return Math.min(adjustedTimeout, maxTimeout); } /** * Get retry configuration */ getRetryConfig(): RetryConfig { if (!this.config) { logger.warn('Using fallback retry config - config not initialized'); return { maxRetries: 3, // Test expectation backoffMultiplier: 2.0, // Test expectation initialDelayMs: 1000, maxDelayMs: 30000, // Test expectation enableExponentialBackoff: true }; } return this.config.retryPolicy; } /** * Get retry configuration adjusted for operation complexity */ getComplexityAdjustedRetryConfig(complexity: TaskComplexity): RetryConfig { const baseConfig = this.getRetryConfig(); // Adjust retry parameters based on complexity const complexityAdjustments = { simple: { maxRetries: baseConfig.maxRetries, backoffMultiplier: baseConfig.backoffMultiplier }, moderate: { maxRetries: baseConfig.maxRetries + 1, backoffMultiplier: baseConfig.backoffMultiplier * 0.9 }, complex: { maxRetries: baseConfig.maxRetries + 2, backoffMultiplier: baseConfig.backoffMultiplier * 0.8 }, critical: { maxRetries: baseConfig.maxRetries + 3, backoffMultiplier: baseConfig.backoffMultiplier * 0.7 } }; const adjustment = complexityAdjustments[complexity]; return { ...baseConfig, maxRetries: Math.min(adjustment.maxRetries, 10), // Cap at 10 retries backoffMultiplier: Math.max(adjustment.backoffMultiplier, 1.2), // Minimum 1.2x backoff maxDelayMs: Math.min(baseConfig.maxDelayMs * 2, 120000) // Cap at 2 minutes }; } /** * Execute operation with timeout and retry logic */ async executeWithTimeout<T>( operation: TimeoutOperation, operationFn: () => Promise<T>, customTimeout?: number, customRetryConfig?: Partial<RetryConfig> ): Promise<TimeoutResult<T>> { const timeout = customTimeout || this.getTimeout(operation); const retryConfig = { ...this.getRetryConfig(), ...customRetryConfig }; let retryCount = 0; let lastError: string | undefined; const startTime = Date.now(); while (retryCount <= retryConfig.maxRetries) { try { const result = await this.executeWithTimeoutOnce(operationFn, timeout); if (result.success) { const duration = Date.now() - startTime; logger.debug({ operation, duration, retryCount, timeout }, 'Operation completed successfully'); return { success: true, data: result.data, timedOut: false, duration, retryCount }; } lastError = result.error; if (result.timedOut && retryCount < retryConfig.maxRetries) { const delay = this.calculateDelay(retryCount, retryConfig); logger.warn({ operation, retryCount: retryCount + 1, delay, timeout, error: lastError }, 'Operation timed out, retrying'); await this.delay(delay); } } catch (error) { lastError = error instanceof Error ? error.message : 'Unknown error'; if (retryCount < retryConfig.maxRetries) { const delay = this.calculateDelay(retryCount, retryConfig); logger.warn({ operation, retryCount: retryCount + 1, delay, error: lastError }, 'Operation failed, retrying'); await this.delay(delay); } } retryCount++; } const duration = Date.now() - startTime; logger.error({ operation, retryCount, duration, timeout, error: lastError }, 'Operation failed after all retries'); return { success: false, error: lastError || 'Operation failed after maximum retries', timedOut: true, duration, retryCount }; } /** * Execute operation with timeout (single attempt) */ private async executeWithTimeoutOnce<T>( operationFn: () => Promise<T>, timeout: number ): Promise<{ success: boolean; data?: T; error?: string; timedOut: boolean }> { return new Promise((resolve) => { let completed = false; // Set up timeout const timeoutHandle = setTimeout(() => { if (!completed) { completed = true; resolve({ success: false, error: `Operation timed out after ${timeout}ms`, timedOut: true }); } }, timeout); // Execute operation operationFn() .then((result) => { if (!completed) { completed = true; clearTimeout(timeoutHandle); resolve({ success: true, data: result, timedOut: false }); } }) .catch((error) => { if (!completed) { completed = true; clearTimeout(timeoutHandle); resolve({ success: false, error: error instanceof Error ? error.message : 'Unknown error', timedOut: false }); } }); }); } /** * Calculate delay for retry with exponential backoff */ private calculateDelay(retryCount: number, config: RetryConfig): number { if (!config.enableExponentialBackoff) { return config.initialDelayMs; } const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, retryCount); return Math.min(delay, config.maxDelayMs); } /** * Delay utility function */ delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Create a timeout promise for manual timeout handling */ createTimeoutPromise(operation: TimeoutOperation, customTimeout?: number): Promise<never> { const timeout = customTimeout || this.getTimeout(operation); return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`${operation} operation timed out after ${timeout}ms`)); }, timeout); }); } /** * Race operation against timeout */ async raceWithTimeout<T>( operation: TimeoutOperation, operationPromise: Promise<T>, customTimeout?: number ): Promise<T> { const timeoutPromise = this.createTimeoutPromise(operation, customTimeout); return Promise.race([operationPromise, timeoutPromise]); } /** * Get timeout configuration summary */ getTimeoutSummary(): Record<TimeoutOperation, number> { const operations: TimeoutOperation[] = [ 'taskExecution', 'taskDecomposition', 'recursiveTaskDecomposition', 'taskRefinement', 'agentCommunication', 'llmRequest', 'fileOperations', 'databaseOperations', 'networkOperations' ]; const summary: Record<TimeoutOperation, number> = {} as Record<TimeoutOperation, number>; for (const operation of operations) { summary[operation] = this.getTimeout(operation); } return summary; } /** * Validate timeout configuration */ validateTimeouts(): { valid: boolean; issues: string[] } { const issues: string[] = []; if (!this.config) { issues.push('Timeout configuration not initialized'); return { valid: false, issues }; } // Check for reasonable timeout values const timeouts = this.config.timeouts; if (timeouts.taskExecution < 10000) { issues.push('Task execution timeout is too low (< 10 seconds)'); } if (timeouts.taskExecution > 3600000) { issues.push('Task execution timeout is too high (> 1 hour)'); } if (timeouts.llmRequest < 5000) { issues.push('LLM request timeout is too low (< 5 seconds)'); } if (timeouts.fileOperations < 1000) { issues.push('File operations timeout is too low (< 1 second)'); } // Check retry configuration const retry = this.config.retryPolicy; if (retry.maxRetries < 0 || retry.maxRetries > 10) { issues.push('Max retries should be between 0 and 10'); } if (retry.backoffMultiplier < 1.0 || retry.backoffMultiplier > 5.0) { issues.push('Backoff multiplier should be between 1.0 and 5.0'); } if (retry.initialDelayMs < 100 || retry.initialDelayMs > 10000) { issues.push('Initial delay should be between 100ms and 10 seconds'); } return { valid: issues.length === 0, issues }; } } /** * Check if timeout manager has been created and initialized */ export function isTimeoutManagerInitialized(): boolean { return TimeoutManager.isInstanceInitialized(); } /** * Convenience function to get timeout manager instance */ export function getTimeoutManager(): TimeoutManager { return TimeoutManager.getInstance(); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server