Production telemetry (30d) shows Patrol averaging ~9.4 billed AI calls
per run while only ~1-2 come from the analysis passes. The remainder is
the alert auto-resolve review: one QuickAnalysis call per active alert
(>=10min old) on every run, re-asking the model about the same standing
alert every interval even when the trigger condition demonstrably still
holds.
Two structural changes, both resolution-neutral:
- Still-firing gate: skip the model review for alerts whose current
snapshot still shows the trigger condition (metric at/above threshold,
offline resource still offline). A correct review could only answer
KEEP, so the question is not worth a billed call. The gate never
resolves anything locally - uncertain cases (unknown types, missing
resources, unmapped metrics) still go to the model, which retains
sole authority over resolution.
- Batched review: the remaining candidates are reviewed in one model
call per 20 alerts (numbered verdict lines, unparseable -> KEEP,
bare-RESOLVE fallback for single-alert batches) instead of one call
per alert.
Steady-state runs with standing alerts drop from N review calls to 0;
runs where conditions may have cleared pay ceil(K/20) instead of K.
QuickAnalysisRequest gains a TargetType tag (recorded on the usage
event) so cost telemetry can decompose alert-review spend from the
main patrol pass going forward. Contract updated in-commit
(ai-runtime: model-owned resolve direction, cost-gated keep direction,
batched review, fail-safe parsing, alert_autoresolve usage tagging).
Production telemetry (7d, 2026-07-17) shows a persistent tail of live
installs on 6.0.0-rc.x and 6.0.3/6.0.4 while 6.0.5 has been stable since
2026-07-09. Root cause for the reachable stranding class: release
selection trusted GitHub's created_at ordering, and this repo interleaves
v5-line maintenance releases with v6 releases (v5.1.36 was created the
day before v6.0.5). Whenever a v5.1.x release is the most recently
created stable:
- stable channel (getLatestReleaseForChannel) returned the first
non-prerelease in list order -> "no update" for every v6 install
until the next v6 release ships
- the RSS rate-limit fallback returned the first feed entry matching the
channel, same failure, both channels
- the unattended updater trusted /releases/latest, which GitHub defines
as most-recently-created stable -> timer no-ops in the same window
All three now select the highest eligible version: the API path tracks
newest stable + newest prerelease in one pass (prerelease = GitHub flag
OR tag shape), the feed fallback picks the max matching the channel, and
pulse-auto-update.sh scans /releases pairing each tag with its own
draft/prerelease flags before the fail-closed shape filter, keeping
/releases/latest as fallback. helm-chart-* tags fail semver parsing and
are dropped everywhere.
Verified not broken (pinned by new tests): rc-channel installs are
offered the newer stable (6.0.0-rc.6 -> 6.0.5) and move onto the next rc
line when one opens (-> 6.1.0-rc.2); the auto-update prerelease filter
(fail-closed is_prerelease_tag, metadata-flag refusal) still refuses
prerelease targets on stable. Go tests fail on the pre-fix code
(returned 5.1.37 / "no update").
Contract: deployment-installability now pins version-max release
selection with proofs in manager_stranded_upgrade_test.go and
pulse_auto_update_test.go.
Telemetry shows thousands of installs configure an AI provider but almost
none ever use the interactive Assistant. A live first-session exercise
(fresh install, Ollama qwen3:8b quickstart) found why: after enabling,
nothing changes on screen — the launcher and handoff buttons are gated on
sessionCapabilities.assistantEnabled, which was only read at page load;
the empty transcript was blank; and the blessed Ollama+qwen3:8b path
reported Patrol degraded while telling the user to pull the model they
had just selected.
- Setup-modal success now opens the Assistant drawer, and the AI settings
save paths refresh the assistantEnabled capability in place
(aiChatStore.refreshEnabledFromServer) so entry points appear without a
reload; toasts point at the Assistant instead of back at settings.
- The empty transcript owns a plain-language welcome and three suggested
prompts that dispatch as real turns (ASSISTANT_SUGGESTED_PROMPTS).
- Patrol static readiness: the blessed Ollama Patrol model is Ready;
other Ollama models keep the warning, now naming the selected model.
- pulse_summarize fleet argument errors instruct the model to enumerate
resources itself instead of interrogating the operator (observed live:
'how is my machine doing?' ended in a resource-ID elicitation).
Contracts: ai-runtime and frontend-primitives Current State updated.
Tests: full internal/ai + internal/api suites green; vitest ChatMessages,
AISettings, aiChat store, and settingsArchitecture suites green; flow
verified live end-to-end.
Production telemetry (30d) shows the Pulse Intelligence funnel collapsing at
governed actions: 332 installs enabled chat actions but only 23 created an
action plan. Root cause: both action dials default off, and an install in the
default Watch only mode has no forward path — findings on unlocked installs
show neither an Investigate button (gated on non-monitor modes) nor any nudge
(the Pro handoff only renders when the capability is plan-locked), and the
empty Actions inbox never says why it is empty.
- Patrol findings on unlocked Watch-only installs now offer 'Switch to Ask
first' in the expanded row (same slot as the Pro handoff), driving
handleAutonomyChange('approval') so the Investigate action appears in place.
- The empty Actions Open tab now explains that Watch only never queues fixes
and points at the Patrol mode switch, or names the Pro capability on locked
installs (presentation-policy aware, upgrade link suppressed with
hideUpgrade).
- Contracts updated: patrol-intelligence (expanded-row forward-path rule),
unified-resources (Actions calm-state producer explanation), api-contracts
(canonical autonomy read/save reuse), frontend-primitives (guidance stays in
governed presentation helpers).
Verified live on a scratch instance (locked drawer keeps the Pro handoff with
no nudge; empty inbox renders the locked explanation) plus DOM tests for the
unlocked slot, which needs the Pro runtime to exercise live.
Every mock-mode boot (e2e/CI managed backends, qual runs, demo containers)
was sending startup/heartbeat pings describing the synthetic fixture fleet,
registering as real installs in the license-server telemetry DB and
dominating the VMware/Kubernetes/TrueNAS adoption aggregates (346 of 6,225
weekly-active installs matched the mock signature, incl. 120 of 227 on
6.1.0-rc.1).
- internal/telemetry: route startup and heartbeat sends through sendEvent,
which drops the ping while mock.IsMockEnabled() is true; checked per event
so runtime mock toggles take effect immediately. Disclosed in the package
doc and both PRIVACY.md copies.
- scripts/telemetry_adoption_report.py: exclude historical mock-fleet rows
(kubernetes_pods = 120×N with vmware_hosts = 7×N, the internal/mock
fixture template) by default, report the excluded row/install counts, and
add --include-mock-fleet to audit them. Versions through 6.1.0-rc.2 keep
pinging the mock fleet until upgraded, so report-side filtering stays
necessary.
- security-privacy contract: record the mock-mode suppression boundary and
the report-side mock-fleet exclusion as subsystem invariants.
- Tests: Go sendEvent suppression/sending pair against an httptest endpoint;
Python signature-matcher and summarize_rows exclusion coverage.
Cloned VMs that keep the same /etc/machine-id collapse into a single
Docker host in Pulse, with each clone's report silently overwriting the
other (#1584). The server now tracks per-identity hostname and machine
ID observations and flags a conflict when a value flaps back to one
already seen inside a 15-minute window, a signature a one-time rename
never produces. The conflict rides the DockerHost model through unified
resources, and the Docker page shows a warning naming the flapping
hostnames with the machine-id remedy. The warning self-clears once one
clone stops reporting for the window.