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>
This commit is contained in:
264
services/chat-monitor.js
Normal file
264
services/chat-monitor.js
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user