mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Cap AI patrol history and correlation reads
This commit is contained in:
parent
73597f8b1a
commit
76ba35a3a9
5 changed files with 94 additions and 12 deletions
|
|
@ -87,6 +87,15 @@ governed floor is ready.
|
|||
`internal/ai/` is the live backend AI engine. It owns chat execution, Patrol
|
||||
orchestration, findings generation, investigation support, quickstart and
|
||||
provider selection, remediation flow, and cost persistence.
|
||||
That same backend runtime ownership also includes bounded Patrol and
|
||||
investigation read models. `internal/ai/patrol_history_persistence.go` and
|
||||
`internal/ai/proxmox/events.go` must cap persisted-history loads and
|
||||
caller-requested read limits at the canonical runtime maxima instead of
|
||||
allocating directly from raw on-disk counts or transport-supplied limits.
|
||||
Callers may request fewer records, but AI runtime storage and correlation
|
||||
surfaces remain responsible for enforcing the governed ceilings that protect
|
||||
memory and keep Patrol/history behavior stable under malformed or oversized
|
||||
inputs.
|
||||
That same backend runtime ownership includes `internal/config/ai.go`, because
|
||||
provider auth, base URLs, provider-scoped model defaults, and other persisted
|
||||
runtime AI selection rules must stay canonical in the shared AI config model
|
||||
|
|
|
|||
|
|
@ -87,8 +87,9 @@ func (a *PatrolHistoryPersistenceAdapter) LoadPatrolRunHistory() ([]PatrolRunRec
|
|||
}
|
||||
|
||||
// Convert from config.PatrolRunRecord to ai.PatrolRunRecord
|
||||
runs := make([]PatrolRunRecord, len(data.Runs))
|
||||
for i, r := range data.Runs {
|
||||
runCount := clampHistoryCount(len(data.Runs), 0, MaxPatrolRunHistory)
|
||||
runs := make([]PatrolRunRecord, runCount)
|
||||
for i, r := range data.Runs[:runCount] {
|
||||
runs[i] = normalizePatrolRunRecord(PatrolRunRecord{
|
||||
ID: r.ID,
|
||||
StartedAt: r.StartedAt,
|
||||
|
|
@ -191,6 +192,25 @@ type PatrolRunHistoryStore struct {
|
|||
onSaveError func(err error)
|
||||
}
|
||||
|
||||
func clampHistoryCount(value, minValue, maxValue int) int {
|
||||
if value < minValue {
|
||||
return minValue
|
||||
}
|
||||
if value > maxValue {
|
||||
return maxValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeHistoryRequestCount(requested, storeMax, available int) int {
|
||||
if requested <= 0 {
|
||||
requested = MaxPatrolRunHistory
|
||||
}
|
||||
requested = clampHistoryCount(requested, 0, MaxPatrolRunHistory)
|
||||
requested = clampHistoryCount(requested, 0, storeMax)
|
||||
return clampHistoryCount(requested, 0, available)
|
||||
}
|
||||
|
||||
// NewPatrolRunHistoryStore creates a new patrol run history store
|
||||
func NewPatrolRunHistoryStore(maxRuns int) *PatrolRunHistoryStore {
|
||||
if maxRuns <= 0 {
|
||||
|
|
@ -216,12 +236,11 @@ func (s *PatrolRunHistoryStore) SetPersistence(p PatrolHistoryPersistence) error
|
|||
return err
|
||||
}
|
||||
if len(runs) > 0 {
|
||||
if len(runs) > s.maxRuns {
|
||||
runs = runs[:s.maxRuns]
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.runs = runs
|
||||
// Trim to max if loaded more than maxRuns
|
||||
if len(s.runs) > s.maxRuns {
|
||||
s.runs = s.runs[:s.maxRuns]
|
||||
}
|
||||
s.mu.Unlock()
|
||||
log.Info().Int("count", len(runs)).Msg("loaded patrol run history from disk")
|
||||
}
|
||||
|
|
@ -268,9 +287,7 @@ func (s *PatrolRunHistoryStore) GetRecent(n int) []PatrolRunRecord {
|
|||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if n <= 0 || n > len(s.runs) {
|
||||
n = len(s.runs)
|
||||
}
|
||||
n = normalizeHistoryRequestCount(n, s.maxRuns, len(s.runs))
|
||||
|
||||
result := make([]PatrolRunRecord, n)
|
||||
copy(result, s.runs[:n])
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package ai
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -266,6 +267,27 @@ func TestPatrolHistoryPersistenceAdapter_NormalizesAlertIdentity(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPatrolHistoryPersistenceAdapter_LoadPatrolRunHistoryCapsRuns(t *testing.T) {
|
||||
records := make([]config.PatrolRunRecord, MaxPatrolRunHistory+10)
|
||||
for i := range records {
|
||||
records[i] = config.PatrolRunRecord{ID: fmt.Sprintf("run-%d", i)}
|
||||
}
|
||||
|
||||
persistence := config.NewConfigPersistence(t.TempDir())
|
||||
if err := persistence.SavePatrolRunHistory(records); err != nil {
|
||||
t.Fatalf("SavePatrolRunHistory failed: %v", err)
|
||||
}
|
||||
|
||||
adapter := NewPatrolHistoryPersistenceAdapter(persistence)
|
||||
loaded, err := adapter.LoadPatrolRunHistory()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPatrolRunHistory failed: %v", err)
|
||||
}
|
||||
if len(loaded) != MaxPatrolRunHistory {
|
||||
t.Fatalf("expected %d runs, got %d", MaxPatrolRunHistory, len(loaded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatrolRunHistoryStore_PersistenceStatusAndErrors(t *testing.T) {
|
||||
store := NewPatrolRunHistoryStore(5)
|
||||
store.saveDebounce = 0
|
||||
|
|
|
|||
|
|
@ -86,6 +86,25 @@ type OperationWindow struct {
|
|||
ExpectedMetrics []string `json:"expected_metrics"` // Metrics expected to be affected
|
||||
}
|
||||
|
||||
func clampCorrelationCount(value, minValue, maxValue int) int {
|
||||
if value < minValue {
|
||||
return minValue
|
||||
}
|
||||
if value > maxValue {
|
||||
return maxValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeCorrelationLimit(requested, configMax, available int) int {
|
||||
if requested <= 0 {
|
||||
requested = DefaultEventCorrelatorConfig().MaxCorrelations
|
||||
}
|
||||
requested = clampCorrelationCount(requested, 0, DefaultEventCorrelatorConfig().MaxCorrelations)
|
||||
requested = clampCorrelationCount(requested, 0, configMax)
|
||||
return clampCorrelationCount(requested, 0, available)
|
||||
}
|
||||
|
||||
// EventCorrelatorConfig configures the event correlator
|
||||
type EventCorrelatorConfig struct {
|
||||
DataDir string
|
||||
|
|
@ -403,9 +422,7 @@ func (c *EventCorrelator) GetCorrelations(limit int) []EventCorrelation {
|
|||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if limit <= 0 || limit > len(c.correlations) {
|
||||
limit = len(c.correlations)
|
||||
}
|
||||
limit = normalizeCorrelationLimit(limit, c.config.MaxCorrelations, len(c.correlations))
|
||||
|
||||
// Return most recent
|
||||
start := len(c.correlations) - limit
|
||||
|
|
|
|||
|
|
@ -153,6 +153,23 @@ func TestEventCorrelator_GetCorrelations(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEventCorrelator_GetCorrelationsCapsToConfiguredLimit(t *testing.T) {
|
||||
cfg := DefaultEventCorrelatorConfig()
|
||||
cfg.MaxCorrelations = 2
|
||||
correlator := NewEventCorrelator(cfg)
|
||||
|
||||
correlator.correlations = []EventCorrelation{
|
||||
{ID: "one"},
|
||||
{ID: "two"},
|
||||
{ID: "three"},
|
||||
}
|
||||
|
||||
correlations := correlator.GetCorrelations(1000)
|
||||
if len(correlations) != 2 {
|
||||
t.Fatalf("expected 2 correlations, got %d", len(correlations))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventCorrelator_FormatForPatrol(t *testing.T) {
|
||||
correlator := NewEventCorrelator(DefaultEventCorrelatorConfig())
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue