- 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>
312 lines
9.4 KiB
JavaScript
312 lines
9.4 KiB
JavaScript
/**
|
|
* 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 <failure-file>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const agent = new AutoFixAgent(failureFile);
|
|
agent.run().catch(console.error);
|
|
}
|
|
|
|
module.exports = AutoFixAgent;
|