/** * pi-health v1.1 — Session health with error diagnosis * * Not just stats — detects recurring errors, warns on degrading sessions, * tracks error patterns, and suggests fixes. * * /health — health score + stats * /health diagnose — analyze error patterns + suggest fixes * /health errors — list all errors this session * /health reset — reset counters */ import type { ExtensionAPI } from '@anthropic-ai/claude-code' interface ErrorRecord { toolName: string; message: string; timestamp: number; count: number } const state = { start: Date.now(), messages: 0, toolCalls: 0, errors: 0, tokens: 0, toolBreakdown: new Map(), errorLog: [] as ErrorRecord[], errorPatterns: new Map(), // pattern → count consecutiveErrors: 0, maxConsecutiveErrors: 0, lastErrorTool: '', degrading: false, } function uptimeStr(): string { const s = Math.round((Date.now() - state.start) / 1000) if (s < 60) return `${s}s` if (s < 3600) return `${Math.floor(s / 60)}m` return `${Math.floor(s / 3600)}h${Math.floor((s % 3600) / 60)}m` } function healthScore(): number { let score = 100 if (state.errors > 10) score -= 30 else if (state.errors > 5) score -= 20 else if (state.errors > 0) score -= state.errors * 3 if (state.maxConsecutiveErrors > 3) score -= 15 // error spiral if (state.degrading) score -= 10 const mins = (Date.now() - state.start) / 60000 if (mins > 5 && state.messages < 2) score -= 10 // stalled const errorRate = state.toolCalls > 0 ? state.errors / state.toolCalls : 0 if (errorRate > 0.3) score -= 20 // >30% error rate return Math.max(0, Math.min(100, score)) } function formatHealth(): string { const h = healthScore() const icon = h >= 80 ? '💚' : h >= 50 ? '💛' : '❤️' const mins = Math.max(1, Math.round((Date.now() - state.start) / 60000)) const errorRate = state.toolCalls > 0 ? Math.round(state.errors / state.toolCalls * 100) : 0 const lines = [ `## Session Health ${icon} ${h}/100`, '', '| Metric | Value |', '|--------|-------|', `| Uptime | ${uptimeStr()} |`, `| Messages | ${state.messages} |`, `| Tool calls | ${state.toolCalls} |`, `| Errors | ${state.errors} (${errorRate}% rate) |`, `| Max error streak | ${state.maxConsecutiveErrors} |`, `| Status | ${state.degrading ? '⚠️ Degrading' : '✅ Stable'} |`, ] if (state.errorPatterns.size > 0) { lines.push('', '### Recurring errors') const sorted = Array.from(state.errorPatterns.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3) for (const [pattern, count] of sorted) { lines.push(`- **${count}x** \`${pattern.slice(0, 80)}\``) } } return lines.join('\n') } function formatDiagnosis(): string { if (state.errors === 0) return '✅ No errors this session. Health is good.' const lines = ['## Session Diagnosis', ''] const errorRate = state.toolCalls > 0 ? state.errors / state.toolCalls : 0 // Error rate analysis if (errorRate > 0.5) lines.push('🔴 **Critical:** >50% of tool calls are failing. Consider:') else if (errorRate > 0.3) lines.push('🟡 **Warning:** >30% error rate. Investigate:') else lines.push('🟢 **Manageable:** Error rate is below 30%.') lines.push('') // Pattern analysis if (state.errorPatterns.size > 0) { lines.push('### Error patterns') const sorted = Array.from(state.errorPatterns.entries()).sort((a, b) => b[1] - a[1]) for (const [pattern, count] of sorted) { lines.push(`- **${count}x** \`${pattern.slice(0, 100)}\``) // Suggest fixes if (pattern.includes('not found')) lines.push(' → Check file paths and working directory') if (pattern.includes('permission')) lines.push(' → Check file permissions') if (pattern.includes('timeout')) lines.push(' → Command is taking too long, try breaking it up') if (pattern.includes('syntax')) lines.push(' → Code has syntax errors, review recent edits') if (pattern.includes('ENOENT')) lines.push(' → File or directory does not exist') if (pattern.includes('EACCES')) lines.push(' → Permission denied') } lines.push('') } // Error streak analysis if (state.maxConsecutiveErrors > 3) { lines.push(`### Error spiral detected`) lines.push(`${state.maxConsecutiveErrors} consecutive errors on \`${state.lastErrorTool}\`. The agent may be stuck in a retry loop.`) lines.push('→ Consider: change approach, use a different tool, or ask for help.') lines.push('') } // Tool reliability if (state.toolBreakdown.size > 0) { lines.push('### Tool reliability') const errorsByTool = new Map() for (const err of state.errorLog) { errorsByTool.set(err.toolName, (errorsByTool.get(err.toolName) || 0) + 1) } for (const [tool, errCount] of Array.from(errorsByTool.entries()).sort((a, b) => b[1] - a[1])) { const totalCalls = state.toolBreakdown.get(tool) || errCount const reliability = Math.round((1 - errCount / totalCalls) * 100) const icon = reliability > 90 ? '🟢' : reliability > 70 ? '🟡' : '🔴' lines.push(`- ${icon} \`${tool}\`: ${reliability}% reliable (${errCount}/${totalCalls} failed)`) } } return lines.join('\n') } function formatErrors(): string { if (state.errorLog.length === 0) return 'No errors this session.' const lines = ['## Error Log', ''] const recent = state.errorLog.slice(-15).reverse() for (const err of recent) { const ago = Math.round((Date.now() - err.timestamp) / 1000) const agoStr = ago < 60 ? `${ago}s ago` : `${Math.floor(ago / 60)}m ago` lines.push(`- **${agoStr}** \`${err.toolName}\`: ${err.message.slice(0, 100)}`) } return lines.join('\n') } function extractErrorPattern(message: string): string { // Normalize error messages for pattern matching return message .replace(/\/[^\s]+/g, '') // paths .replace(/\d+/g, 'N') // numbers .replace(/["'][^"']+["']/g, '') // strings .slice(0, 80) } export default function init(pi: ExtensionAPI) { pi.on('message_end', (event: any) => { state.messages++ const msg = event.message as any if (msg?.usage) state.tokens += (msg.usage.input_tokens || 0) + (msg.usage.output_tokens || 0) }) pi.on('tool_execution_end', (event: any) => { state.toolCalls++ state.toolBreakdown.set(event.toolName, (state.toolBreakdown.get(event.toolName) || 0) + 1) if (event.isError) { state.errors++ state.consecutiveErrors++ state.lastErrorTool = event.toolName if (state.consecutiveErrors > state.maxConsecutiveErrors) { state.maxConsecutiveErrors = state.consecutiveErrors } const msg = typeof event.result === 'string' ? event.result : JSON.stringify(event.result || '').slice(0, 200) state.errorLog.push({ toolName: event.toolName, message: msg, timestamp: Date.now(), count: 1 }) if (state.errorLog.length > 200) state.errorLog.shift() const pattern = extractErrorPattern(msg) state.errorPatterns.set(pattern, (state.errorPatterns.get(pattern) || 0) + 1) // Detect degradation if (state.consecutiveErrors >= 3) { if (!state.degrading) { state.degrading = true pi.sendMessage({ content: `⚠️ **Session degrading:** ${state.consecutiveErrors} consecutive errors on \`${event.toolName}\`. Run \`/health diagnose\` for analysis.`, display: true }, { triggerTurn: false }) } } } else { state.consecutiveErrors = 0 state.degrading = false } }) pi.addCommand({ name: 'health', description: 'Session health with error diagnosis', handler: async (args) => { const sub = args.trim().toLowerCase() if (sub === 'diagnose') { pi.sendMessage({ content: formatDiagnosis(), display: true }, { triggerTurn: false }); return } if (sub === 'errors') { pi.sendMessage({ content: formatErrors(), display: true }, { triggerTurn: false }); return } if (sub === 'reset') { state.messages = 0; state.toolCalls = 0; state.errors = 0; state.tokens = 0 state.toolBreakdown.clear(); state.errorLog.length = 0; state.errorPatterns.clear() state.consecutiveErrors = 0; state.maxConsecutiveErrors = 0; state.degrading = false state.start = Date.now() pi.sendMessage({ content: 'Health reset.', display: true }, { triggerTurn: false }); return } pi.sendMessage({ content: formatHealth(), display: true }, { triggerTurn: false }) } }) pi.addTool({ name: 'health_check', description: 'Session health score with error pattern diagnosis.', parameters: { type: 'object', properties: {} }, handler: async () => formatHealth() }) pi.addTool({ name: 'health_diagnose', description: 'Diagnose session errors — patterns, tool reliability, suggestions.', parameters: { type: 'object', properties: {} }, handler: async () => formatDiagnosis() }) }