diff --git a/internal/ai/patrol_init.go b/internal/ai/patrol_init.go index c4f1a2d49..67c14303e 100644 --- a/internal/ai/patrol_init.go +++ b/internal/ai/patrol_init.go @@ -550,6 +550,18 @@ func (p *PatrolService) GetTriggerManager() *TriggerManager { return p.triggerManager } +// CanAcceptTriggers reports whether event-driven patrol triggers should be queued. +// Trigger sources must honor both the enabled flag and the patrol loop lifecycle. +func (p *PatrolService) CanAcceptTriggers() bool { + if p == nil { + return false + } + + p.mu.RLock() + defer p.mu.RUnlock() + return p.config.Enabled && p.running +} + // TriggerScopedPatrol runs a targeted patrol for specific resources. // This is called by the TriggerManager for event-driven patrols. // When ResourceIDs or ResourceTypes are specified in the scope, only those resources diff --git a/internal/ai/patrol_init_additional_test.go b/internal/ai/patrol_init_additional_test.go index 07d2fc575..6a61e82fd 100644 --- a/internal/ai/patrol_init_additional_test.go +++ b/internal/ai/patrol_init_additional_test.go @@ -166,3 +166,26 @@ func TestPatrolService_AdditionalSetters(t *testing.T) { t.Fatalf("expected unified finding resolver to capture id, got %q", resolvedID) } } + +func TestPatrolService_CanAcceptTriggers(t *testing.T) { + ps := NewPatrolService(nil, nil) + + if ps.CanAcceptTriggers() { + t.Fatalf("expected triggers to be rejected before patrol starts") + } + + ps.SetConfig(PatrolConfig{Enabled: true}) + if ps.CanAcceptTriggers() { + t.Fatalf("expected triggers to be rejected while patrol is stopped") + } + + ps.running = true + if !ps.CanAcceptTriggers() { + t.Fatalf("expected triggers to be accepted when patrol is enabled and running") + } + + ps.SetConfig(PatrolConfig{Enabled: false}) + if ps.CanAcceptTriggers() { + t.Fatalf("expected triggers to be rejected when patrol is disabled") + } +} diff --git a/internal/api/router.go b/internal/api/router.go index 1904d3fc5..be2704f1a 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -2652,6 +2652,13 @@ func (r *Router) initializeAIIntelligenceServices(ctx context.Context, dataDir s Str("resource_id", resourceID). Str("reason", reason). Msg("Alert bridge: Triggering mini-patrol") + if !patrol.CanAcceptTriggers() { + log.Debug(). + Str("resource_id", resourceID). + Str("reason", reason). + Msg("Alert bridge: Skipping mini-patrol because patrol is disabled or stopped") + return + } if triggerManager := r.aiSettingsHandler.GetTriggerManager(); triggerManager != nil { if triggerManager.TriggerPatrol(scope) { log.Debug(). @@ -2734,6 +2741,14 @@ func (r *Router) initializeAIIntelligenceServices(ctx context.Context, dataDir s baselineStore.SetAnomalyCallback(func(resourceID, resourceType, metric string, severity baseline.AnomalySeverity, value, baselineValue float64) { // Only trigger for significant anomalies (high or critical) if severity == baseline.AnomalyHigh || severity == baseline.AnomalyCritical { + if !patrol.CanAcceptTriggers() { + log.Debug(). + Str("resourceID", resourceID). + Str("metric", metric). + Str("severity", string(severity)). + Msg("Anomaly trigger skipped because patrol is disabled or stopped") + return + } scope := ai.AnomalyTriggeredPatrolScope( resourceID, resourceType,