UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

308 lines • 13.1 kB
/** * Session Stability Manager - P0 Fix for Session Disconnect Issues * * Addresses critical feedback: * - Sessions consistently disconnect after 20-40 seconds * - "Session is unhealthy or disconnected" errors * - Need automatic session recovery/reconnection */ import { ProjectSessionManager } from '../utils/project-session-manager.js'; import { lazyBrowser } from '../utils/lazy-dependencies.js'; export class SessionStabilityManager { static healthChecks = new Map(); static recoveryInProgress = new Set(); static healthMonitorInterval = null; // Default recovery settings optimized for Phoenix LiveView static DEFAULT_RECOVERY_OPTIONS = { maxRetries: 3, retryDelayMs: 1000, preserveUrl: true, restoreContext: true, timeoutMs: 5000 }; /** * P0 FIX: Enhanced session health validation with auto-recovery * Replaces the timeout-prone validateSessionHealth in SessionDebugFixes */ static async validateSessionWithRecovery(session, sessions, options = {}) { const sessionId = session.id || session.sessionId; const recoveryOptions = { ...this.DEFAULT_RECOVERY_OPTIONS, ...options }; // Quick structural check first if (!sessionId || !session.url) { return { isHealthy: false, session, recovered: false }; } // Fast browser connection check (no page.title() timeout) const healthStatus = await this.checkSessionHealth(session, sessionId); if (healthStatus.isHealthy) { this.updateHealthStatus(sessionId, healthStatus); return { isHealthy: true, session, recovered: false }; } // Attempt recovery if session is unhealthy if (healthStatus.canRecover && !this.recoveryInProgress.has(sessionId)) { console.log(`🔄 Attempting automatic recovery for session ${sessionId}: ${healthStatus.reason}`); const recoveredSession = await this.recoverSession(session, sessions, recoveryOptions); if (recoveredSession) { console.log(`✅ Session ${sessionId} recovered successfully`); return { isHealthy: true, session: recoveredSession, recovered: true }; } } return { isHealthy: false, session, recovered: false }; } /** * Fast, non-blocking session health check * Avoids timeout issues with page.title() calls */ static async checkSessionHealth(session, sessionId) { const now = new Date(); try { // Basic structure validation if (!session.page) { return { isHealthy: false, reason: 'No page object in session', canRecover: true, lastActivity: now, connectionAttempts: 0, browserConnected: false, pageResponsive: false }; } // Fast browser context check let browserConnected = false; let pageResponsive = false; try { // Quick check - if this throws, browser is disconnected const context = session.page.context ? session.page.context() : null; browserConnected = !!context; if (browserConnected) { // Fast responsiveness check with timeout const titlePromise = session.page.title(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Page timeout')), 2000); }); await Promise.race([titlePromise, timeoutPromise]); pageResponsive = true; } } catch (error) { // Browser/page is disconnected - this is recoverable } const isHealthy = browserConnected && pageResponsive; return { isHealthy, reason: !browserConnected ? 'Browser disconnected' : !pageResponsive ? 'Page unresponsive' : 'Healthy', canRecover: true, // Most issues are recoverable lastActivity: now, connectionAttempts: this.healthChecks.get(sessionId)?.connectionAttempts || 0, browserConnected, pageResponsive }; } catch (error) { return { isHealthy: false, reason: `Health check failed: ${error instanceof Error ? error.message : String(error)}`, canRecover: true, lastActivity: now, connectionAttempts: this.healthChecks.get(sessionId)?.connectionAttempts || 0, browserConnected: false, pageResponsive: false }; } } /** * P0 FIX: Automatic session recovery * Reconnects browser and restores session context without losing state */ static async recoverSession(session, sessions, options) { const sessionId = session.id || session.sessionId; if (this.recoveryInProgress.has(sessionId)) { return null; // Recovery already in progress } this.recoveryInProgress.add(sessionId); try { // Step 1: Save current session context const context = { url: session.url, startTime: session.startTime, framework: session.framework, events: session.events || [], metadata: session.metadata || {} }; // Step 2: Attempt browser reconnection const projectSessionManager = new ProjectSessionManager(); for (let attempt = 1; attempt <= options.maxRetries; attempt++) { try { console.log(`🔄 Recovery attempt ${attempt}/${options.maxRetries} for session ${sessionId}`); // Get fresh browser connection const browserConnection = await projectSessionManager.getBrowserConnection(); if (browserConnection?.endpoint) { const playwright = await lazyBrowser.getPlaywright(); const browser = await playwright.chromium.connectOverCDP(browserConnection.endpoint); // Create or reuse page let page; const pages = browser.pages(); if (pages.length > 0) { page = pages[0]; } else { page = await browser.newPage(); } // Navigate to preserved URL if needed if (options.preserveUrl && context.url) { const currentUrl = await page.url().catch(() => ''); if (currentUrl !== context.url) { await page.goto(context.url, { waitUntil: 'networkidle', timeout: options.timeoutMs }); } } // Reconstruct session object const recoveredSession = { id: sessionId, sessionId, browserContext: browser, page, url: context.url, framework: context.framework, startTime: context.startTime, events: context.events, metadata: context.metadata, recovered: true, recoveredAt: new Date().toISOString() }; // Update sessions map sessions.set(sessionId, recoveredSession); // Update health status this.updateHealthStatus(sessionId, { isHealthy: true, reason: 'Recovered successfully', canRecover: true, lastActivity: new Date(), connectionAttempts: attempt, browserConnected: true, pageResponsive: true }); return recoveredSession; } } catch (error) { console.warn(`Recovery attempt ${attempt} failed:`, error); if (attempt < options.maxRetries) { await new Promise(resolve => setTimeout(resolve, options.retryDelayMs)); } } } return null; // All recovery attempts failed } finally { this.recoveryInProgress.delete(sessionId); } } /** * P0 FIX: Proactive session health monitoring * Monitors sessions and attempts recovery before users encounter issues */ static startHealthMonitoring(sessions, intervalMs = 10000) { if (this.healthMonitorInterval) { clearInterval(this.healthMonitorInterval); } this.healthMonitorInterval = setInterval(async () => { for (const [sessionId, session] of sessions) { try { const health = await this.checkSessionHealth(session, sessionId); this.updateHealthStatus(sessionId, health); // Proactive recovery for degraded sessions if (!health.isHealthy && health.canRecover && !this.recoveryInProgress.has(sessionId)) { console.log(`🔄 Proactive recovery triggered for session ${sessionId}`); await this.recoverSession(session, sessions, this.DEFAULT_RECOVERY_OPTIONS); } } catch (error) { console.error(`Health monitoring error for session ${sessionId}:`, error); } } }, intervalMs); console.log(`✅ Session health monitoring started (${intervalMs}ms interval)`); } static stopHealthMonitoring() { if (this.healthMonitorInterval) { clearInterval(this.healthMonitorInterval); this.healthMonitorInterval = null; console.log('🛑 Session health monitoring stopped'); } } /** * Get health status for debugging */ static getSessionHealthStatus(sessionId) { return this.healthChecks.get(sessionId) || null; } /** * Get health overview for all sessions */ static getHealthOverview() { const details = new Map(this.healthChecks); const healthySessions = Array.from(details.values()).filter(h => h.isHealthy).length; return { totalSessions: details.size, healthySessions, unhealthySessions: details.size - healthySessions, recoveringSessions: this.recoveryInProgress.size, details }; } static updateHealthStatus(sessionId, status) { this.healthChecks.set(sessionId, { ...status, lastActivity: new Date() }); } /** * Enhanced error response with recovery context */ static createRecoveryErrorResponse(sessionId, error, recoveryAttempted = false) { const health = this.getSessionHealthStatus(sessionId); return { success: false, error: error.message, sessionId, timestamp: new Date().toISOString(), recovery: { attempted: recoveryAttempted, canRecover: health?.canRecover || false, healthStatus: health?.reason || 'Unknown', connectionAttempts: health?.connectionAttempts || 0, lastActivity: health?.lastActivity?.toISOString() || null }, troubleshooting: [ 'Session health monitoring is active', recoveryAttempted ? 'Automatic recovery was attempted - try again in a few seconds' : 'Automatic recovery will be attempted shortly', 'Check if the target URL is still accessible', 'Verify no browser tabs were manually closed' ] }; } } //# sourceMappingURL=session-stability-manager.js.map