Skip to main content
Glama
ooples

MCP Console Automation Server

SessionRecovery.ts38.2 kB
import { EventEmitter } from 'events'; import { Logger } from '../utils/logger.js'; import { SessionState, SessionOptions } from '../types/index.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import { v4 as uuidv4 } from 'uuid'; export interface RecoveryConfig { enabled: boolean; maxRecoveryAttempts: number; recoveryDelay: number; backoffMultiplier: number; maxBackoffDelay: number; persistenceEnabled: boolean; persistencePath: string; enableSmartRecovery: boolean; enablePersistentSession: boolean; snapshotInterval: number; recoveryTimeout: number; enableRecoveryMetrics: boolean; } export interface RecoverySnapshot { sessionId: string; timestamp: Date; sessionState: SessionState; sessionOptions: SessionOptions; environment: Record<string, string>; workingDirectory: string; commandHistory: string[]; outputBuffer: string[]; errorContext?: { lastError: string; errorTimestamp: Date; errorCount: number; }; interactiveState?: { isInteractive: boolean; promptType?: string; lastPromptDetected?: Date; pendingCommands: string[]; sessionUnresponsive: boolean; timeoutCount: number; lastSuccessfulCommand?: Date; }; } export interface RecoveryAttempt { sessionId: string; attemptNumber: number; startTime: Date; endTime?: Date; success: boolean; error?: string; strategy: RecoveryStrategy; metrics: { duration: number; resourcesUsed: number; dataRestored: number; }; } export type RecoveryStrategy = | 'restart' | 'reconnect' | 'restore' | 'replicate' | 'migrate' | 'fallback' | 'prompt-interrupt' | 'prompt-reset' | 'session-refresh' | 'command-retry'; export interface RecoveryPlan { sessionId: string; strategies: RecoveryStrategy[]; priority: 'high' | 'medium' | 'low'; estimatedTime: number; requiredResources: string[]; fallbackOptions: string[]; interactiveContext?: { promptTimeout: boolean; commandInProgress: boolean; lastKnownPrompt?: string; unresponsiveTime?: number; }; } /** * Automatic Session Recovery System * Provides intelligent session restoration with multiple recovery strategies */ export class SessionRecovery extends EventEmitter { private logger: Logger; private config: RecoveryConfig; private recoveryAttempts: Map<string, RecoveryAttempt[]> = new Map(); private sessionSnapshots: Map<string, RecoverySnapshot> = new Map(); private activeRecoveries: Set<string> = new Set(); private snapshotTimers: Map<string, NodeJS.Timeout> = new Map(); private isRunning = false; // Recovery statistics private stats = { totalRecoveryAttempts: 0, successfulRecoveries: 0, failedRecoveries: 0, averageRecoveryTime: 0, sessionsSaved: 0, sessionsRestored: 0, bytesRestored: 0, strategiesUsed: new Map<RecoveryStrategy, number>(), lastRecovery: null as Date | null, }; constructor(config?: Partial<RecoveryConfig>) { super(); this.logger = new Logger('SessionRecovery'); this.config = { enabled: config?.enabled ?? true, maxRecoveryAttempts: config?.maxRecoveryAttempts || 3, recoveryDelay: config?.recoveryDelay || 5000, backoffMultiplier: config?.backoffMultiplier || 2, maxBackoffDelay: config?.maxBackoffDelay || 60000, persistenceEnabled: config?.persistenceEnabled ?? true, persistencePath: config?.persistencePath || './data/session-snapshots', enableSmartRecovery: config?.enableSmartRecovery ?? true, enablePersistentSession: config?.enablePersistentSession ?? true, snapshotInterval: config?.snapshotInterval || 30000, recoveryTimeout: config?.recoveryTimeout || 120000, enableRecoveryMetrics: config?.enableRecoveryMetrics ?? true, }; this.logger.info('SessionRecovery initialized with config:', this.config); } /** * Start the session recovery system */ async start(): Promise<void> { if (this.isRunning) { this.logger.warn('SessionRecovery is already running'); return; } this.logger.info('Starting SessionRecovery...'); this.isRunning = true; // Ensure persistence directory exists if (this.config.persistenceEnabled) { try { await fs.mkdir(this.config.persistencePath, { recursive: true }); } catch (error) { this.logger.error('Failed to create persistence directory:', error); } } // Load existing snapshots from disk await this.loadPersistedSnapshots(); this.emit('started'); this.logger.info('SessionRecovery started successfully'); } /** * Stop the session recovery system */ async stop(): Promise<void> { if (!this.isRunning) { this.logger.warn('SessionRecovery is not running'); return; } this.logger.info('Stopping SessionRecovery...'); this.isRunning = false; // Clear snapshot timers for (const [sessionId, timer] of this.snapshotTimers) { clearInterval(timer); } this.snapshotTimers.clear(); // Wait for active recoveries to complete if (this.activeRecoveries.size > 0) { this.logger.info( `Waiting for ${this.activeRecoveries.size} active recoveries to complete...` ); const timeout = setTimeout(() => { this.logger.warn('Timeout waiting for active recoveries'); }, 30000); while (this.activeRecoveries.size > 0) { await this.delay(1000); } clearTimeout(timeout); } // Persist current snapshots if (this.config.persistenceEnabled) { await this.persistAllSnapshots(); } this.emit('stopped'); this.logger.info('SessionRecovery stopped'); } /** * Register a session for recovery monitoring */ async registerSession( sessionId: string, sessionState: SessionState, sessionOptions: SessionOptions ): Promise<void> { if (!this.config.enabled) { return; } this.logger.info( `Registering session ${sessionId} for recovery monitoring` ); // Create initial snapshot const snapshot: RecoverySnapshot = { sessionId, timestamp: new Date(), sessionState: { ...sessionState }, sessionOptions: { ...sessionOptions }, environment: { ...(sessionOptions.env || {}) }, workingDirectory: sessionOptions.cwd || process.cwd(), commandHistory: [], outputBuffer: [], }; this.sessionSnapshots.set(sessionId, snapshot); this.stats.sessionsSaved++; // Start periodic snapshotting if (this.config.snapshotInterval > 0) { this.startPeriodicSnapshots(sessionId); } // Persist snapshot if enabled if (this.config.persistenceEnabled) { await this.persistSnapshot(snapshot); } this.emit('session-registered', { sessionId, snapshot }); } /** * Unregister a session from recovery monitoring */ async unregisterSession(sessionId: string): Promise<void> { this.logger.info( `Unregistering session ${sessionId} from recovery monitoring` ); // Clear snapshot timer const timer = this.snapshotTimers.get(sessionId); if (timer) { clearInterval(timer); this.snapshotTimers.delete(sessionId); } // Remove snapshot this.sessionSnapshots.delete(sessionId); // Remove persisted snapshot if (this.config.persistenceEnabled) { try { const snapshotPath = path.join( this.config.persistencePath, `${sessionId}.snapshot.json` ); await fs.unlink(snapshotPath); } catch (error) { // Ignore if file doesn't exist } } this.emit('session-unregistered', { sessionId }); } /** * Update session snapshot with new state */ async updateSessionSnapshot( sessionId: string, updates: Partial< Pick< RecoverySnapshot, | 'sessionState' | 'environment' | 'workingDirectory' | 'commandHistory' | 'outputBuffer' | 'errorContext' | 'interactiveState' > > ): Promise<void> { const snapshot = this.sessionSnapshots.get(sessionId); if (!snapshot) { return; } // Update snapshot Object.assign(snapshot, updates); snapshot.timestamp = new Date(); // Persist if enabled if (this.config.persistenceEnabled) { await this.persistSnapshot(snapshot); } this.emit('snapshot-updated', { sessionId, snapshot }); } /** * Attempt to recover a failed session */ async recoverSession( sessionId: string, failureReason?: string ): Promise<boolean> { if (!this.config.enabled || this.activeRecoveries.has(sessionId)) { return false; } const snapshot = this.sessionSnapshots.get(sessionId); if (!snapshot) { this.logger.warn( `No snapshot found for session ${sessionId} - cannot recover` ); return false; } const existingAttempts = this.recoveryAttempts.get(sessionId) || []; if (existingAttempts.length >= this.config.maxRecoveryAttempts) { this.logger.warn( `Max recovery attempts (${this.config.maxRecoveryAttempts}) reached for session ${sessionId}` ); this.emit('recovery-max-attempts', { sessionId, attempts: existingAttempts.length, }); return false; } this.activeRecoveries.add(sessionId); const attemptNumber = existingAttempts.length + 1; this.logger.info( `Starting recovery attempt ${attemptNumber}/${this.config.maxRecoveryAttempts} for session ${sessionId}` ); try { // Generate recovery plan const recoveryPlan = await this.generateRecoveryPlan( sessionId, failureReason ); this.emit('recovery-started', { sessionId, attemptNumber, plan: recoveryPlan, failureReason, }); // Execute recovery strategies const success = await this.executeRecoveryPlan( sessionId, recoveryPlan, attemptNumber ); if (success) { this.stats.successfulRecoveries++; this.stats.lastRecovery = new Date(); this.logger.info( `Successfully recovered session ${sessionId} on attempt ${attemptNumber}` ); this.emit('recovery-success', { sessionId, attemptNumber, plan: recoveryPlan, }); } else { this.stats.failedRecoveries++; this.logger.warn( `Failed to recover session ${sessionId} on attempt ${attemptNumber}` ); // Schedule next attempt with exponential backoff if (attemptNumber < this.config.maxRecoveryAttempts) { const delay = Math.min( this.config.recoveryDelay * Math.pow(this.config.backoffMultiplier, attemptNumber - 1), this.config.maxBackoffDelay ); this.logger.info( `Scheduling next recovery attempt for session ${sessionId} in ${delay}ms` ); setTimeout(() => { this.recoverSession(sessionId, failureReason); }, delay); } } return success; } catch (error) { this.logger.error( `Recovery attempt ${attemptNumber} failed for session ${sessionId}:`, error ); this.stats.failedRecoveries++; this.emit('recovery-error', { sessionId, attemptNumber, error: error instanceof Error ? error.message : String(error), }); return false; } finally { this.activeRecoveries.delete(sessionId); this.stats.totalRecoveryAttempts++; } } /** * Generate intelligent recovery plan based on failure analysis */ private async generateRecoveryPlan( sessionId: string, failureReason?: string ): Promise<RecoveryPlan> { const snapshot = this.sessionSnapshots.get(sessionId)!; const sessionType = snapshot.sessionOptions.sshOptions ? 'ssh' : 'local'; let strategies: RecoveryStrategy[] = []; let priority: 'high' | 'medium' | 'low' = 'medium'; let estimatedTime = 30000; // 30 seconds default // Analyze failure reason to determine best recovery strategy const reason = failureReason?.toLowerCase(); if (this.config.enableSmartRecovery && failureReason && reason) { if (reason.includes('network') || reason.includes('connection')) { strategies = sessionType === 'ssh' ? ['reconnect', 'restart', 'migrate', 'fallback'] : ['restart', 'restore', 'fallback']; priority = 'high'; estimatedTime = sessionType === 'ssh' ? 45000 : 15000; } else if ( reason.includes('timeout') || reason.includes('unresponsive') ) { // Enhanced timeout handling with interactive prompt awareness if (reason.includes('prompt') || reason.includes('interactive')) { strategies = [ 'prompt-interrupt', 'prompt-reset', 'session-refresh', 'restart', 'fallback', ]; priority = 'high'; estimatedTime = 15000; } else { strategies = ['restart', 'restore', 'replicate', 'fallback']; priority = 'high'; estimatedTime = 20000; } } else if (reason.includes('memory') || reason.includes('resource')) { strategies = ['migrate', 'restart', 'fallback']; priority = 'medium'; estimatedTime = 60000; } else if (reason.includes('permission') || reason.includes('auth')) { strategies = ['reconnect', 'restore', 'fallback']; priority = 'low'; estimatedTime = 30000; } else { // Generic failure strategies = sessionType === 'ssh' ? ['reconnect', 'restart', 'restore', 'fallback'] : ['restart', 'restore', 'replicate', 'fallback']; } } else { // Default recovery strategies strategies = sessionType === 'ssh' ? ['reconnect', 'restart', 'restore', 'fallback'] : ['restart', 'restore', 'fallback']; } const requiredResources = [ 'cpu', sessionType === 'ssh' ? 'network' : 'local-process', 'memory', ]; const fallbackOptions = [ 'Create new session with restored state', 'Notify user of session failure', 'Archive session for manual recovery', ]; // Add interactive context if dealing with prompt/timeout issues let interactiveContext: RecoveryPlan['interactiveContext']; if ( reason && (reason.includes('timeout') || reason.includes('prompt') || reason.includes('interactive')) ) { const interactiveState = snapshot.interactiveState; interactiveContext = { promptTimeout: reason.includes('timeout'), commandInProgress: interactiveState?.pendingCommands.length > 0 || false, lastKnownPrompt: interactiveState?.promptType, unresponsiveTime: interactiveState?.sessionUnresponsive ? Date.now() - (interactiveState.lastSuccessfulCommand?.getTime() || Date.now()) : undefined, }; } return { sessionId, strategies, priority, estimatedTime, requiredResources, fallbackOptions, interactiveContext, }; } /** * Execute recovery plan using the specified strategies */ private async executeRecoveryPlan( sessionId: string, plan: RecoveryPlan, attemptNumber: number ): Promise<boolean> { const startTime = Date.now(); const attempt: RecoveryAttempt = { sessionId, attemptNumber, startTime: new Date(), success: false, strategy: plan.strategies[0], // Will be updated as we try strategies metrics: { duration: 0, resourcesUsed: 0, dataRestored: 0, }, }; // Store attempt if (!this.recoveryAttempts.has(sessionId)) { this.recoveryAttempts.set(sessionId, []); } this.recoveryAttempts.get(sessionId)!.push(attempt); // Try each strategy in order for (const strategy of plan.strategies) { attempt.strategy = strategy; this.stats.strategiesUsed.set( strategy, (this.stats.strategiesUsed.get(strategy) || 0) + 1 ); this.logger.info( `Attempting recovery strategy '${strategy}' for session ${sessionId}` ); try { const success = await this.executeRecoveryStrategy(sessionId, strategy); if (success) { attempt.success = true; attempt.endTime = new Date(); attempt.metrics.duration = Date.now() - startTime; // Update average recovery time this.updateAverageRecoveryTime(attempt.metrics.duration); this.logger.info( `Recovery strategy '${strategy}' succeeded for session ${sessionId}` ); return true; } else { this.logger.warn( `Recovery strategy '${strategy}' failed for session ${sessionId}` ); } } catch (error) { this.logger.error( `Recovery strategy '${strategy}' error for session ${sessionId}:`, error ); } } // All strategies failed attempt.endTime = new Date(); attempt.metrics.duration = Date.now() - startTime; attempt.error = 'All recovery strategies failed'; return false; } /** * Execute a specific recovery strategy */ private async executeRecoveryStrategy( sessionId: string, strategy: RecoveryStrategy ): Promise<boolean> { const snapshot = this.sessionSnapshots.get(sessionId); if (!snapshot) return false; switch (strategy) { case 'restart': return await this.executeRestartStrategy(sessionId, snapshot); case 'reconnect': return await this.executeReconnectStrategy(sessionId, snapshot); case 'restore': return await this.executeRestoreStrategy(sessionId, snapshot); case 'replicate': return await this.executeReplicateStrategy(sessionId, snapshot); case 'migrate': return await this.executeMigrateStrategy(sessionId, snapshot); case 'fallback': return await this.executeFallbackStrategy(sessionId, snapshot); case 'prompt-interrupt': return await this.executePromptInterruptStrategy(sessionId, snapshot); case 'prompt-reset': return await this.executePromptResetStrategy(sessionId, snapshot); case 'session-refresh': return await this.executeSessionRefreshStrategy(sessionId, snapshot); case 'command-retry': return await this.executeCommandRetryStrategy(sessionId, snapshot); default: this.logger.warn(`Unknown recovery strategy: ${strategy}`); return false; } } /** * Restart the session with the same configuration */ private async executeRestartStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'restart', message: 'Restarting session with same configuration', }); // Request session restart with original options this.emit('session-restart-request', { sessionId, sessionOptions: snapshot.sessionOptions, restoreState: { workingDirectory: snapshot.workingDirectory, environment: snapshot.environment, }, }); return true; // Assume success - actual verification would come from session manager } catch (error) { this.logger.error( `Restart strategy failed for session ${sessionId}:`, error ); return false; } } /** * Reconnect to existing session (SSH only) */ private async executeReconnectStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { if (!snapshot.sessionOptions.sshOptions) { return false; // Not applicable for local sessions } try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'reconnect', message: 'Attempting to reconnect to SSH session', }); // Request SSH reconnection this.emit('ssh-reconnect-request', { sessionId, sshOptions: snapshot.sessionOptions.sshOptions, }); return true; } catch (error) { this.logger.error( `Reconnect strategy failed for session ${sessionId}:`, error ); return false; } } /** * Restore session state from snapshot */ private async executeRestoreStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'restore', message: 'Restoring session from snapshot data', }); // Restore session state this.emit('session-restore-request', { sessionId, snapshot: { sessionState: snapshot.sessionState, environment: snapshot.environment, workingDirectory: snapshot.workingDirectory, commandHistory: snapshot.commandHistory, }, }); this.stats.sessionsRestored++; this.stats.bytesRestored += this.estimateSnapshotSize(snapshot); return true; } catch (error) { this.logger.error( `Restore strategy failed for session ${sessionId}:`, error ); return false; } } /** * Replicate session on different resources */ private async executeReplicateStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'replicate', message: 'Replicating session on alternative resources', }); // Create new session with replicated state const newSessionId = `${sessionId}-replica-${Date.now()}`; this.emit('session-replicate-request', { originalSessionId: sessionId, newSessionId, sessionOptions: snapshot.sessionOptions, restoreState: snapshot, }); return true; } catch (error) { this.logger.error( `Replicate strategy failed for session ${sessionId}:`, error ); return false; } } /** * Migrate session to different host/resource */ private async executeMigrateStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'migrate', message: 'Migrating session to alternative host', }); // Request session migration this.emit('session-migrate-request', { sessionId, currentSnapshot: snapshot, migrationOptions: { preferredHosts: [], // Would be populated based on configuration resourceRequirements: snapshot.sessionOptions, }, }); return true; } catch (error) { this.logger.error( `Migrate strategy failed for session ${sessionId}:`, error ); return false; } } /** * Execute fallback strategy (last resort) */ private async executeFallbackStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'fallback', message: 'Executing fallback recovery - notifying user and archiving session', }); // Archive session data for manual recovery if (this.config.persistenceEnabled) { const archivePath = path.join( this.config.persistencePath, 'failed-sessions', `${sessionId}-${Date.now()}.archive.json` ); await fs.mkdir(path.dirname(archivePath), { recursive: true }); await fs.writeFile(archivePath, JSON.stringify(snapshot, null, 2)); } // Notify about failed recovery this.emit('recovery-fallback', { sessionId, snapshot, message: 'Session could not be automatically recovered. Data has been archived for manual recovery.', archiveLocation: this.config.persistenceEnabled ? 'failed-sessions' : null, }); return true; // Fallback always "succeeds" by definition } catch (error) { this.logger.error( `Fallback strategy failed for session ${sessionId}:`, error ); return false; } } /** * Start periodic snapshots for a session */ private startPeriodicSnapshots(sessionId: string): void { const timer = setInterval(() => { // Request updated session state this.emit('snapshot-request', { sessionId, callback: (updates?: Partial<RecoverySnapshot>) => { if (updates) { this.updateSessionSnapshot(sessionId, updates); } }, }); }, this.config.snapshotInterval); this.snapshotTimers.set(sessionId, timer); } /** * Persist a snapshot to disk */ private async persistSnapshot(snapshot: RecoverySnapshot): Promise<void> { if (!this.config.persistenceEnabled) return; try { const snapshotPath = path.join( this.config.persistencePath, `${snapshot.sessionId}.snapshot.json` ); await fs.writeFile(snapshotPath, JSON.stringify(snapshot, null, 2)); } catch (error) { this.logger.error( `Failed to persist snapshot for session ${snapshot.sessionId}:`, error ); } } /** * Load persisted snapshots from disk */ private async loadPersistedSnapshots(): Promise<void> { if (!this.config.persistenceEnabled) return; try { const files = await fs.readdir(this.config.persistencePath); const snapshotFiles = files.filter((f) => f.endsWith('.snapshot.json')); for (const file of snapshotFiles) { try { const filePath = path.join(this.config.persistencePath, file); const content = await fs.readFile(filePath, 'utf-8'); const snapshot: RecoverySnapshot = JSON.parse(content); // Convert date strings back to Date objects snapshot.timestamp = new Date(snapshot.timestamp); if (snapshot.errorContext) { snapshot.errorContext.errorTimestamp = new Date( snapshot.errorContext.errorTimestamp ); } this.sessionSnapshots.set(snapshot.sessionId, snapshot); this.logger.debug( `Loaded snapshot for session ${snapshot.sessionId}` ); } catch (error) { this.logger.error(`Failed to load snapshot from ${file}:`, error); } } this.logger.info(`Loaded ${snapshotFiles.length} persisted snapshots`); } catch (error) { this.logger.error('Failed to load persisted snapshots:', error); } } /** * Persist all current snapshots */ private async persistAllSnapshots(): Promise<void> { const snapshots = Array.from(this.sessionSnapshots.values()); await Promise.all( snapshots.map((snapshot) => this.persistSnapshot(snapshot)) ); this.logger.info(`Persisted ${snapshots.length} snapshots`); } /** * Estimate the size of a snapshot in bytes */ private estimateSnapshotSize(snapshot: RecoverySnapshot): number { return JSON.stringify(snapshot).length * 2; // Rough estimate (UTF-16) } /** * Update average recovery time metric */ private updateAverageRecoveryTime(duration: number): void { if (this.stats.successfulRecoveries === 1) { this.stats.averageRecoveryTime = duration; } else { this.stats.averageRecoveryTime = (this.stats.averageRecoveryTime * (this.stats.successfulRecoveries - 1) + duration) / this.stats.successfulRecoveries; } } /** * Get recovery statistics */ getRecoveryStatistics() { return { ...this.stats, activeRecoveries: this.activeRecoveries.size, registeredSessions: this.sessionSnapshots.size, strategiesUsed: Object.fromEntries(this.stats.strategiesUsed), successRate: this.stats.totalRecoveryAttempts > 0 ? (this.stats.successfulRecoveries / this.stats.totalRecoveryAttempts) * 100 : 0, config: this.config, isRunning: this.isRunning, }; } /** * Get recovery history for a specific session */ getSessionRecoveryHistory(sessionId: string): RecoveryAttempt[] { return this.recoveryAttempts.get(sessionId) || []; } /** * Get all registered snapshots */ getAllSnapshots(): Record<string, RecoverySnapshot> { const result: Record<string, RecoverySnapshot> = {}; for (const [sessionId, snapshot] of this.sessionSnapshots) { result[sessionId] = { ...snapshot }; } return result; } /** * Utility method for delays */ private delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Execute prompt interrupt strategy - interrupt stuck interactive prompts */ private async executePromptInterruptStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'prompt-interrupt', message: 'Interrupting stuck interactive prompt', }); // Send interrupt signals to break out of stuck prompts this.emit('session-interrupt-request', { sessionId, interruptType: 'prompt', signals: ['SIGINT', 'CTRL_C', 'ESC'], interactiveState: snapshot.interactiveState, }); return true; } catch (error) { this.logger.error( `Prompt interrupt strategy failed for session ${sessionId}:`, error ); return false; } } /** * Execute prompt reset strategy - reset prompt state and clear buffers */ private async executePromptResetStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'prompt-reset', message: 'Resetting prompt state and clearing buffers', }); // Clear output buffers and reset prompt detection this.emit('session-prompt-reset-request', { sessionId, actions: [ 'clear-output-buffer', 'reset-prompt-detector', 'flush-pending-commands', 'reinitialize-prompt-patterns', ], preserveState: { workingDirectory: snapshot.workingDirectory, environment: snapshot.environment, }, }); return true; } catch (error) { this.logger.error( `Prompt reset strategy failed for session ${sessionId}:`, error ); return false; } } /** * Execute session refresh strategy - refresh session without full restart */ private async executeSessionRefreshStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'session-refresh', message: 'Refreshing session state without full restart', }); // Send refresh command (like newline or space) to re-establish communication this.emit('session-refresh-request', { sessionId, refreshActions: [ 'send-newline', 'check-responsiveness', 'verify-prompt', 'restore-context', ], timeout: 10000, fallbackToRestart: true, preserveState: snapshot.interactiveState, }); return true; } catch (error) { this.logger.error( `Session refresh strategy failed for session ${sessionId}:`, error ); return false; } } /** * Execute command retry strategy - retry failed commands with exponential backoff */ private async executeCommandRetryStrategy( sessionId: string, snapshot: RecoverySnapshot ): Promise<boolean> { try { this.emit('recovery-strategy-attempt', { sessionId, strategy: 'command-retry', message: 'Retrying failed commands with intelligent backoff', }); const interactiveState = snapshot.interactiveState; if (!interactiveState?.pendingCommands.length) { this.logger.info( `No pending commands to retry for session ${sessionId}` ); return true; // Success - nothing to retry } // Retry pending commands with exponential backoff this.emit('session-command-retry-request', { sessionId, commands: interactiveState.pendingCommands, retryConfig: { maxRetries: 3, baseDelay: 1000, backoffMultiplier: 2, maxDelay: 10000, jitter: true, }, verification: { checkPrompt: true, timeoutPerCommand: 15000, verifyOutput: true, }, }); return true; } catch (error) { this.logger.error( `Command retry strategy failed for session ${sessionId}:`, error ); return false; } } /** * Update interactive state for a session */ async updateInteractiveState( sessionId: string, interactiveUpdates: Partial<RecoverySnapshot['interactiveState']> ): Promise<void> { const snapshot = this.sessionSnapshots.get(sessionId); if (!snapshot) { return; } // Initialize interactive state if it doesn't exist if (!snapshot.interactiveState) { snapshot.interactiveState = { isInteractive: false, pendingCommands: [], sessionUnresponsive: false, timeoutCount: 0, }; } // Update interactive state Object.assign(snapshot.interactiveState, interactiveUpdates); snapshot.timestamp = new Date(); // Persist if enabled if (this.config.persistenceEnabled) { await this.persistSnapshot(snapshot); } this.emit('interactive-state-updated', { sessionId, interactiveState: snapshot.interactiveState, }); } /** * Check if session needs interactive prompt recovery */ shouldTriggerInteractiveRecovery(sessionId: string): { shouldTrigger: boolean; reason?: string; urgency: 'low' | 'medium' | 'high'; } { const snapshot = this.sessionSnapshots.get(sessionId); if (!snapshot?.interactiveState) { return { shouldTrigger: false, urgency: 'low' }; } const state = snapshot.interactiveState; const now = Date.now(); // Check for timeout conditions if (state.sessionUnresponsive) { const unresponsiveTime = state.lastSuccessfulCommand ? now - state.lastSuccessfulCommand.getTime() : 0; if (unresponsiveTime > 30000) { // 30 seconds return { shouldTrigger: true, reason: 'Session unresponsive for extended period', urgency: 'high', }; } } // Check for excessive timeouts if (state.timeoutCount >= 3) { return { shouldTrigger: true, reason: 'Multiple consecutive timeouts detected', urgency: 'high', }; } // Check for stuck interactive prompts if (state.isInteractive && state.lastPromptDetected) { const promptAge = now - state.lastPromptDetected.getTime(); if (promptAge > 60000) { // 1 minute return { shouldTrigger: true, reason: 'Interactive prompt appears stuck', urgency: 'medium', }; } } // Check for pending commands that haven't been processed if (state.pendingCommands.length > 0) { const commandAge = state.lastSuccessfulCommand ? now - state.lastSuccessfulCommand.getTime() : now; if (commandAge > 45000) { // 45 seconds return { shouldTrigger: true, reason: 'Pending commands not processing', urgency: 'medium', }; } } return { shouldTrigger: false, urgency: 'low' }; } /** * Get interactive recovery statistics */ getInteractiveRecoveryStats(): { totalInteractiveSessions: number; sessionsWithTimeouts: number; unresponsiveSessions: number; averageTimeoutCount: number; successfulPromptInterrupts: number; successfulPromptResets: number; } { const snapshots = Array.from(this.sessionSnapshots.values()); const interactiveSessions = snapshots.filter( (s) => s.interactiveState?.isInteractive ); const sessionsWithTimeouts = interactiveSessions.filter( (s) => s.interactiveState!.timeoutCount > 0 ); const unresponsiveSessions = interactiveSessions.filter( (s) => s.interactiveState!.sessionUnresponsive ); const totalTimeouts = interactiveSessions.reduce( (sum, s) => sum + (s.interactiveState!.timeoutCount || 0), 0 ); const averageTimeoutCount = interactiveSessions.length > 0 ? totalTimeouts / interactiveSessions.length : 0; // Get strategy usage stats const promptInterrupts = this.stats.strategiesUsed.get('prompt-interrupt') || 0; const promptResets = this.stats.strategiesUsed.get('prompt-reset') || 0; return { totalInteractiveSessions: interactiveSessions.length, sessionsWithTimeouts: sessionsWithTimeouts.length, unresponsiveSessions: unresponsiveSessions.length, averageTimeoutCount, successfulPromptInterrupts: promptInterrupts, successfulPromptResets: promptResets, }; } /** * Clean up resources */ async destroy(): Promise<void> { await this.stop(); this.sessionSnapshots.clear(); this.recoveryAttempts.clear(); this.removeAllListeners(); this.logger.info('SessionRecovery destroyed'); } }

Latest Blog Posts

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/ooples/mcp-console-automation'

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