- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
265 lines
7.4 KiB
JavaScript
265 lines
7.4 KiB
JavaScript
/**
|
|
* 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;
|