From a76c254ff53156125e3f03044141129cfc9150aa Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 12 Dec 2025 13:53:04 +0000 Subject: [PATCH] feat(ai): Wire change detection into patrol service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate operational memory into patrol context: - Add changeDetector and remediationLog fields to PatrolService - Add SetChangeDetector and SetRemediationLog methods - Integrate change detection into buildEnrichedContext - Convert state to ResourceSnapshots for change tracking - Append recent changes summary to AI context The AI now sees a 'Recent Infrastructure Changes (24h)' section showing events like: - VM 'web-server' status changed: running → stopped (2h ago) - 'db-server' migrated from node1 to node2 (4h ago) - 'web-server' memory increased: 4 GB → 8 GB (1d ago) All tests passing. --- internal/ai/patrol.go | 97 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/internal/ai/patrol.go b/internal/ai/patrol.go index 6202f00..c2d1921 100644 --- a/internal/ai/patrol.go +++ b/internal/ai/patrol.go @@ -212,6 +212,8 @@ type PatrolService struct { knowledgeStore *knowledge.Store // For per-resource notes in patrol context metricsHistory MetricsHistoryProvider // For trend analysis and predictions baselineStore *baseline.Store // For anomaly detection via learned baselines + changeDetector *ChangeDetector // For tracking infrastructure changes + remediationLog *RemediationLog // For tracking remediation actions // Cached thresholds (recalculated when thresholdProvider changes) thresholds PatrolThresholds @@ -358,6 +360,29 @@ func (p *PatrolService) GetBaselineStore() *baseline.Store { return p.baselineStore } +// SetChangeDetector sets the change detector for tracking infrastructure changes +func (p *PatrolService) SetChangeDetector(detector *ChangeDetector) { + p.mu.Lock() + defer p.mu.Unlock() + p.changeDetector = detector + log.Info().Msg("AI Patrol: Change detector set for operational memory") +} + +// SetRemediationLog sets the remediation log for tracking fix attempts +func (p *PatrolService) SetRemediationLog(remLog *RemediationLog) { + p.mu.Lock() + defer p.mu.Unlock() + p.remediationLog = remLog + log.Info().Msg("AI Patrol: Remediation log set for operational memory") +} + +// GetRemediationLog returns the remediation log (for logging actions) +func (p *PatrolService) GetRemediationLog() *RemediationLog { + p.mu.RLock() + defer p.mu.RUnlock() + return p.remediationLog +} + // GetConfig returns the current patrol configuration func (p *PatrolService) GetConfig() PatrolConfig { p.mu.RLock() @@ -1703,6 +1728,7 @@ func (p *PatrolService) buildEnrichedContext(state models.StateSnapshot) string metricsHistory := p.metricsHistory knowledgeStore := p.knowledgeStore baselineStore := p.baselineStore + changeDetector := p.changeDetector p.mu.RUnlock() // If no metrics history, fall back to basic summary @@ -1737,6 +1763,25 @@ func (p *PatrolService) buildEnrichedContext(state models.StateSnapshot) string // Format for AI consumption formatted := aicontext.FormatInfrastructureContext(infraCtx) + + // Append recent changes if change detector is available + if changeDetector != nil { + // Detect any new changes from current state + snapshots := stateToSnapshots(state) + newChanges := changeDetector.DetectChanges(snapshots) + + // Get summary of recent changes (last 24 hours) + since := time.Now().Add(-24 * time.Hour) + changesSummary := changeDetector.GetChangesSummary(since, 20) + + if changesSummary != "" { + formatted += "\n## Recent Infrastructure Changes (24h)\n\n" + changesSummary + } + + if len(newChanges) > 0 { + log.Debug().Int("new_changes", len(newChanges)).Msg("AI Patrol: Detected infrastructure changes") + } + } log.Debug(). Int("resources", infraCtx.TotalResources). @@ -1747,6 +1792,58 @@ func (p *PatrolService) buildEnrichedContext(state models.StateSnapshot) string return formatted } +// stateToSnapshots converts state to resource snapshots for change detection +func stateToSnapshots(state models.StateSnapshot) []ResourceSnapshot { + var snapshots []ResourceSnapshot + + for _, node := range state.Nodes { + snapshots = append(snapshots, ResourceSnapshot{ + ID: node.ID, + Name: node.Name, + Type: "node", + Status: node.Status, + CPUCores: node.CPUInfo.Cores, + MemoryBytes: node.Memory.Total, + }) + } + + for _, vm := range state.VMs { + if vm.Template { + continue + } + snapshots = append(snapshots, ResourceSnapshot{ + ID: vm.ID, + Name: vm.Name, + Type: "vm", + Status: vm.Status, + Node: vm.Node, + CPUCores: vm.CPUs, + MemoryBytes: vm.Memory.Total, + DiskBytes: vm.Disk.Total, + LastBackup: vm.LastBackup, + }) + } + + for _, ct := range state.Containers { + if ct.Template { + continue + } + snapshots = append(snapshots, ResourceSnapshot{ + ID: ct.ID, + Name: ct.Name, + Type: "container", + Status: ct.Status, + Node: ct.Node, + CPUCores: ct.CPUs, + MemoryBytes: ct.Memory.Total, + DiskBytes: ct.Disk.Total, + LastBackup: ct.LastBackup, + }) + } + + return snapshots +} + // metricsHistoryShim adapts ai.MetricsHistoryProvider to aicontext.MetricsHistoryProvider type metricsHistoryShim struct { provider MetricsHistoryProvider