From 2a9691ee80953763bf922adaca021cd977060910 Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Mon, 22 Jun 2026 17:59:30 +0000 Subject: [PATCH] Kernel DNA mutations --- kernel_ai/services/telemetry_orchestration.py | 58 ++++++++++++++++++- static/js/kernel-dna.js | 56 +++++++++++++----- 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/kernel_ai/services/telemetry_orchestration.py b/kernel_ai/services/telemetry_orchestration.py index a87f2eb..801c233 100644 --- a/kernel_ai/services/telemetry_orchestration.py +++ b/kernel_ai/services/telemetry_orchestration.py @@ -72,13 +72,69 @@ def _kernel_dna_softirq_nucleotides(limit=8): ) +KERNEL_DNA_ML_SINCE_SEC = int(os.environ.get("KERNEL_DNA_ML_SINCE_SEC", "120")) +KERNEL_DNA_ML_MAX = int(os.environ.get("KERNEL_DNA_ML_MAX", "8")) + + +def _ml_anomalies_to_mutations(rows): + """Map stored ML anomalies onto the Kernel DNA mutation contract. + + Keeps only the most recent anomaly per feature (rows arrive newest-first) + and caps the total so the helix is never flooded. + """ + mutations = [] + seen = set() + for row in rows: + feature = row.get("feature") + if feature in seen: + continue + seen.add(feature) + mutations.append( + { + "type": feature or row.get("type") or "ml_anomaly", + "severity": row.get("severity", "medium"), + "message": row.get("message", ""), + "description": row.get("message", ""), + "position": row.get("position", 0.5), + "source": "ml", + "subsystem": row.get("subsystem"), + "score": row.get("score"), + } + ) + if len(mutations) >= KERNEL_DNA_ML_MAX: + break + return mutations + + +def _get_ml_mutations(): + """Read recent ML anomalies from the shared store. Never raises: if the ML + store is unavailable the Kernel DNA view simply shows rule-based mutations.""" + try: + from kernel_ai.ml.config import MLConfig + from kernel_ai.ml.store import fetch_recent_anomalies + + cfg = MLConfig() + rows = fetch_recent_anomalies( + cfg.dsn, since_seconds=KERNEL_DNA_ML_SINCE_SEC, limit=100 + ) + return _ml_anomalies_to_mutations(rows) + except Exception: + return [] + + def get_kernel_dna_data(): - return _execution_service.get_kernel_dna_data( + data = _execution_service.get_kernel_dna_data( get_real_system_calls_fn=get_real_system_calls, map_syscall_to_subsystem_fn=map_syscall_to_subsystem, map_interrupt_to_subsystem_fn=map_interrupt_to_subsystem, softirq_nucleotides_fn=_kernel_dna_softirq_nucleotides, ) + # Tag existing (threshold) mutations and merge in ML-detected ones so the + # UI can render both sources side by side, visually distinguished. + for mutation in data.get("mutations", []): + mutation.setdefault("source", "rule") + data.setdefault("mutations", []).extend(_get_ml_mutations()) + return data def get_execution_context_data(exec_context_prev): diff --git a/static/js/kernel-dna.js b/static/js/kernel-dna.js index 7bce41c..b090276 100755 --- a/static/js/kernel-dna.js +++ b/static/js/kernel-dna.js @@ -416,12 +416,22 @@ class KernelDNAVisualization { const group = new THREE.Group(); const t = mutationData.position; const point = helixCurve.getPoint(t); - - // Mutation: "broken" appearance - distorted/irregular shape with yellow assessment marker - // Use octahedron for "broken" geometric look instead of perfect sphere + + // Two sources of mutations, rendered side by side but distinguishable: + // rule -> threshold rules (gray "broken" wireframe) + // ml -> statistical baseline detector (cyan wireframe) + // Severity drives the assessment marker colour (high = red, else yellow). + const isML = mutationData.source === 'ml'; + const isHigh = String(mutationData.severity || '').toLowerCase() === 'high'; + const wireColor = isML ? 0x67C8E0 : this.colors.mutedText; + const markerColor = isHigh ? 0xE0564E : this.colors.signalYellow; + const accentCss = isML ? 'rgba(103, 200, 224, 0.9)' : 'rgba(230, 193, 90, 0.7)'; + const accentText = isML ? '#67C8E0' : '#E6C15A'; + + // Mutation: "broken" appearance - distorted/irregular shape. const geometry = new THREE.OctahedronGeometry(0.25, 0); // Irregular shape const material = new THREE.MeshBasicMaterial({ - color: this.colors.mutedText, // Muted text: #6B7076 - "broken" gray + color: wireColor, side: THREE.DoubleSide, wireframe: true // Wireframe for "broken" appearance }); @@ -434,17 +444,18 @@ class KernelDNAVisualization { Math.random() * Math.PI ); // Random rotation for "broken" look - // Yellow assessment marker - small sphere + // Assessment marker - small sphere (red when severity is high). const markerGeometry = new THREE.SphereGeometry(0.08, 8, 8); const markerMaterial = new THREE.MeshBasicMaterial({ - color: this.colors.signalYellow, // Signal yellow: #E6C15A + color: markerColor, side: THREE.DoubleSide }); const yellowMarker = new THREE.Mesh(markerGeometry, markerMaterial); yellowMarker.position.copy(point); yellowMarker.position.y += 0.3; // Position above mutation - const labelText = String(mutationData.type || 'anomaly').replace(/_/g, ' ').toUpperCase().slice(0, 24); + const baseLabel = String(mutationData.type || 'anomaly').replace(/_/g, ' ').toUpperCase().slice(0, 22); + const tag = isML ? 'ML' : 'RULE'; const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 48; @@ -452,11 +463,14 @@ class KernelDNAVisualization { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(14, 17, 20, 0.72)'; ctx.fillRect(0, 6, canvas.width, 30); - ctx.strokeStyle = 'rgba(230, 193, 90, 0.7)'; + ctx.strokeStyle = accentCss; ctx.strokeRect(0.5, 6.5, canvas.width - 1, 29); - ctx.fillStyle = '#E6C15A'; + // Source tag chip on the left edge. + ctx.fillStyle = accentText; + ctx.font = 'bold 11px "Share Tech Mono", monospace'; + ctx.fillText(tag, 8, 24); ctx.font = '18px "Share Tech Mono", monospace'; - ctx.fillText(labelText, 10, 27); + ctx.fillText(baseLabel, 42, 27); const labelTexture = new THREE.CanvasTexture(canvas); const labelMaterial = new THREE.SpriteMaterial({ map: labelTexture, @@ -473,7 +487,10 @@ class KernelDNAVisualization { // Store reference for cleanup and animation mutation.userData.isMutation = true; mutation.userData.mutationType = mutationData.type || 'anomaly'; - mutation.userData.description = mutationData.description || 'Anomaly detected'; + mutation.userData.mutationSource = isML ? 'ml' : 'rule'; + mutation.userData.mutationSeverity = mutationData.severity || 'medium'; + mutation.userData.mutationScore = mutationData.score; + mutation.userData.description = mutationData.description || mutationData.message || 'Anomaly detected'; group.add(mutation); group.add(yellowMarker); @@ -1553,6 +1570,8 @@ class KernelDNAVisualization {
C (▲) = Context Switch
G (◆) = Lock/Mutex
⚠ Mutations = Anomalies
+
◇ RULE = threshold
+
◇ ML = baseline z-score
`); legendDiv.style.cssText = ` @@ -1978,13 +1997,22 @@ class KernelDNAVisualization {
Subsystem: ${userData.subsystem || 'kernel'}
`; } else if (userData.mutationType) { - // Mutation - yellow assessment, not red + // Mutation - source-coloured header (ML cyan, rule yellow). + const isML = userData.mutationSource === 'ml'; + const headColor = isML ? '#67C8E0' : '#E6C15A'; + const sourceLabel = isML ? 'ML baseline detector' : 'Threshold rule'; + const sev = String(userData.mutationSeverity || 'medium').toUpperCase(); + const scoreLine = (userData.mutationScore != null) + ? `
z-score: ${userData.mutationScore}
` + : ''; tooltipContent = ` -
+
MUTATION: ${userData.mutationType}
${userData.description || 'Anomaly detected'}
-
Assessment: Anomaly
+
Source: ${sourceLabel}
+
Severity: ${sev}
+ ${scoreLine} `; } else { // Generic tooltip