/** * Real-Time Chat Monitor Service * * Monitors agentic chat behavior and detects failures in real-time. * Triggers auto-fix pipeline when issues are detected. */ const EventEmitter = require('events'); const fs = require('fs'); const path = require('path'); class ChatMonitorService extends EventEmitter { constructor() { super(); this.activeMonitors = new Map(); this.logsPath = path.join(__dirname, '../logs/chat-monitor'); this.ensureLogsDirectory(); this.startAutoFixListener(); } ensureLogsDirectory() { if (!fs.existsSync(this.logsPath)) { fs.mkdirSync(this.logsPath, { recursive: true }); } } /** * Start monitoring a session */ startSessionMonitor(sessionId) { if (this.activeMonitors.has(sessionId)) { console.log(`[ChatMonitor] Session ${sessionId} already monitored`); return; } const monitor = { sessionId, startTime: Date.now(), events: [], lastActivity: Date.now(), state: 'monitoring' }; this.activeMonitors.set(sessionId, monitor); console.log(`[ChatMonitor] Started monitoring session ${sessionId}`); // Set up timeout for response detection this.setupResponseTimeout(sessionId); } /** * Log an event for a session */ logEvent(sessionId, eventType, data) { const monitor = this.activeMonitors.get(sessionId); if (!monitor) { this.startSessionMonitor(sessionId); return this.logEvent(sessionId, eventType, data); } const event = { timestamp: Date.now(), eventType, data, sessionId }; monitor.events.push(event); monitor.lastActivity = Date.now(); // Write to log file this.writeLog(sessionId, event); // Detect failures based on event type this.detectFailures(sessionId, event); console.log(`[ChatMonitor] [${sessionId}] ${eventType}:`, JSON.stringify(data).substring(0, 100)); } /** * Write event to log file */ writeLog(sessionId, event) { const date = new Date().toISOString().split('T')[0]; const logFile = path.join(this.logsPath, `${date}-${sessionId}.log`); const logLine = `[${new Date(event.timestamp).toISOString()}] [${event.eventType}] ${JSON.stringify(event.data)}\n`; fs.appendFileSync(logFile, logLine, 'utf-8'); } /** * Set up response timeout detection */ setupResponseTimeout(sessionId) { // Check for response after 30 seconds setTimeout(() => { this.checkResponseReceived(sessionId); }, 30000); } /** * Check if response was received */ checkResponseReceived(sessionId) { const monitor = this.activeMonitors.get(sessionId); if (!monitor) return; const hasUserMessage = monitor.events.some(e => e.eventType === 'user_message'); const hasAIResponse = monitor.events.some(e => e.eventType === 'ai_response'); if (hasUserMessage && !hasAIResponse) { this.logEvent(sessionId, 'failure_detected', { reason: 'no_ai_response', message: 'User message sent but no AI response received within 30 seconds' }); this.emit('chat-failure', { sessionId, failureType: 'no_response', events: monitor.events }); } } /** * Detect failures based on events */ detectFailures(sessionId, event) { switch (event.eventType) { case 'user_message_sent': // Expect AI response within 30 seconds break; case 'claude_spawn_error': this.emit('chat-failure', { sessionId, failureType: 'claude_spawn_failed', error: event.data.error }); break; case 'json_parse_error': this.emit('chat-failure', { sessionId, failureType: 'json_parse_failed', error: event.data.error }); break; case 'sse_emit_error': this.emit('chat-failure', { sessionId, failureType: 'sse_emit_failed', error: event.data.error }); break; case 'browser_error': // Analyze browser errors - only trigger on actual errors, not info logs const msg = event.data.message || ''; const type = event.data.type || ''; // Only trigger on actual error types or SSE connection failures const isError = type === 'console-error' || type === 'console-warn' || type === 'uncaughterror' || msg.includes('EventSource failed') || msg.includes('SSE connection failed') || msg.includes('Connection lost') || msg.includes('Failed to connect'); // Don't trigger on informational SSE logs (these indicate SSE is working!) if (isError && (msg.includes('SSE') || msg.includes('EventSource'))) { this.emit('chat-failure', { sessionId, failureType: 'browser_sse_error', error: event.data.message }); } break; } } /** * Start auto-fix listener */ startAutoFixListener() { this.on('chat-failure', async (failure) => { console.log(`[ChatMonitor] 💥 Failure detected in session ${failure.sessionId}:`, failure.failureType); this.logEvent(failure.sessionId, 'auto_fix_triggered', failure); // Trigger auto-fix agent await this.triggerAutoFix(failure); }); } /** * Trigger auto-fix agent */ async triggerAutoFix(failure) { console.log(`[ChatMonitor] 🔧 Triggering auto-fix for ${failure.failureType}`); // Write failure to file for auto-fix agent to process const failureFile = path.join(this.logsPath, `failure-${Date.now()}.json`); fs.writeFileSync(failureFile, JSON.stringify(failure, null, 2)); // Launch auto-fix via background process const { spawn } = require('child_process'); const autoFix = spawn('node', [__dirname + '../scripts/auto-fix.js', failureFile], { detached: true, stdio: 'ignore' }); autoFix.unref(); console.log(`[ChatMonitor] 🚀 Auto-fix agent launched: ${autoFix.pid}`); } /** * Stop monitoring a session */ stopSessionMonitor(sessionId) { const monitor = this.activeMonitors.get(sessionId); if (!monitor) return; monitor.state = 'stopped'; monitor.endTime = Date.now(); monitor.duration = monitor.endTime - monitor.startTime; // Write summary this.writeSummary(sessionId, monitor); this.activeMonitors.delete(sessionId); console.log(`[ChatMonitor] Stopped monitoring session ${sessionId}`); } /** * Write monitoring summary */ writeSummary(sessionId, monitor) { const summaryPath = path.join(this.logsPath, `${sessionId}-summary.json`); fs.writeFileSync(summaryPath, JSON.stringify(monitor, null, 2)); } /** * Get monitor status for all sessions */ getStatus() { return { activeMonitors: this.activeMonitors.size, sessions: Array.from(this.activeMonitors.values()).map(m => ({ sessionId: m.sessionId, state: m.state, eventsCount: m.events.length, duration: Date.now() - m.startTime })) }; } } // Singleton instance const chatMonitor = new ChatMonitorService(); module.exports = chatMonitor;