Each tenant monitor owns its own IncidentStore, but incident and
incident-event IDs came from package-level int64 counters bumped with ++
under nothing but the calling store's mutex. Two tenants recording
alerts at once raced on them: go test -race failed
TestLeavingMockModeClearsEveryRunningTenantMonitor on PR 2698 at
incidents.go:1753. The remediation record counter had the same shape,
and remediation logs are created per org.
All six internal/ai/memory ID generators now mint through one atomic
helper. The numeric suffix also stops wrapping at 1000: even with atomic
counters, one generator minting more than a thousand IDs in a
wall-clock second repeated an ID, and incident notes find their incident
by ID. The prefix and timestamp layout is unchanged, and intToString
lost its last callers.
ids_test.go mints 2,400 IDs from every generator on 16 goroutines and
records alerts into two tenant stores concurrently. Against the old
files it reports DATA RACE at incidents.go:1753/1760 and
remediation.go:355, and every generator mints duplicate or wrapped IDs.
Patrol stopped calling DetectChanges when it moved to agentic execution
(1b1c9bb2a3). Recent-change context comes from the unified-resource
timeline, and the detector survives only as a fallback reader of the
legacy ai_changes.json that older releases wrote. The previous change
removed the detector's unjoined save; the detection itself was still
there, kept alive only by tests.
Remove DetectChanges, its snapshot diffing, ResourceSnapshot (and its
internal/ai alias), change-ID generation, the description formatters and
the detector's lock. The history is now loaded once and never modified,
so reads need no lock. The read APIs, trimming and legacy loading are
unchanged.
Tests that used detection to seed the fallback now write a legacy
ai_changes.json and load it, the same path production reads. With
loading broken, 10 of the 12 re-seeded internal/ai tests fail; the other
two assert that the fallback is absent or loses to the canonical
timeline. Two formatter comparisons now reject empty output, which let
them pass against an empty detector. Tests of the detection logic, its
heap stability and ID uniqueness go with it.
DetectChanges launched an untracked goroutine to rewrite ai_changes.json.
Tests backed by a temp directory slept 20ms or 100ms hoping it had
finished; when the goroutine started late it wrote into a directory that
cleanup was removing, which is the latent TempDir race already fixed for
IncidentStore and KnowledgeStore.
Nothing in the runtime has called DetectChanges since patrol moved to
agentic execution (1b1c9bb2a3). Recent-change context comes from the
unified-resource timeline, and the detector is only its read fallback, so
the save goroutine was reachable only from tests. Joining it would harden
a dead legacy writer, so remove it along with saveToDisk and the unused
save-scheduling fields. The detector still loads legacy history at
construction; detected changes stay in memory.
Replace the sleeping tests with TestChangeDetector_LegacyHistoryIsReadOnly,
which fails 210/300 runs against the old async writer. With the save
delayed 0-150ms, 349 of 600 old-code saves ran after their test's
directory was removed; none remain. Also drop two sleeps after
RemediationLog.Log, which already persists before returning.
Clears the ten dupl pairs across internal/ai:
- patrol_intelligence.go, tools_query.go, tools_storage.go: VM and LXC
system-container paths collapse into generics over read-state view
method subsets (gatherGuestIntelligenceFromViews, canonicalGuestGetResult
+ guestViewGetResult, addCanonicalGuestSearchMatches +
addGuestViewSearchMatches, appendGuestDiskSummaries).
- tools_file.go: append/write share executeFileMutation driven by
fileMutationSpec (approval-command text, shell redirect, verification
strategy stay per-action and verbatim).
- tools_kubernetes.go: deployment restart / pod delete share
executeKubernetesResourceAction driven by kubernetesResourceAction.
- providers/anthropic.go + anthropic_oauth.go: message conversion shared
via convertMessagesToAnthropic (the OAuth copy was annotated 'same as
regular client').
- memory/changes.go + memory/remediation.go: history loading shared via
the generic loadMemoryHistory in memory/paths.go (10 MiB cap, sort,
missing-file semantics preserved via a found flag).
- findings.go and unified/alerts.go: Finding/findingJSON and
UnifiedFinding/unifiedFindingJSON are deliberate marshal-mirror twins
(AlertIdentifier json:"-" vs alert_identifier round-trip); merging
would break every public literal. Suppressed with nolint:dupl and
enforced instead by new reflect-based mirror-sync tests.
Contract Extension Points name the shared helpers and the mirror
invariant. Full ./internal/ai/... test tree passes.
Dead-code sweep. Functions flagged unreachable by golang.org/x/tools/cmd/deadcode
and confirmed unused across pulse, pulse-enterprise, pulse-pro and pulse-mobile by
adversarial cross-repo verification. Cross-module reachability was checked
explicitly (only pkg/ exported symbols are importable by other modules; internal/
packages and _test.go files are not). go build, go vet and test-compile all pass.
- Add DOMPurify sanitization for AI chat markdown rendering (XSS fix)
- Configure DOMPurify to add target=_blank and rel=noopener to links
- Update system prompt to align with command approval policy
- Clarify safe vs destructive commands in prompt
- Improve patrol auto-fix mode guidance with safe operation list
- Add verification requirements for auto-fix actions
- Update observe-only mode to be clearer about read-only restrictions
Phase 3 of Pulse AI differentiation:
Create internal/ai/memory package with:
1. Change Detection (changes.go):
- Tracks infrastructure changes: creation, deletion, config changes
- Detects status changes (started, stopped)
- Detects VM/container migrations between nodes
- Detects CPU/memory configuration changes
- Detects backup completions
- Persists change history to ai_changes.json
- GetChangesSummary for AI context
2. Remediation Logging (remediation.go):
- Records actions taken to fix problems
- Tracks command, output, and outcome
- Links to AI findings via findingID
- GetSimilar finds past similar problems
- GetSuccessfulRemediations for learning
- Persists to ai_remediations.json
3. Type exports (memory_exports.go):
- Clean re-exports from ai package
This enables the AI to say things like:
- 'This VM was migrated 2 hours ago'
- 'Memory was increased from 4GB to 8GB yesterday'
- 'Last time this happened, restarting nginx resolved it'
All tests passing.