/** * Auto-Fix Agent for Agentic Chat * * Automatically diagnoses and fixes common issues with agentic chat. * Triggered by ChatMonitor when failures are detected. */ const fs = require('fs'); const path = require('path'); const { spawn, exec } = require('child_process'); class AutoFixAgent { constructor(failureFile) { this.failureFile = failureFile; this.failure = this.loadFailure(); this.logsDir = path.join(__dirname, '../logs/chat-monitor'); } loadFailure() { const content = fs.readFileSync(this.failureFile, 'utf-8'); return JSON.parse(content); } async run() { console.log(`[AutoFix] 🔧 Starting auto-fix for session ${this.failure.sessionId}`); console.log(`[AutoFix] Failure type: ${this.failure.failureType}`); const diagnosis = await this.diagnose(); console.log(`[AutoFix] Diagnosis:`, diagnosis); const fix = await this.applyFix(diagnosis); console.log(`[AutoFix] Fix applied:`, fix); const verification = await this.verify(fix); console.log(`[AutoFix] Verification:`, verification); // Write results this.writeResults({ diagnosis, fix, verification }); } async diagnose() { const { sessionId, failureType, events } = this.failure; const diagnosis = { sessionId, failureType, rootCause: null, fixes: [] }; // Check each potential issue diagnosis.checks = { sessionExists: await this.checkSessionExists(sessionId), serverRunning: await this.checkServerRunning(), claudeWorking: await this.checkClaudeCLI(), sseWorking: await this.checkSSE(), frontendConnected: await this.checkFrontendConnection(sessionId) }; // Determine root cause if (!diagnosis.checks.serverRunning) { diagnosis.rootCause = 'server_not_running'; diagnosis.fixes.push('restart_server'); } else if (!diagnosis.checks.sessionExists) { diagnosis.rootCause = 'session_not_found'; diagnosis.fixes.push('create_session'); } else if (!diagnosis.checks.claudeWorking) { diagnosis.rootCause = 'claude_cli_failed'; diagnosis.fixes.push('check_claude_installation'); } else if (!diagnosis.checks.sseWorking) { diagnosis.rootCause = 'sse_broken'; diagnosis.fixes.push('fix_sse_routes'); } else if (!diagnosis.checks.frontendConnected) { diagnosis.rootCause = 'frontend_not_receiving'; diagnosis.fixes.push('check_frontend_sse_client'); } // Event-based diagnosis if (events) { const spawnError = events.find(e => e.eventType === 'claude_spawn_error'); if (spawnError) { diagnosis.rootCause = 'claude_spawn_failed'; diagnosis.fixes.push('check_claude_command'); } const parseError = events.find(e => e.eventType === 'json_parse_error'); if (parseError) { diagnosis.rootCause = 'json_parse_failed'; diagnosis.fixes.push('fix_json_parsing'); } } return diagnosis; } async checkSessionExists(sessionId) { return new Promise((resolve) => { exec(`curl -s http://localhost:3010/claude/api/session/${sessionId}/status`, (error, stdout) => { if (error) { resolve(false); } else { try { const data = JSON.parse(stdout); resolve(data.sessionId === sessionId); } catch { resolve(false); } } }); }); } async checkServerRunning() { return new Promise((resolve) => { exec('pgrep -f "node.*server.js"', (error) => { resolve(!error); }); }); } async checkClaudeCLI() { return new Promise((resolve) => { exec('which claude', (error) => { resolve(!error); }); }); } async checkSSE() { return new Promise((resolve) => { exec('curl -s http://localhost:3010/claude/api/session/test-session/events', (error) => { // SSE endpoints return 200 even if session doesn't exist resolve(!error || error.code !== 'ECONNREFUSED'); }); }); } async checkFrontendConnection(sessionId) { // Check if frontend has received SSE events const logFile = path.join(this.logsDir, `${new Date().toISOString().split('T')[0]}-${sessionId}.log`); if (!fs.existsSync(logFile)) { return false; } const content = fs.readFileSync(logFile, 'utf-8'); return content.includes('ai_response'); } async applyFix(diagnosis) { const fixResults = []; for (const fix of diagnosis.fixes) { console.log(`[AutoFix] Applying fix: ${fix}`); const result = await this.applySingleFix(fix, diagnosis); fixResults.push({ fix, result }); } return { fixes: fixResults }; } async applySingleFix(fix, diagnosis) { switch (fix) { case 'restart_server': return this.restartServer(); case 'create_session': return this.createSession(diagnosis.sessionId); case 'check_claude_installation': return this.checkClaudeInstallation(); case 'fix_sse_routes': return this.fixSSERoutes(); case 'check_frontend_sse_client': return this.checkFrontendSSEClient(); case 'fix_json_parsing': return this.fixJSONParsing(); default: return { success: false, message: `Unknown fix: ${fix}` }; } } async restartServer() { return new Promise((resolve) => { exec('pkill -f "node.*server.js" && sleep 2 && cd /home/uroma/obsidian-web-interface && node server.js > /tmp/server.log 2>&1 &', (error, stdout, stderr) => { if (error) { resolve({ success: false, error: error.message }); } else { setTimeout(() => { resolve({ success: true, message: 'Server restarted' }); }, 3000); } } ); }); } async createSession(sessionId) { // Can't create a session with a specific ID, need to create new one return { success: false, message: 'Cannot create session with specific ID. User should create new session.' }; } async checkClaudeInstallation() { return new Promise((resolve) => { exec('claude --version', (error, stdout) => { if (error) { resolve({ success: false, error: 'Claude CLI not found', fix: 'Install Claude CLI' }); } else { resolve({ success: true, version: stdout.trim() }); } }); }); } async fixSSERoutes() { // Check if sessions-routes.js exists const routesPath = path.join(__dirname, '../routes/sessions-routes.js'); if (!fs.existsSync(routesPath)) { return { success: false, error: 'sessions-routes.js missing', needsManualFix: true }; } return { success: true, message: 'SSE routes present' }; } async checkFrontendSSEClient() { // Check sse-client.js for the event routing fix const sseClientPath = path.join(__dirname, '../public/claude-ide/sse-client.js'); if (!fs.existsSync(sseClientPath)) { return { success: false, error: 'sse-client.js missing' }; } const content = fs.readFileSync(sseClientPath, 'utf-8'); const hasFix = content.includes('_eventType') && content.includes('routeEvent'); if (!hasFix) { return { success: false, error: 'SSE client missing event routing fix', fix: 'Apply _eventType fix to sse-client.js line 95' }; } return { success: true, message: 'SSE client has routing fix' }; } async fixJSONParsing() { // Check if claude-service.js has JSON parsing fix const servicePath = path.join(__dirname, '../services/claude-service.js'); if (!fs.existsSync(servicePath)) { return { success: false, error: 'claude-service.js missing' }; } const content = fs.readFileSync(servicePath, 'utf-8'); const hasJSONFix = content.includes('--output-format') && content.includes('jsonOutput'); if (!hasJSONFix) { return { success: false, error: 'claude-service.js missing JSON output format fix', fix: 'Add --output-format json flag and JSON parsing' }; } return { success: true, message: 'JSON parsing fix present' }; } async verify(fix) { // Wait a moment then check if server is responding await new Promise(r => setTimeout(r, 2000)); const checks = { serverRunning: await this.checkServerRunning(), portOpen: await this.checkPortOpen() }; return { success: checks.serverRunning && checks.portOpen, checks }; } async checkPortOpen() { return new Promise((resolve) => { exec('curl -s http://localhost:3010/health > /dev/null', (error) => { resolve(!error); }); }); } writeResults(results) { const resultsFile = path.join(this.logsDir, `fix-result-${Date.now()}.json`); fs.writeFileSync(resultsFile, JSON.stringify(results, null, 2)); console.log(`[AutoFix] Results written to ${resultsFile}`); } } // Main execution if (require.main === module) { const failureFile = process.argv[2]; if (!failureFile) { console.error('Usage: node auto-fix.js '); process.exit(1); } const agent = new AutoFixAgent(failureFile); agent.run().catch(console.error); } module.exports = AutoFixAgent;