mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
63c40ebe5e
Two counters could not answer the question they exist for. pulse_intelligence_patrol_runs_30d was counted from the operator-facing patrol run history, which is capped at MaxPatrolRunHistory (100). On any install patrolling on a normal schedule that cap is reached within hours of a thirty-day window opening, so the field reported the cap rather than the run count, and the calls-per-run ratio derived from it was inflated by a censored denominator. Patrol run history now carries an uncapped daily tally beside the capped list, advanced from a persisted high-water mark so repeated full-list saves cannot double count, pruned to 31 days, and read in preference to the list. An install with no tally yet falls back to the list, so the counter never regresses on upgrade. Schema v8 split agent-side pre-mutation refusals into target-change, prerequisite and contract categories so they would stop hiding in "other". In production all three are zero fleet-wide and "other" still absorbs every refusal, because the dominant reason code is the legacy preflight_refused aggregate recorded when an agent sends no machine reason code at all. Agents older than the typed refusal contract report every refusal that way, so the split is starved rather than broken, and folding the two together made those two states indistinguishable. Schema v9 counts uncoded refusals separately.
418 lines
15 KiB
Go
418 lines
15 KiB
Go
package api
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/telemetry"
|
|
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/aicontracts"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// ApplyUpdateTelemetrySnapshot adds router-owned, content-free update funnel
|
|
// counters to the outbound usage telemetry snapshot.
|
|
func (r *Router) ApplyUpdateTelemetrySnapshot(s *telemetry.Snapshot, now time.Time) {
|
|
if r == nil || s == nil {
|
|
return
|
|
}
|
|
telemetry.ApplyUpdateTelemetrySnapshot(s, r.updateHistory, now)
|
|
}
|
|
|
|
// GetPulseIntelligenceActionTelemetry returns count-only action-governance
|
|
// telemetry for the outbound Pulse Intelligence usage loop. It deliberately drops
|
|
// command text, approval actors/reasons, action outputs, and resource IDs.
|
|
func (r *Router) GetPulseIntelligenceActionTelemetry(since time.Time) telemetry.PulseIntelligenceActionSnapshot {
|
|
var snapshot telemetry.PulseIntelligenceActionSnapshot
|
|
if r == nil || r.resourceHandlers == nil {
|
|
return snapshot
|
|
}
|
|
|
|
for _, orgID := range r.pulseIntelligenceTelemetryOrgIDs() {
|
|
store, err := r.resourceHandlers.getStore(orgID)
|
|
if err != nil || store == nil {
|
|
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to resolve action audit store for telemetry summary")
|
|
continue
|
|
}
|
|
approvedAttemptIDs, approvedSuccessIDs := pulseIntelligenceApprovedActionOutcomeIDs(store, orgID, since)
|
|
rejectedDecisionIDs := pulseIntelligenceRejectedActionDecisionIDs(store, orgID, since)
|
|
approvedDecisionIDs := pulseIntelligenceApprovedActionDecisionIDs(store, orgID, since)
|
|
records, err := store.GetActionAudits("", since, 0)
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to query action audit telemetry summary")
|
|
continue
|
|
}
|
|
recordsByID := make(map[string]unifiedresources.ActionAuditRecord, len(records))
|
|
for _, record := range records {
|
|
if actionID := strings.TrimSpace(record.ID); actionID != "" {
|
|
recordsByID[actionID] = record
|
|
}
|
|
snapshot.ActionPlans30d++
|
|
if pulseIntelligenceActionRequiresApproval(record) {
|
|
snapshot.ApprovalRequests30d++
|
|
}
|
|
if pulseIntelligenceActionWasRejected(record) {
|
|
if actionID := strings.TrimSpace(record.ID); actionID != "" {
|
|
rejectedDecisionIDs[actionID] = struct{}{}
|
|
}
|
|
}
|
|
if pulseIntelligenceActionWasApprovedSince(record, since) {
|
|
if actionID := strings.TrimSpace(record.ID); actionID != "" {
|
|
approvedDecisionIDs[actionID] = struct{}{}
|
|
}
|
|
}
|
|
if pulseIntelligenceApprovedActionAttempt(record) {
|
|
if actionID := strings.TrimSpace(record.ID); actionID != "" {
|
|
approvedAttemptIDs[actionID] = struct{}{}
|
|
}
|
|
}
|
|
if pulseIntelligenceApprovedActionSuccess(record) {
|
|
if actionID := strings.TrimSpace(record.ID); actionID != "" {
|
|
approvedSuccessIDs[actionID] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
snapshot.RejectedActionDecisions30d += len(rejectedDecisionIDs)
|
|
snapshot.ApprovedActionDecisions30d += len(approvedDecisionIDs)
|
|
snapshot.ApprovedActionAttempts30d += len(approvedAttemptIDs)
|
|
snapshot.ApprovedActionSuccesses30d += len(approvedSuccessIDs)
|
|
accumulatePulseIntelligenceApprovedActionOutcomes(&snapshot, store, orgID, approvedAttemptIDs, approvedSuccessIDs, recordsByID, time.Now().UTC())
|
|
}
|
|
|
|
return snapshot
|
|
}
|
|
|
|
// pulseIntelligenceStuckExecutingThreshold separates an in-flight dispatch
|
|
// from an abandoned one. The longest legitimate typed dispatch transport wait
|
|
// is 30 minutes, so an executing record untouched for longer is stuck.
|
|
const pulseIntelligenceStuckExecutingThreshold = time.Hour
|
|
|
|
// pulseIntelligenceReasonCodePattern bounds exported failure reason codes to
|
|
// closed machine-code shape so telemetry stays content-free even if a future
|
|
// executor misuses the reason-code field.
|
|
var pulseIntelligenceReasonCodePattern = regexp.MustCompile(`^[a-z0-9_.-]{1,64}$`)
|
|
|
|
// accumulatePulseIntelligenceApprovedActionOutcomes attributes every approved
|
|
// attempt to exactly one success, failure, stuck, in-flight, or unclassified
|
|
// bucket. It also records fixed refusal categories and the machine reason code
|
|
// of the most recent failure.
|
|
func accumulatePulseIntelligenceApprovedActionOutcomes(snapshot *telemetry.PulseIntelligenceActionSnapshot, store unifiedresources.ResourceStore, orgID string, attemptIDs, successIDs map[string]struct{}, recordsByID map[string]unifiedresources.ActionAuditRecord, now time.Time) {
|
|
var lastFailureAt time.Time
|
|
for actionID := range attemptIDs {
|
|
_, isSuccess := successIDs[actionID]
|
|
record, ok := recordsByID[actionID]
|
|
if !ok {
|
|
fetched, found, err := store.GetActionAudit(actionID)
|
|
if err != nil || !found {
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to resolve action audit for failure-cause telemetry summary")
|
|
}
|
|
// A success outcome is already accounted by its lifecycle event;
|
|
// only unresolved non-success attempts belong in unclassified.
|
|
if !isSuccess {
|
|
snapshot.ApprovedActionUnclassified30d++
|
|
}
|
|
continue
|
|
}
|
|
record = fetched
|
|
}
|
|
if isSuccess {
|
|
if pulseIntelligenceVerifiedFindingResolution(record) {
|
|
snapshot.VerifiedFindingResolutions30d++
|
|
}
|
|
continue
|
|
}
|
|
cause, reason := pulseIntelligenceApprovedActionFailureCause(record, now)
|
|
switch cause {
|
|
case "pre_dispatch":
|
|
snapshot.ApprovedActionFailuresPreDispatch30d++
|
|
switch pulseIntelligenceApprovedActionRefusalCategory(reason) {
|
|
case "plan_stale":
|
|
snapshot.ApprovedActionRefusalsPlanStale30d++
|
|
case "policy":
|
|
snapshot.ApprovedActionRefusalsPolicy30d++
|
|
case "capability":
|
|
snapshot.ApprovedActionRefusalsCapability30d++
|
|
case "target_changed":
|
|
snapshot.ApprovedActionRefusalsTargetChanged30d++
|
|
case "prerequisite":
|
|
snapshot.ApprovedActionRefusalsPrerequisite30d++
|
|
case "contract":
|
|
snapshot.ApprovedActionRefusalsContract30d++
|
|
case "uncoded":
|
|
snapshot.ApprovedActionRefusalsUncoded30d++
|
|
default:
|
|
snapshot.ApprovedActionRefusalsOther30d++
|
|
}
|
|
case "execution":
|
|
snapshot.ApprovedActionFailuresExecution30d++
|
|
case "unverified":
|
|
snapshot.ApprovedActionFailuresUnverified30d++
|
|
case "stuck_executing":
|
|
snapshot.ApprovedActionStuckExecuting30d++
|
|
case "in_flight":
|
|
snapshot.ApprovedActionInFlight30d++
|
|
continue
|
|
default:
|
|
snapshot.ApprovedActionUnclassified30d++
|
|
continue
|
|
}
|
|
if record.UpdatedAt.After(lastFailureAt) {
|
|
lastFailureAt = record.UpdatedAt
|
|
snapshot.ApprovedActionLastFailureReason30d = reason
|
|
}
|
|
}
|
|
}
|
|
|
|
// pulseIntelligenceApprovedActionFailureCause classifies an approved attempt
|
|
// that is not a verified success into a coarse cause bucket plus the specific
|
|
// machine reason code. A recently-executing record is explicitly classified as
|
|
// in flight because it may yet succeed.
|
|
func pulseIntelligenceApprovedActionFailureCause(record unifiedresources.ActionAuditRecord, now time.Time) (string, string) {
|
|
switch record.State {
|
|
case unifiedresources.ActionStateExecuting:
|
|
if record.UpdatedAt.IsZero() || now.Sub(record.UpdatedAt) >= pulseIntelligenceStuckExecutingThreshold {
|
|
return "stuck_executing", "stuck_executing"
|
|
}
|
|
return "in_flight", ""
|
|
case unifiedresources.ActionStateFailed:
|
|
truth := unifiedresources.CanonicalActionResultV2(record)
|
|
if truth.Execution.Status == unifiedresources.ActionExecutionNotRun {
|
|
return "pre_dispatch", pulseIntelligenceSanitizedReasonCode(truth.Execution.ReasonCode, "pre_dispatch_refused")
|
|
}
|
|
return "execution", pulseIntelligenceSanitizedReasonCode(truth.Execution.ReasonCode, "execution_failed")
|
|
case unifiedresources.ActionStateCompleted:
|
|
truth := unifiedresources.CanonicalActionResultV2(record)
|
|
return "unverified", pulseIntelligenceSanitizedReasonCode(truth.Verification.ReasonCode, "verification_unconfirmed")
|
|
default:
|
|
return "", ""
|
|
}
|
|
}
|
|
|
|
func pulseIntelligenceApprovedActionRefusalCategory(reason string) string {
|
|
switch reason {
|
|
case "plan_drift", "action_plan_expired", "action_replan_required":
|
|
return "plan_stale"
|
|
case "resource_remediation_locked", "policy_authorization_expired",
|
|
"policy_authorization_invalid", "policy_authorization_revoked", "action_emergency_stop":
|
|
return "policy"
|
|
case "action_dry_run_only", "action_execution_unavailable", "agent_capability_unavailable":
|
|
return "capability"
|
|
case "target_state_changed", "target_precondition_failed", "package_inventory_changed", "cleanup_inventory_changed":
|
|
return "target_changed"
|
|
case "target_inspection_unavailable", "package_manager_busy", "package_index_refresh_failed",
|
|
"package_preflight_failed", "package_manager_unhealthy", "cleanup_preflight_failed":
|
|
return "prerequisite"
|
|
case "action_contract_invalid":
|
|
return "contract"
|
|
case actionRefusalUncoded, "pre_dispatch_refused":
|
|
// The refusal carried no machine reason code, so there is nothing to
|
|
// categorize. This is what an agent older than the typed refusal
|
|
// contract reports, and it must stay separable from a code this server
|
|
// simply does not recognise: the first says the split is waiting on
|
|
// agent rollout, the second says the split is wrong.
|
|
return "uncoded"
|
|
default:
|
|
return "other"
|
|
}
|
|
}
|
|
|
|
func pulseIntelligenceVerifiedFindingResolution(record unifiedresources.ActionAuditRecord) bool {
|
|
return isPatrolActionOrigin(record.Origin) &&
|
|
pulseIntelligenceApprovedActionSuccess(record) &&
|
|
patrolOutcomeForActionAudit(record) == aicontracts.OutcomeFixVerified
|
|
}
|
|
|
|
func pulseIntelligenceSanitizedReasonCode(code, fallback string) string {
|
|
code = strings.TrimSpace(code)
|
|
if pulseIntelligenceReasonCodePattern.MatchString(code) {
|
|
return code
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func (r *Router) pulseIntelligenceTelemetryOrgIDs() []string {
|
|
if r == nil || r.multiTenant == nil {
|
|
return []string{"default"}
|
|
}
|
|
orgs, err := r.multiTenant.ListOrganizations()
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("Unable to list organizations for telemetry summary")
|
|
return []string{"default"}
|
|
}
|
|
seen := map[string]struct{}{}
|
|
orgIDs := make([]string, 0, len(orgs))
|
|
for _, org := range orgs {
|
|
if org == nil {
|
|
continue
|
|
}
|
|
orgID := strings.TrimSpace(org.ID)
|
|
if orgID == "" {
|
|
orgID = "default"
|
|
}
|
|
if _, ok := seen[orgID]; ok {
|
|
continue
|
|
}
|
|
seen[orgID] = struct{}{}
|
|
orgIDs = append(orgIDs, orgID)
|
|
}
|
|
if len(orgIDs) == 0 {
|
|
return []string{"default"}
|
|
}
|
|
return orgIDs
|
|
}
|
|
|
|
func pulseIntelligenceActionRequiresApproval(record unifiedresources.ActionAuditRecord) bool {
|
|
if record.Plan.RequiresApproval {
|
|
return true
|
|
}
|
|
return len(record.Approvals) > 0 || record.State == unifiedresources.ActionStatePending ||
|
|
record.State == unifiedresources.ActionStateApproved || record.State == unifiedresources.ActionStateRejected
|
|
}
|
|
|
|
func pulseIntelligenceApprovedActionOutcomeIDs(store unifiedresources.ResourceStore, orgID string, since time.Time) (map[string]struct{}, map[string]struct{}) {
|
|
attemptIDs := map[string]struct{}{}
|
|
successIDs := map[string]struct{}{}
|
|
if store == nil {
|
|
return attemptIDs, successIDs
|
|
}
|
|
events, err := store.GetActionLifecycleEvents("", since, 0)
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to query action lifecycle telemetry summary")
|
|
return attemptIDs, successIDs
|
|
}
|
|
for _, event := range events {
|
|
if !pulseIntelligenceActionLifecycleIndicatesAttempt(event) {
|
|
continue
|
|
}
|
|
actionID := strings.TrimSpace(event.ActionID)
|
|
if actionID == "" {
|
|
continue
|
|
}
|
|
record, ok, err := store.GetActionAudit(actionID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to resolve action audit for lifecycle telemetry summary")
|
|
continue
|
|
}
|
|
if ok && pulseIntelligenceActionWasApproved(record) {
|
|
attemptIDs[actionID] = struct{}{}
|
|
if event.State == unifiedresources.ActionStateCompleted && pulseIntelligenceApprovedActionSuccess(record) {
|
|
successIDs[actionID] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
return attemptIDs, successIDs
|
|
}
|
|
|
|
func pulseIntelligenceRejectedActionDecisionIDs(store unifiedresources.ResourceStore, orgID string, since time.Time) map[string]struct{} {
|
|
return pulseIntelligenceActionDecisionIDs(store, orgID, since, unifiedresources.ActionStateRejected, "rejected", pulseIntelligenceActionWasRejected)
|
|
}
|
|
|
|
func pulseIntelligenceApprovedActionDecisionIDs(store unifiedresources.ResourceStore, orgID string, since time.Time) map[string]struct{} {
|
|
return pulseIntelligenceActionDecisionIDs(store, orgID, since, unifiedresources.ActionStateApproved, "approved", pulseIntelligenceActionWasApproved)
|
|
}
|
|
|
|
func pulseIntelligenceActionDecisionIDs(store unifiedresources.ResourceStore, orgID string, since time.Time, state unifiedresources.ActionState, decisionLabel string, auditMatches func(unifiedresources.ActionAuditRecord) bool) map[string]struct{} {
|
|
decisionIDs := map[string]struct{}{}
|
|
if store == nil {
|
|
return decisionIDs
|
|
}
|
|
events, err := store.GetActionLifecycleEvents("", since, 0)
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("org_id", orgID).Msgf("Unable to query %s action lifecycle telemetry summary", decisionLabel)
|
|
return decisionIDs
|
|
}
|
|
for _, event := range events {
|
|
if event.State != state {
|
|
continue
|
|
}
|
|
actionID := strings.TrimSpace(event.ActionID)
|
|
if actionID == "" {
|
|
continue
|
|
}
|
|
record, ok, err := store.GetActionAudit(actionID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("org_id", orgID).Msgf("Unable to resolve %s action audit for telemetry summary", decisionLabel)
|
|
continue
|
|
}
|
|
if ok && auditMatches(record) {
|
|
decisionIDs[actionID] = struct{}{}
|
|
}
|
|
}
|
|
return decisionIDs
|
|
}
|
|
|
|
func pulseIntelligenceApprovedActionAttempt(record unifiedresources.ActionAuditRecord) bool {
|
|
if !pulseIntelligenceActionWasApproved(record) {
|
|
return false
|
|
}
|
|
switch record.State {
|
|
case unifiedresources.ActionStateExecuting, unifiedresources.ActionStateCompleted, unifiedresources.ActionStateFailed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func pulseIntelligenceApprovedActionSuccess(record unifiedresources.ActionAuditRecord) bool {
|
|
if record.State != unifiedresources.ActionStateCompleted {
|
|
return false
|
|
}
|
|
return pulseIntelligenceActionVerifiedOutcome(record)
|
|
}
|
|
|
|
func pulseIntelligenceActionVerifiedOutcome(record unifiedresources.ActionAuditRecord) bool {
|
|
if !pulseIntelligenceActionWasApproved(record) {
|
|
return false
|
|
}
|
|
truth := unifiedresources.CanonicalActionResultV2(record)
|
|
return truth.Execution.Status == unifiedresources.ActionExecutionSucceeded && truth.Verification.Status == unifiedresources.ActionVerificationConfirmed
|
|
}
|
|
|
|
func pulseIntelligenceActionLifecycleIndicatesAttempt(event unifiedresources.ActionLifecycleEvent) bool {
|
|
switch event.State {
|
|
case unifiedresources.ActionStateExecuting, unifiedresources.ActionStateCompleted, unifiedresources.ActionStateFailed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func pulseIntelligenceActionWasApproved(record unifiedresources.ActionAuditRecord) bool {
|
|
if record.State == unifiedresources.ActionStateApproved {
|
|
return true
|
|
}
|
|
for _, approval := range record.Approvals {
|
|
if approval.Outcome == unifiedresources.OutcomeApproved {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func pulseIntelligenceActionWasApprovedSince(record unifiedresources.ActionAuditRecord, since time.Time) bool {
|
|
for _, approval := range record.Approvals {
|
|
if approval.Outcome != unifiedresources.OutcomeApproved {
|
|
continue
|
|
}
|
|
if approval.Timestamp.IsZero() || approval.Timestamp.Before(since) {
|
|
continue
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func pulseIntelligenceActionWasRejected(record unifiedresources.ActionAuditRecord) bool {
|
|
if record.State == unifiedresources.ActionStateRejected {
|
|
return true
|
|
}
|
|
for _, approval := range record.Approvals {
|
|
if approval.Outcome == unifiedresources.OutcomeRejected {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|