Files
SuperCharged-Claude-Code-Up…/services/chat-monitor.js
uroma 55aafbae9a Fix project isolation: Make loadChatHistory respect active project sessions
- 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>
2026-01-22 14:43:05 +00:00

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;