mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
3420fcdee4
The 2026-09-01 Patrol assessment could not answer four questions from telemetry: what share of Patrol installs run local versus cloud models, what Patrol costs an install per month (issue 1789 estimates ~104k input tokens per full run at ~5.5 runs a day, above the Pro fee on non-Flash tiers), how the 240 of 255 fleet investigations that produced no plan actually ended, and which effective Patrol mode an install runs. Schema v17 adds closed buckets only. ai_provider_class classifies the Patrol model route (none, local, cloud_byok, cloud_subscription, hosted_quickstart, unknown) with a syntactic private-host check on custom OpenAI-compatible endpoints; it never resolves DNS or carries a provider, model, endpoint, or account. pulse_intelligence_patrol_autonomy_level is the effective level after licence and Autopilot gating. The two token fields bucket the existing local usage ledger's Patrol events, so exact counts and prices stay on the install. Thirteen investigation outcome counters partition the findings already counted as investigated, one bucket per finding, adding no finding, resource, or session identity. The strings are never omitted, so an empty value can only mean a pre-v17 sender; the receiver stores those as unknown. PRIVACY.md, its shipped mirror, the Settings preview interface, and the security-privacy and api-contracts subsystem notes carry the disclosure, and the coverage gap is registered in status.json. The agent-lifecycle and storage-recovery contracts name internal/api under their extension points, so both record that this telemetry is adjacent adoption analytics and never agent or storage state. The stable E2E tier pins the public schema version, so the disclosure spec moves to 17, and the Settings preview interface change carries a browser receipt from a scratch build of this tree at 1280x800 and 390x844. status.json also drops an identical duplicate of the ai-provider-guided-setup coverage-gap record that two merges each appended, which the private governance audit rejects. The bucket for a legacy config still pointing at the retired Pulse-hosted route is named hosted_legacy: the shipped privacy document discloses the vocabulary verbatim and the frontend copy contract keeps that document free of the retired hosted quickstart wording, so the telemetry package now pins the vocabulary at the source.
173 lines
5.9 KiB
Go
173 lines
5.9 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
// PulseIntelligenceAIUsageEvidence is the content-free subset of AI usage
|
|
// history used to prove Pulse Intelligence loop collaboration.
|
|
type PulseIntelligenceAIUsageEvidence struct {
|
|
AssistantAICalls int
|
|
AssistantContextAICalls int
|
|
AssistantToolCalls int
|
|
PatrolAICalls int
|
|
// PatrolInputTokens and PatrolOutputTokens are the exact local Patrol
|
|
// token totals for the window. They are bucketed before export; the
|
|
// exact figures never leave the install.
|
|
PatrolInputTokens int64
|
|
PatrolOutputTokens int64
|
|
}
|
|
|
|
// PulseIntelligenceAIUsageEvidenceFromHistory projects AI usage history into
|
|
// the same content-free Pulse Intelligence evidence used by telemetry and the
|
|
// agent operations-loop status endpoint.
|
|
func PulseIntelligenceAIUsageEvidenceFromHistory(history *config.AIUsageHistoryData, since time.Time) PulseIntelligenceAIUsageEvidence {
|
|
var evidence PulseIntelligenceAIUsageEvidence
|
|
if history == nil {
|
|
return evidence
|
|
}
|
|
for _, event := range history.Events {
|
|
if event.Timestamp.IsZero() || event.Timestamp.Before(since) {
|
|
continue
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(event.UseCase)) {
|
|
case "chat":
|
|
evidence.AssistantAICalls++
|
|
if PulseIntelligenceAssistantContextEvent(event) {
|
|
evidence.AssistantContextAICalls++
|
|
}
|
|
if event.ToolCallCount > 0 {
|
|
evidence.AssistantToolCalls += event.ToolCallCount
|
|
}
|
|
case "patrol":
|
|
evidence.PatrolAICalls++
|
|
if event.InputTokens > 0 {
|
|
evidence.PatrolInputTokens += int64(event.InputTokens)
|
|
}
|
|
if event.OutputTokens > 0 {
|
|
evidence.PatrolOutputTokens += int64(event.OutputTokens)
|
|
}
|
|
}
|
|
}
|
|
return evidence
|
|
}
|
|
|
|
// PulseIntelligenceAssistantContextEvent reports whether a chat usage event
|
|
// carried governed context. It deliberately ignores prompt text and responses.
|
|
func PulseIntelligenceAssistantContextEvent(event config.AIUsageEventRecord) bool {
|
|
return strings.TrimSpace(event.ContextScope) != "" ||
|
|
strings.TrimSpace(event.TargetType) != "" ||
|
|
strings.TrimSpace(event.TargetID) != "" ||
|
|
strings.TrimSpace(event.FindingID) != ""
|
|
}
|
|
|
|
// PulseIntelligenceExternalAgentEvidence is the content-free subset of
|
|
// authenticated agent/MCP route activity used to prove external collaboration.
|
|
//
|
|
// Interpretation constraint: the agent_api surface counts any suitably scoped
|
|
// API-token request to manifest-listed routes — including plain REST consumers
|
|
// like scripts and dashboards hitting GET /api/config/nodes or
|
|
// /api/ai/patrol/findings — so Used does NOT imply a BYO AI agent. Only
|
|
// MCPAdapterUsed (the pulse_mcp surface header) certainly identifies an MCP
|
|
// client. Do not read Used-vs-MCPAdapterUsed as an adoption funnel of the
|
|
// same population.
|
|
type PulseIntelligenceExternalAgentEvidence struct {
|
|
Used bool
|
|
MCPAdapterUsed bool
|
|
ContextRequests int
|
|
EventStreamRequests int
|
|
ProvisioningRequests int
|
|
OperatorStateRequests int
|
|
FindingRequests int
|
|
ActionRequests int
|
|
}
|
|
|
|
// CollaborationActive reports whether an external agent or MCP adapter used a
|
|
// manifest-published Pulse Intelligence capability in the evidence window.
|
|
func (e PulseIntelligenceExternalAgentEvidence) CollaborationActive() bool {
|
|
return e.Used ||
|
|
e.MCPAdapterUsed ||
|
|
e.ContextRequests > 0 ||
|
|
e.EventStreamRequests > 0 ||
|
|
e.ProvisioningRequests > 0 ||
|
|
e.OperatorStateRequests > 0 ||
|
|
e.FindingRequests > 0 ||
|
|
e.ActionRequests > 0
|
|
}
|
|
|
|
// CollaborationCount returns a coarse activity count for step-level rollups.
|
|
func (e PulseIntelligenceExternalAgentEvidence) CollaborationCount() int {
|
|
count := e.ContextRequests +
|
|
e.EventStreamRequests +
|
|
e.ProvisioningRequests +
|
|
e.OperatorStateRequests +
|
|
e.FindingRequests +
|
|
e.ActionRequests
|
|
if count > 0 {
|
|
return count
|
|
}
|
|
if e.Used || e.MCPAdapterUsed {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// PulseIntelligenceExternalAgentEvidenceFromHistory projects external-agent
|
|
// activity history into content-free collaboration evidence.
|
|
func PulseIntelligenceExternalAgentEvidenceFromHistory(history *config.ExternalAgentActivityHistoryData, since time.Time) PulseIntelligenceExternalAgentEvidence {
|
|
var evidence PulseIntelligenceExternalAgentEvidence
|
|
if history == nil {
|
|
return evidence
|
|
}
|
|
for _, event := range history.Events {
|
|
if event.Timestamp.IsZero() || event.Timestamp.Before(since) {
|
|
continue
|
|
}
|
|
if !PulseIntelligenceExternalAgentActivitySurface(event.Surface) {
|
|
continue
|
|
}
|
|
evidence.Used = true
|
|
if strings.TrimSpace(event.Surface) == config.ExternalAgentActivitySurfacePulseMCP {
|
|
evidence.MCPAdapterUsed = true
|
|
}
|
|
evidence.ApplyActivity(event.Activity)
|
|
}
|
|
return evidence
|
|
}
|
|
|
|
// PulseIntelligenceExternalAgentActivitySurface reports whether a persisted
|
|
// activity surface is part of the governed external-agent contract.
|
|
func PulseIntelligenceExternalAgentActivitySurface(surface string) bool {
|
|
switch strings.TrimSpace(surface) {
|
|
case config.ExternalAgentActivitySurfaceAgentAPI, config.ExternalAgentActivitySurfacePulseMCP:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ApplyActivity increments the matching content-free external-agent activity
|
|
// bucket.
|
|
func (e *PulseIntelligenceExternalAgentEvidence) ApplyActivity(activity string) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
switch strings.TrimSpace(activity) {
|
|
case config.ExternalAgentActivityResourceContext, config.ExternalAgentActivityFleetContext:
|
|
e.ContextRequests++
|
|
case config.ExternalAgentActivityEventStream:
|
|
e.EventStreamRequests++
|
|
case config.ExternalAgentActivityProvisioning:
|
|
e.ProvisioningRequests++
|
|
case config.ExternalAgentActivityOperatorState:
|
|
e.OperatorStateRequests++
|
|
case config.ExternalAgentActivityFindingList, config.ExternalAgentActivityFindingDecision:
|
|
e.FindingRequests++
|
|
case config.ExternalAgentActivityActionPlan, config.ExternalAgentActivityActionDecision, config.ExternalAgentActivityActionExecute:
|
|
e.ActionRequests++
|
|
}
|
|
}
|