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