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
JavaScript
/**
* 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