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

414 lines 17.2 kB
/** * Session Registry - Multi-Project Resource Coordination * * Central coordinator for managing multiple concurrent debugging sessions * with automatic resource isolation and intelligent cleanup. */ import { EventEmitter } from 'events'; import * as crypto from 'crypto'; import * as path from 'path'; import * as fs from 'fs/promises'; import { existsSync } from 'fs'; import { DEFAULT_CONFIG } from './types.js'; import { PortAllocator } from './port-allocator.js'; import { ResourceMonitor } from './resource-monitor.js'; import { CleanupCoordinator } from './cleanup-coordinator.js'; import { PersistentSessionStorage } from './persistent-storage.js'; export class SessionRegistry extends EventEmitter { sessions; workingDirToSession; // workingDir -> sessionId portAllocator; resourceMonitor; cleanupCoordinator; config; cleanupInterval; monitoringInterval; persistentStorage; initialized; constructor(config = {}) { super(); this.sessions = new Map(); this.workingDirToSession = new Map(); this.config = { ...DEFAULT_CONFIG, ...config }; this.portAllocator = new PortAllocator(this.config.basePort, this.config.portRangeSize); this.resourceMonitor = new ResourceMonitor(this.config.monitoringIntervalMs); this.cleanupCoordinator = new CleanupCoordinator(this.config.tempBaseDirectory); this.persistentStorage = new PersistentSessionStorage(); // Initialize with persistent sessions this.initialized = this.initializeFromStorage(); this.startBackgroundTasks(); } /** * Initialize session registry from persistent storage */ async initializeFromStorage() { try { const { sessions, workingDirToSession } = await this.persistentStorage.loadSessions(); this.sessions = sessions; this.workingDirToSession = workingDirToSession; if (sessions.size > 0) { console.log(`📂 Restored ${sessions.size} persistent debugging sessions`); } } catch (error) { console.warn('⚠️ Failed to initialize from persistent storage:', error instanceof Error ? error.message : String(error)); } } /** * Save current sessions to persistent storage */ async saveToStorage() { try { await this.persistentStorage.saveSessions(this.sessions, this.workingDirToSession); } catch (error) { console.warn('⚠️ Failed to save sessions to storage:', error instanceof Error ? error.message : String(error)); } } /** * Register a new project debugging session */ async registerProject(workingDirectory, targetUrl) { // Ensure we're initialized first await this.initialized; const normalizedWorkingDir = path.resolve(workingDirectory); // Check if project already has an active session const existingSessionId = this.workingDirToSession.get(normalizedWorkingDir); if (existingSessionId) { const existingSession = this.sessions.get(existingSessionId); if (existingSession && existingSession.status !== 'cleanup') { // Update last activity and return existing session existingSession.lastActivity = new Date(); existingSession.status = 'active'; // Save updated session to persistent storage this.saveToStorage().catch(error => { console.warn('⚠️ Failed to persist session update:', error instanceof Error ? error.message : String(error)); }); this.emitEvent('updated', existingSession.id, { reason: 'reactivated' }); return existingSession; } } // Check global session limit const activeSessions = Array.from(this.sessions.values()) .filter(s => s.status === 'active' || s.status === 'initializing'); if (activeSessions.length >= this.config.defaultQuotas.maxConcurrentSessions) { throw new Error(`Maximum concurrent sessions (${this.config.defaultQuotas.maxConcurrentSessions}) reached. ` + 'Consider closing idle sessions or increasing quotas.'); } // Create new session const sessionId = this.generateProjectId(normalizedWorkingDir); const assignedPorts = await this.portAllocator.allocatePortRange(sessionId); const tempDirectory = await this.cleanupCoordinator.createProjectTempDir(sessionId); const metadata = await this.detectProjectMetadata(normalizedWorkingDir); const session = { id: sessionId, workingDirectory: normalizedWorkingDir, targetUrl, assignedPorts, browserInstanceId: `browser_${sessionId}`, tempDirectory, resourceUsage: { browserProcesses: 0, memoryUsageMB: 0, cpuUsagePercent: 0, tempDirectorySize: 0, lastActivity: new Date() }, quotas: { ...this.config.defaultQuotas }, lastActivity: new Date(), createdAt: new Date(), status: 'initializing', metadata }; this.sessions.set(sessionId, session); this.workingDirToSession.set(normalizedWorkingDir, sessionId); // Save to persistent storage this.saveToStorage().catch(error => { console.warn('⚠️ Failed to persist new session:', error instanceof Error ? error.message : String(error)); }); this.emitEvent('created', sessionId, { workingDirectory: normalizedWorkingDir, targetUrl }); // Update status to active after initialization setTimeout(() => { if (this.sessions.has(sessionId)) { session.status = 'active'; this.emitEvent('updated', sessionId, { status: 'active' }); } }, 1000); return session; } /** * Get existing project session by working directory */ async getProjectSession(workingDirectory) { const normalizedWorkingDir = path.resolve(workingDirectory); const sessionId = this.workingDirToSession.get(normalizedWorkingDir); if (!sessionId) { return null; } const session = this.sessions.get(sessionId); if (!session || session.status === 'cleanup') { return null; } // Update last activity session.lastActivity = new Date(); return session; } /** * Get session by ID */ getSession(sessionId) { return this.sessions.get(sessionId) || null; } /** * Update session resource usage */ async updateResourceUsage(sessionId, metrics) { const session = this.sessions.get(sessionId); if (!session) { return; } session.resourceUsage = { ...session.resourceUsage, ...metrics, lastActivity: new Date() }; session.lastActivity = new Date(); // Check for quota violations await this.enforceQuotas(session); } /** * Get comprehensive resource summary */ async getResourceSummary() { const sessions = Array.from(this.sessions.values()); const activeSessions = sessions.filter(s => s.status === 'active'); const idleSessions = sessions.filter(s => s.status === 'idle'); const totalMemoryUsage = sessions.reduce((sum, s) => sum + s.resourceUsage.memoryUsageMB, 0); const totalBrowserProcesses = sessions.reduce((sum, s) => sum + s.resourceUsage.browserProcesses, 0); const availableResources = await this.resourceMonitor.getSystemResources(); const quotaUtilization = this.calculateQuotaUtilization(sessions); return { totalSessions: sessions.length, activeSessions: activeSessions.length, idleSessions: idleSessions.length, totalMemoryUsage, totalBrowserProcesses, availableResources, quotaUtilization }; } /** * Get optimization suggestions */ async getOptimizationSuggestions() { const suggestions = []; const sessions = Array.from(this.sessions.values()); const now = new Date(); // Check for idle sessions that should be cleaned up for (const session of sessions) { const idleMinutes = (now.getTime() - session.lastActivity.getTime()) / (1000 * 60); if (session.status === 'idle' && idleMinutes > session.quotas.maxIdleTimeMinutes) { suggestions.push({ type: 'cleanup', priority: 'medium', projectId: session.id, description: `Session idle for ${Math.round(idleMinutes)} minutes, consider cleanup`, estimatedSavings: { memoryUsageMB: session.resourceUsage.memoryUsageMB }, actionRequired: false }); } // Check for quota violations if (session.resourceUsage.memoryUsageMB > session.quotas.maxMemoryMB) { suggestions.push({ type: 'upgrade_quota', priority: 'high', projectId: session.id, description: `Memory usage (${session.resourceUsage.memoryUsageMB}MB) exceeds quota (${session.quotas.maxMemoryMB}MB)`, estimatedSavings: {}, actionRequired: true }); } } // Check for resource consolidation opportunities const activeSessionsCount = sessions.filter(s => s.status === 'active').length; if (activeSessionsCount > 3 && this.config.enableSharedBrowsers) { suggestions.push({ type: 'share_resources', priority: 'low', description: `${activeSessionsCount} active sessions could benefit from browser instance sharing`, estimatedSavings: { memoryUsageMB: 100 * Math.floor(activeSessionsCount / 2) }, actionRequired: false }); } return suggestions.sort((a, b) => this.getPriorityWeight(b.priority) - this.getPriorityWeight(a.priority)); } /** * Cleanup idle sessions */ async cleanupIdleSessions() { const cleanedSessions = []; const now = new Date(); for (const [sessionId, session] of this.sessions) { const idleMinutes = (now.getTime() - session.lastActivity.getTime()) / (1000 * 60); if (session.status === 'idle' && idleMinutes > session.quotas.maxIdleTimeMinutes) { await this.cleanupSession(sessionId); cleanedSessions.push(sessionId); } } return cleanedSessions; } /** * Cleanup specific session */ async cleanupSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { return; } session.status = 'cleanup'; this.emitEvent('cleanup', sessionId, { reason: 'manual_cleanup' }); try { // Release allocated ports await this.portAllocator.releasePortRange(sessionId); // Cleanup project temp directory await this.cleanupCoordinator.cleanupProject(sessionId); // Remove from tracking maps this.workingDirToSession.delete(session.workingDirectory); this.sessions.delete(sessionId); // Save to persistent storage this.saveToStorage().catch(error => { console.warn('⚠️ Failed to persist session cleanup:', error instanceof Error ? error.message : String(error)); }); this.emitEvent('cleanup', sessionId, { reason: 'cleanup_complete' }); } catch (error) { this.emitEvent('error', sessionId, { error: error instanceof Error ? error.message : String(error), phase: 'cleanup' }); throw error; } } /** * Emergency cleanup - clean up all sessions */ async emergencyCleanup() { const sessionIds = Array.from(this.sessions.keys()); this.emitEvent('cleanup', 'all', { reason: 'emergency', sessionCount: sessionIds.length }); await Promise.allSettled(sessionIds.map(sessionId => this.cleanupSession(sessionId))); await this.cleanupCoordinator.emergencyCleanup(); } /** * Shutdown the registry */ async shutdown() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } if (this.monitoringInterval) { clearInterval(this.monitoringInterval); } await this.emergencyCleanup(); this.removeAllListeners(); } // Private methods generateProjectId(workingDirectory) { const hash = crypto.createHash('sha256') .update(workingDirectory) .digest('hex') .substring(0, 8); return `proj_${hash}_${Date.now()}`; } async detectProjectMetadata(workingDirectory) { const metadata = {}; try { // Try to read package.json const packageJsonPath = path.join(workingDirectory, 'package.json'); if (existsSync(packageJsonPath)) { const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); metadata.packageJson = JSON.parse(packageJsonContent); // Extract key dependencies for framework detection const deps = { ...metadata.packageJson.dependencies, ...metadata.packageJson.devDependencies }; if (deps.react) metadata.framework = 'react'; else if (deps.vue) metadata.framework = 'vue'; else if (deps.angular) metadata.framework = 'angular'; else if (deps.phoenix) metadata.framework = 'phoenix'; else if (deps.next) metadata.framework = 'nextjs'; metadata.dependencies = Object.keys(deps); } } catch (error) { // Ignore metadata detection errors } return metadata; } async enforceQuotas(session) { const violations = []; if (session.resourceUsage.memoryUsageMB > session.quotas.maxMemoryMB) { violations.push(`Memory usage (${session.resourceUsage.memoryUsageMB}MB) exceeds quota (${session.quotas.maxMemoryMB}MB)`); } if (session.resourceUsage.browserProcesses > session.quotas.maxBrowserInstances) { violations.push(`Browser instances (${session.resourceUsage.browserProcesses}) exceed quota (${session.quotas.maxBrowserInstances})`); } if (violations.length > 0) { this.emitEvent('resource_warning', session.id, { violations, severity: 'warning' }); } } calculateQuotaUtilization(sessions) { if (sessions.length === 0) return 0; const totalMemoryUsed = sessions.reduce((sum, s) => sum + s.resourceUsage.memoryUsageMB, 0); const totalMemoryQuota = sessions.reduce((sum, s) => sum + s.quotas.maxMemoryMB, 0); return totalMemoryQuota > 0 ? (totalMemoryUsed / totalMemoryQuota) * 100 : 0; } getPriorityWeight(priority) { switch (priority) { case 'critical': return 4; case 'high': return 3; case 'medium': return 2; case 'low': return 1; default: return 0; } } startBackgroundTasks() { // Background cleanup task this.cleanupInterval = setInterval(async () => { try { await this.cleanupIdleSessions(); } catch (error) { this.emitEvent('error', 'background', { error: error instanceof Error ? error.message : String(error), task: 'cleanup' }); } }, this.config.cleanupIntervalMs); // Background monitoring task this.monitoringInterval = setInterval(async () => { try { const summary = await this.getResourceSummary(); this.emit('resource_summary', summary); } catch (error) { this.emitEvent('error', 'background', { error: error instanceof Error ? error.message : String(error), task: 'monitoring' }); } }, this.config.monitoringIntervalMs); } emitEvent(type, projectId, data) { const event = { type, projectId, timestamp: new Date(), data, severity: data.severity || 'info' }; this.emit('session_event', event); this.emit(type, event); } } //# sourceMappingURL=session-registry.js.map