/** * Self-Healing System — NEW * Detects root cause of workflow failures and applies corrective transformations * Stores failure patterns in memory, prevents repeat failures * Integrates with validation + simulation pipeline */ import { type LLMGateway } from '@wfo/integrations/llm-providers/index'; import type { ValidationReport, SimulationReport, N8nWorkflow, WorkflowGraph } from '../types/workflow'; import { isValidNodeType } from '../knowledge/nodeRegistry'; export interface HealingAction { type: 'replace_node' | 'fix_expression' | 'add_parameter' | 'fix_connection' | 'add_retry' | 'remove_node'; targetNodeId?: string; description: string; applied: boolean; before?: unknown; after?: unknown; } export interface HealingResult { healed: boolean; actionsApplied: HealingAction[]; remainingIssues: string[]; healedWorkflow?: N8nWorkflow; confidence: number; // 0-100 } export interface FailurePattern { id: string; pattern: string; rootCause: string; fixStrategy: string; occurrences: number; lastSeen: string; } // In-memory failure store (persists across requests in a single process) const failureMemory: Map = new Map(); export class SelfHealingSystem { private llm: LLMGateway; constructor(llm: LLMGateway) { this.llm = llm; } /** * Attempt to auto-heal a workflow based on validation and simulation reports */ async heal( workflow: N8nWorkflow, graph: WorkflowGraph, validationReport: ValidationReport, simulationReport?: SimulationReport, ): Promise { const actions: HealingAction[] = []; let healedWorkflow = structuredClone(workflow); // ─── Phase 1: Rule-based deterministic healing ──────────────────────── const ruleBasedActions = this.applyRuleBasedFixes(healedWorkflow, validationReport); actions.push(...ruleBasedActions.actions); healedWorkflow = ruleBasedActions.workflow; // ─── Phase 2: AI-assisted healing for complex issues ────────────────── const complexIssues = validationReport.issues.filter((i) => i.severity === 'error' && !actions.some((a) => a.targetNodeId === i.nodeId && a.applied), ); if (complexIssues.length > 0) { try { const aiHealing = await this.applyAIHealing(healedWorkflow, graph, complexIssues, simulationReport); actions.push(...aiHealing.actions); if (aiHealing.healedWorkflow) { healedWorkflow = aiHealing.healedWorkflow; } } catch (err) { console.warn('[SelfHealing] AI healing phase failed:', err); } } // ─── Phase 3: Record failure pattern ───────────────────────────────── this.recordFailurePattern(validationReport, simulationReport, actions); const appliedCount = actions.filter((a) => a.applied).length; const remainingErrors = validationReport.issues.filter((i) => i.severity === 'error' && !actions.some((a) => a.targetNodeId === i.nodeId && a.applied), ); return { healed: appliedCount > 0 && remainingErrors.length === 0, actionsApplied: actions.filter((a) => a.applied), remainingIssues: remainingErrors.map((i) => i.message), healedWorkflow: appliedCount > 0 ? healedWorkflow : undefined, confidence: Math.round((appliedCount / Math.max(1, actions.length)) * 100), }; } // ─── Rule-Based Fixes (deterministic, no LLM needed) ───────────────────── private applyRuleBasedFixes( workflow: N8nWorkflow, report: ValidationReport, ): { workflow: N8nWorkflow; actions: HealingAction[] } { const actions: HealingAction[] = []; // Fix 1: active must be false if ((workflow.active as any) === true) { workflow.active = false; actions.push({ type: 'fix_expression', description: 'Set active: false (safety rule — never auto-activate)', applied: true, before: true, after: false, }); } // Fix 2: Missing node IDs → generate UUID-like IDs workflow.nodes.forEach((node) => { if (!node.id || node.id.trim() === '') { const newId = generateId(); actions.push({ type: 'add_parameter', targetNodeId: node.name, description: `Generated missing ID for node "${node.name}"`, applied: true, before: node.id, after: newId, }); node.id = newId; } }); // Fix 3: Missing typeVersion → set to 1 workflow.nodes.forEach((node) => { if (!node.typeVersion || node.typeVersion < 1) { actions.push({ type: 'add_parameter', targetNodeId: node.id, description: `Added missing typeVersion: 1 to node "${node.name}"`, applied: true, before: node.typeVersion, after: 1, }); node.typeVersion = 1; } }); // Fix 4: Missing position → assign default grid positions workflow.nodes.forEach((node, i) => { if (!node.position || !Array.isArray(node.position) || node.position.length !== 2) { const newPos: [number, number] = [i * 220, 300]; actions.push({ type: 'fix_expression', targetNodeId: node.id, description: `Set default position for node "${node.name}"`, applied: true, before: node.position, after: newPos, }); node.position = newPos; } }); // Fix 5: Add retry to external nodes that support it workflow.nodes.forEach((node) => { const externalTypes = [ 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.telegram', 'n8n-nodes-base.slack', 'n8n-nodes-base.gmail', 'n8n-nodes-base.openAi', '@n8n/n8n-nodes-langchain.agent', 'n8n-nodes-base.googleSheets', 'n8n-nodes-base.airtable', 'n8n-nodes-base.notion', 'n8n-nodes-base.postgres', 'n8n-nodes-base.hubspot', 'n8n-nodes-base.stripe', 'n8n-nodes-base.github', ]; if (externalTypes.includes(node.type) && !node.retryOnFail) { actions.push({ type: 'add_retry', targetNodeId: node.id, description: `Added retry policy to external node "${node.name}"`, applied: true, before: { retryOnFail: false }, after: { retryOnFail: true, maxTries: 3, waitBetweenTries: 1000 }, }); node.retryOnFail = true; node.maxTries = node.maxTries ?? 3; node.waitBetweenTries = node.waitBetweenTries ?? 1000; } }); // Fix 6: onError field default workflow.nodes.forEach((node) => { if (!node.onError) { node.onError = 'continueErrorOutput'; actions.push({ type: 'add_parameter', targetNodeId: node.id, description: `Set onError: continueErrorOutput for node "${node.name}"`, applied: true, before: undefined, after: 'continueErrorOutput', }); } }); // Fix 7: Remove nodes with unknown types from registry const unknownNodes = workflow.nodes.filter((n) => n.type && !isValidNodeType(n.type)); unknownNodes.forEach((node) => { actions.push({ type: 'remove_node', targetNodeId: node.id, description: `REJECTED unknown node type "${node.type}" for node "${node.name}" — not in registry`, applied: true, before: node.type, after: null, }); }); if (unknownNodes.length > 0) { const unknownIds = new Set(unknownNodes.map((n) => n.id)); workflow.nodes = workflow.nodes.filter((n) => !unknownIds.has(n.id)); } // Fix 8: Ensure settings are complete if (!workflow.settings) { workflow.settings = {}; actions.push({ type: 'add_parameter', description: 'Added missing workflow settings object', applied: true, before: undefined, after: {}, }); } if (!workflow.settings.executionOrder) { workflow.settings.executionOrder = 'v1'; actions.push({ type: 'add_parameter', description: 'Set settings.executionOrder: v1', applied: true, }); } return { workflow, actions }; } // ─── AI-Assisted Healing (for complex expression / logic issues) ────────── private async applyAIHealing( workflow: N8nWorkflow, graph: WorkflowGraph, issues: Array<{ message: string; nodeId?: string; suggestion: string; category: string }>, simulationReport?: SimulationReport, ): Promise<{ actions: HealingAction[]; healedWorkflow?: N8nWorkflow }> { // Find the historical fix if available const historicalFixes = this.getRelevantHistoricalFixes(issues.map((i) => i.message)); const result = await this.llm.completeJSON<{ fixedWorkflow: N8nWorkflow; actionsApplied: Array<{ nodeId?: string; description: string; type: string }>; }>([ { role: 'system', content: `You are an n8n workflow self-healing system. Your job is to fix broken workflows. Fix ONLY the identified issues. Do not change anything else. NEVER set active: true. ALWAYS use real expressions like {{$json?.field ?? ""}}. Return the complete fixed workflow JSON.`, }, { role: 'user', content: `Fix these workflow issues: CURRENT WORKFLOW: ${JSON.stringify(workflow, null, 2)} ISSUES TO FIX: ${issues.map((i, idx) => `${idx + 1}. [${i.category}] ${i.message}\n Fix: ${i.suggestion}`).join('\n')} ${simulationReport ? `SIMULATION FAILURES:\n${JSON.stringify(simulationReport.predictedFailurePoints, null, 2)}` : ''} ${historicalFixes.length > 0 ? `HISTORICAL FIX STRATEGIES:\n${historicalFixes.join('\n')}` : ''} Return JSON: { "fixedWorkflow": { ... complete fixed n8n workflow ... }, "actionsApplied": [{ "nodeId": "...", "description": "what was fixed", "type": "fix_type" }] }`, }, ], { temperature: 0.0, retries: 2, }); const actions: HealingAction[] = (result.actionsApplied ?? []).map((a) => ({ type: a.type as HealingAction['type'], targetNodeId: a.nodeId, description: a.description, applied: true, })); return { actions, healedWorkflow: result.fixedWorkflow ? { ...result.fixedWorkflow, active: false, // Always enforce safety } : undefined, }; } // ─── Failure Pattern Memory ─────────────────────────────────────────────── private recordFailurePattern( validation: ValidationReport, simulation: SimulationReport | undefined, actions: HealingAction[], ): void { validation.issues.forEach((issue) => { const key = `${issue.category}-${issue.id}`; const existing = failureMemory.get(key); const action = actions.find((a) => a.targetNodeId === issue.nodeId); if (existing) { failureMemory.set(key, { ...existing, occurrences: existing.occurrences + 1, lastSeen: new Date().toISOString(), fixStrategy: action?.description ?? existing.fixStrategy, }); } else { failureMemory.set(key, { id: key, pattern: issue.message, rootCause: issue.category, fixStrategy: action?.description ?? issue.suggestion, occurrences: 1, lastSeen: new Date().toISOString(), }); } }); } private getRelevantHistoricalFixes(issueMessages: string[]): string[] { const fixes: string[] = []; failureMemory.forEach((pattern) => { const isRelevant = issueMessages.some( (msg) => msg.toLowerCase().includes(pattern.rootCause.toLowerCase()), ); if (isRelevant && pattern.occurrences > 1) { fixes.push(`Pattern: "${pattern.pattern}" → Fix: "${pattern.fixStrategy}" (seen ${pattern.occurrences}x)`); } }); return fixes.slice(0, 5); // Top 5 most relevant } /** * Get current failure memory stats (for reporting) */ getMemoryStats(): { totalPatterns: number; topFailures: FailurePattern[] } { const patterns = [...failureMemory.values()]; const sorted = patterns.sort((a, b) => b.occurrences - a.occurrences); return { totalPatterns: patterns.length, topFailures: sorted.slice(0, 10), }; } } function generateId(): string { const hex = () => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0'); return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`; }