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