Preserve contradictory rows before monitoring constructs its endpoint trust map. A final matching row must not mask earlier disagreement during saved-view consolidation. Cover both merge orders and retain the existing agreeing-format path; strengthen lifecycle configuration ownership checks.
Change-source: pulse-maintainer
Keep the current clock and stored-History readout alongside canonical row
selection and resource-owned drawer state.
Use one Git inheritance reader for browser and canonical qualification.
Inherited modes and objects keep their parent proof, while new resolutions
and mode changes remain subject to current contract and proof requirements.
Range checks still qualify the complete proposed change.
Refs #1669
Pro sells Patrol investigation, but every install starts in Watch only
and stays there unless the operator finds the mode control. In the paid
telemetry read on 12 October, 20 of 35 Pro installs with Patrol on ran
Watch only, and none of those 20 ran an investigation in 30 days, so
the feature they paid for never ran.
An install that never saved a Patrol mode now runs Ask first when it
holds a positive investigation licence. Patrol investigates new and
re-detected warning and critical findings and proposes a fix; nothing
changes without approval. The default only applies to a positive
licence answer: a missing licence checker counts as unlicensed, so a
startup window cannot spend provider budget on a free install. Free
installs and explicit choices are unchanged.
The Patrol header says so once, with the cost (the operator's AI
provider and 30-day budget) and two actions that save Ask first or
Watch only. The notice never renders on a plan-locked install. The
header also stops claiming "Watching your infrastructure" while Patrol
cannot run yet and reads "Patrol is not running yet" instead.
Use unified resource IDs for workload rows and selection so moving a guest
between Proxmox nodes retains its open drawer and History range. Resolve
existing bookmarks through the registry instead of rebuilding client IDs.
Migrate guest metadata before accepted inventory publication and retain its
operator fields in memory if persistence fails. Native metadata and metrics
coordinates remain explicit compatibility inputs. Operator policy and paid
execution behavior remain intact.
Register the focused publication and writer proofs once. Check proof
acceptance and rejection without copying the registry catalog into tests.
Refs #1669
Reverts #2863 before any release ships it. The live Pro pricing and
Patrol pages, in English, German and Spanish, sell both modes ("applies
safe fixes in the mode you choose: Ask first, Safe auto-fix, or
Autopilot"), and paying installs chose them. Shipping the retirement
would have silently downgraded those installs while the sales page still
promised the modes.
If the modes are retired, that needs a dated notice: stop selling them
first, warn installs on those modes in-app, and then re-ship the change.
It should not arrive through an engineering cleanup.
The receipt #2863 landed stays in place; this commit adds the receipt
for the restored content.
Patrol could run some fixes on its own in two paid modes: assisted (Safe
auto-fix) and full (Autopilot, behind a server-owned acknowledgement).
Schema-v17 telemetry for the week to 2026-10-11 counts 1 assisted and 2
full installs out of 612 reporting Patrol, 4 executed Patrol actions in
30 days, all on one install, and no GitHub request for more autonomy,
while the automatic path carried its own policy leases, acknowledgement
store, per-resource allowlist, release gate and Colima lab. Investigation
is what paid installs use, so it stays.
Patrol now has two modes. Watch only reports findings. Ask first
investigates and proposes fixes, and every fix waits in Actions for an
operator to approve and run it.
- A saved assisted, full or autonomous level reads as approval, and the
update routes refuse those levels with 400 invalid_autonomy_level.
- The core Patrol action broker only plans: Plan and Submit persist the
plan and return. ExecuteUnderPolicy, the policy authorization lease,
the policy admission store methods, the Autopilot acknowledgement
persistence, routes and dialog, and the per-resource automatic-action
allowlist (store column now written as NULL) are gone.
GetPatrolFullModeUnlocked stays on the extension contract and reports
false.
- Historical action records keep their policy lease and tenant/resource
reason codes decodable, labelled as retired modes.
- The Patrol inbox no longer splits issues into "continuing without a
decision"; every current issue is an operator decision.
- Release gate RG-06 now proves Patrol never executes without an operator
decision (integration tier) instead of limited unattended autonomy,
and RG-02/RG-04 point at the surviving human-admission proofs.
- Pro plan copy, AI docs, privacy telemetry wording and the subsystem
contracts describe the two modes.
AI Discovery ran a separate model-backed service scanner beside Patrol
and the Assistant: its own store, scheduler, model route, API, tool and
drawer UI. No telemetry counter ever measured its use (the telemetry
discovery_enabled flag OR'd it with network subnet discovery), the
maintainer judged it a flop, and the context it produced is already
available to the models through pulse_query, pulse_read and the
unified-resource context sections.
This deletes internal/servicediscovery, internal/ai/infradiscovery, the
pulse_discovery tool, the /api/discovery routes, the mock Discovery
fixtures, the Discovery tabs in the resource, guest, node and Docker host
drawers, the identified-service cards, readiness badges, provenance
markers, suggested web-interface URLs, the Workloads AI Context column,
the chat "Discovery is off" hint and the AI Service Context settings page.
The old settings route redirects to Assistant settings. The model-bound
sanitizer allow-list existed only to pass Discovery context through, so it
goes too, along with the CopyableCodeRow primitive that only the
Discovery tab used.
Network subnet discovery (/api/discover, the Infrastructure discovery
settings) and the discoveryTarget agent coordinates are separate features
and stay. Stored discovery_enabled, discovery_interval_hours and
discovery_model AI settings are ignored on load; the settings API decoder
is lenient, so older frontends saving those fields still succeed.
Several Off toggles on the Thresholds page did nothing or did the
opposite, because each layer read 0, -1 and "missing" differently:
- PMG and Docker restart/memory-limit defaults: the toggle sends -1
and NormalizePMGDefaults/NormalizeDockerDefaults turned anything <= 0
back into the factory value, so Off reverted on save. The Docker
restart-loop and memory-limit checks also had no Off at all.
- Backup and snapshot critical age: the editor sanitizers and the save
payload raised a 0 critical to the warning age, so turning critical
off made backups go critical at the warning age.
- A Docker container override saved for CPU alone carried
DisableConnectivity=false and re-enabled that container's state
alerts over a disabled Docker default.
- Per-server PMG cells, PMG bulk edit and per-container restart and
memory-limit cells wrote fields ThresholdConfig does not have, so
the backend dropped them on save.
0 is now Off for these counts; negative values (the toggle's -1) are
stored as 0. Keys an older alerts.json or an import omits decode as
ThresholdCountUnset and take the factory value, so existing installs
keep their alerts; persistence resolves the marker before writing. A
PMG quarantine growth level with one half Off turns that level off
instead of invalidating the spec and skipping the whole check. Docker
container overrides re-enable state alerts only with their own
severity, as applyThresholdOverride does elsewhere. The frontend keeps
a 0 critical age Off, the group header says "Restart loop off", and
rows only offer metrics an override can store.
AI Patrol's threshold adapter still maps an Off alert threshold to its
factory value; that is a Patrol product decision and is left as is.
Settings > Assistant offered a Protected guests list (protected_guests)
that was saved and pushed into the Assistant tool executor, but its only
reader was executeControlGuest, which nothing calls. pulse_control plans
through the shared action lifecycle and never consulted the list, so a
guest named in it (by VMID and by name) was planned like any other. The
"legacy" label added in #2765 states that limit; this removes the
setting instead of keeping a field that cannot do its job.
The canonical way to keep Pulse off a resource is its operator lock
(Never auto-remediate, or a Retired lifecycle). It is keyed by canonical
resource id, applies to every platform, and the action lifecycle refuses
dispatch for it even for approved plans. The one gap was planning: the
lifecycle plans without reading operator state, so the Assistant could
leave a plan in Actions that can never run. assistantTypedActionPlanner
now reads the state through the tenant-scoped lifecycle store (the same
read Patrol's broker uses) and declines before Plan, and pulse_control
reports the block with how to clear it. A state that cannot be read does
not refuse planning, because dispatch re-checks.
Removed: AIConfig.ProtectedGuests and its getter, the settings API
field, the executor field/setter/config, the chat service plumbing, the
Settings field and form state, and the legacy copy, pins and prose from
#2765. A stored value still loads (tolerant decode) and is dropped the
next time the settings are written; older PUT bodies that send the key
are ignored. The Settings panel points at the guest's Manage tab.
Project interactive Assistant controls to Read-only and Ask first without migrating legacy stored preferences or relaxing entitlement, legacy command approval, scoped execution or Patrol policy. Preserve stored policy even when an older client echoes the projected mode on an unrelated save. Explain saved Actions plans and operator review in existing Settings, Chat and help.
Add saved-client and encrypted-config compatibility, authenticated plan-only access, active-stream and scope regression controls with parent comparisons. Record inspected final-byte desktop/phone browser proof and the affected subsystem contracts.
Change-source: pulse-maintainer
Gate mail, queue and quarantine reads at the owning poller. Preserve legacy mail defaults while persisting explicit all-off scope, keep missing readings out of alert recovery and reject absent PMG object data. Partial queue observations may prove the highest configured severity but cannot resolve or downgrade alerts. Retain version connectivity separately from downstream poll failure and cover scope, persistence, pause, in-flight and adverse-read boundaries.
Change-source: pulse-maintainer
Additively merge upstream 1ac600ec2b without changing accepted commit identities. Compose the shared filesystem threshold resolver with the retained explicit host and linked-node override precedence and canonical metric release; extend the existing regression across configuration save and next report. Keep upstream live-config ownership and Patrol cleanup, alongside all reviewed monitoring work.
Change-source: pulse-maintainer
Stacked on #2696, which is stacked on #2692. The unique commit is the
last one, 539eadfab2.
A config save re-judges every active alert
(`reevaluateActiveAlertsLocked`). Filesystem usage alerts were re-judged
against thresholds their evaluator never uses:
- **Agent filesystems** (`agent:<host>/disk:<label>`) were judged by the
host disk threshold. `CheckHost` judges them by the filesystem's own
override, then the `DiskFillByType` threshold for an inferred hardware
type, then the host threshold.
- **Guest filesystems** (`<guestID>-disk-<key>`) were resolved by the
filesystem's resource ID. That ID matches no `guest-disk:` override and
names no guest, so the guest's own override was missed too.
Example: a filesystem override with trigger 70 raises an alert at 80
under a default of 90. Saving any unrelated alert setting resolved the
alert and sent a recovery notification, and the next poll raised it
again as a new alert.
`CheckHost` and `CheckGuest` now resolve a filesystem's threshold
through `hostDiskUsageThresholdNoLock` /
`guestDiskUsageThresholdNoLock`, and reevaluation calls the same
helpers:
- `resolveHostAlertThresholdsNoLock` applies the host helper for agent
filesystem alerts.
- The new `resolveGuestAlertThresholdsNoLock` resolves a guest
filesystem alert's guest by the ID `parseGuestAlertIdentity` reads from
its resource ID.
Both helpers return override and per-type thresholds as owned copies, so
#2696's rule that evaluation never normalizes the live config still
holds.
`TestConfigSaveJudgesFilesystemAlertsByTheirOwnThreshold` (in
`threshold_resolution_shared_test.go`) covers four cases:
- an agent filesystem override
- an NVMe fill threshold
- a guest filesystem override
- a guest-wide override on a filesystem alert
With `host.go`, `guest.go` and `config_runtime.go` overlaid by their
origin/main or #2696 copies, all four fail at the unchanged save. `go
test ./internal/alerts/...` passes, and the new test plus #2696's
`TestAlertEvaluationLeavesTheLiveConfigToTheLock` pass under `-race`.
Known limits, unchanged by this PR:
- Custom rules that filter on live metrics still see no readings during
reevaluation.
- `pulse-relaxed` is not applied during reevaluation.
- The mock generator's dash-separated guest IDs don't parse as guest
identities.
Retain the exact Core candidate and its race, attribution and legacy-reference proof; native recovery remains separately owned.
Change-source: pulse-maintainer
SaveAlertConfig normalized the config it was handed in place, and its
callers hand it alerts.Manager.GetConfig(), a shallow copy whose maps
and threshold pointers were the running config's own. Alert activation
and the alert identity migration therefore wrote the live delay map and
threshold pointers outside the manager lock while evaluation read them.
The shallow copy also let UpdateConfig, which normalizes the override
map it is handed in place, rewrite the live overrides while
GET /api/alerts/config encoded another snapshot. A concurrent map read
or iteration plus a write is a fatal Go runtime error, so these paths
could kill Pulse.
AlertConfig.Clone is a typed deep copy. GetConfig returns one, so
callers own what they encode, persist and edit, and SaveAlertConfig
clones before normalizing, because a config just applied with
UpdateConfig is the live config. GetConfig has no per-resource caller
(once per poll cycle, backup check, request or Patrol run); a clone
with 1,000 overrides takes about 0.3 ms. A test fills every field and
fails when a new map, slice or pointer is not copied.
Persistence also stored and loaded an explicit 0 type-level alert delay
("no delay") as the 5-second factory delay, which the noisy-gauge rule
stretches to 300 seconds for memory and temperature, while the manager
keeps 0. The shared map write was what made the running manager agree
with the file; without it, memory would say 0 and disk 5. Persistence
now keeps 0 on save and load like NormalizeTimeThresholds, and only a
missing or negative delay takes the default. Pulse saves never wrote a
0, and the first v6 start re-saves alerts.json, so only a newly sent or
hand-edited 0 changes meaning.
On main, a reproducer taking the activation, migration and GET steps
against live evaluation died with "fatal error: concurrent map
iteration and map write" in 5 of 5 plain runs and reported 16 races
under -race. It passes with this change.
When a candidate matches the saved cluster authority, its existing certificate pin also vetoes consolidation even if discovery has no per-member pin. The instance pin is not applied to other member endpoints. Cover both named and literal authorities.
Contract-Neutral: Preserve the existing TLS identity conflict boundary during consolidation; no schema, API or certificate-verification setting changes.
Change-source: pulse-maintainer
A fresh install has no alerts.json, and LoadAlertConfig answered that
with its own AlertConfig literal. The literal never set DockerDefaults, a
value struct, so container CPU, memory and disk loaded as trigger 0.
UpdateConfig keeps trigger 0 as a deliberate Off, so container metric
alerts never fired, the thresholds page showed the container Global
Defaults as Custom with every metric Off, and the startup identity
migration wrote those zeros to alerts.json a second after first boot.
The literal had also drifted from the manager's factory config on
flapping detection, alert TTL cleanup and the quiet-hours and
escalation templates.
The missing-file path now returns alerts.DefaultAlertConfig, the factory
constructor (now exported), so persistence keeps no second copy. It
leaves the activation state unset, as the literal did: node updates and
imports reapply LoadAlertConfig, and an explicit pending_review there
would send an active install back to pending review. Stored configs are
untouched, because trigger 0 is how a saved Off looks.
Two factory values would have reached installs for the first time
through this path, so they change with it. AutoAcknowledgeAfterHours
drops from 24 to 0: no settings control shows it, no install ever ran
it, and it would acknowledge still-firing alerts on the user's behalf.
The quiet-hours timezone drops from America/New_York to empty, which
keeps the settings page offering the viewer's browser zone.
Setting a global default to a positive trigger of 5 or less (Machines CPU
at 1%) stored agentDefaults.cpu as {trigger: 1, clear: 75}. The thresholds
page sends clear max(0, trigger - 5), which is 0 there. NormalizeAgentDefaults
then derived trigger - 5, found it non-positive and fell back to the
factory clear, and ValidateHysteresisThresholds never checked agent
CPU/memory/disk. PBS, node temperature and the persistence normalizer had
the same fallback (validation repaired PBS and node temperature on save).
Per-type disk fill and disk temperature entries were worse: a clear of 0
reset the whole pair, so a SATA disk temperature trigger of 4 was stored
as 55.
Alert firing and resolving did not change for these pairs:
buildCanonicalMetricSpec drops a clear at or above the trigger and the
reducer then clears at the trigger. The wrong level showed in
/api/alerts/config and alerts.json, and firing agent alerts reported it as
clearThreshold (75 on a 1% CPU alert, 85 on a disk alert still firing at
81.8%).
PBS, node temperature, agent and per-type disk defaults now use
normalizeThresholdPointer, which Kubernetes, TrueNAS and vSphere already
use: a missing clear sits five points under the trigger floored at 0, and
a clear at or above the trigger is repaired, so an API-written {50, 80}
now gets a real recovery band at 45. A non-positive per-type trigger still
resets to the type default. Persistence calls the same normalizer through
a new NormalizeHysteresisThreshold instead of its own copies, so configs
saved before this load repaired.
Keep admission sealed and reuse a still-running close observer. A later explicit shutdown attempt may persist retained state after a completed write failure; successful close remains idempotent. This avoids a permanent offboarding dead end after an ordinary filesystem repair without an automatic spin retry or overlapping writes.
Contract-Neutral: Internal JSON writer recovery behaviour only; public routes, fields, permissions, identity format, private file mode and atomic persistence are unchanged.
Change-source: pulse-maintainer
Admit detached metadata in order, merge identity fields without restoring stale
operator edits, and persist complete snapshots with one owned writer. Seal late
writes and return shutdown failures before organisation data or offboarding is
removed. Preserve existing drain budgets, file format, tenant boundaries and
atomic persistence. Add controlled burst, ordering, write-error, blocked-close
and real deletion controls.
Contract-Neutral: Behavioural repair of existing JSON persistence and organisation deletion; no route, field, permission, identity format, schema or provider authority change. Shutdown failure uses the existing HTTP error envelope. Internal ownership methods and explanatory contract text change, not the public contract shape.
Change-source: pulse-maintainer
Saved TrueNAS and VMware connections belong to validated tenant storage keys, not optional organisation metadata. A malformed org.json could prevent provider creation and block authoritative disable or removal in an unrelated tenant. Enumerate storage identities while retaining strict metadata/account readers, tenant isolation and config/credential failure handling.
Synthetic probe, polling and API controls reproduce the interval-only response family in #2382. They do not establish either reporter installation’s cause or native appliance acceptance.
Change-source: pulse-maintainer
Merge exact upstream 99927467f5. Resolve only the agent-lifecycle contract append conflict by retaining both monitor recovery ownership and AI usage export clauses. Frontend auto-merges preserve reviewed final bytes; no runtime conflict correction or browser receipt rebinding.
Change-source: pulse-maintainer
The helper could repersist legacy entitlement secrets and clear billing integrity while granting demo fixtures. Require the existing demo capability before mutation, leave all billing/key material untouched, and keep verification and encrypted legacy migration with the canonical application. Preserve full-state recovery, service ownership and the existing native route; seed only non-secret admitted fresh-runner fixtures. Add real canonical encrypted/legacy/HMAC connected controls and malformed or unprovisioned refusal-before-mutation coverage.
Change-source: pulse-maintainer
Use a well-formed local-login fixture for system-settings normalisation, make the existing outbound-IP coverage deterministic through its existing dial seam, and finish the copy audit with fresh inspected browser evidence.
Contract-Neutral: No runtime or archive contract changes. Existing tests now run offline without a real route, and the migration warning uses the required copy style.
Change-source: pulse-maintainer
Separate configuration transfer from complete stopped-state recovery, correct omitted platform and agent-state claims, and preserve safe credential/retargeting guidance. Validate the real encrypted archive and production dialogs with scoped tests and desktop/phone browser interactions. No new backup capability or runtime contract change.
Contract-Neutral: The archive, persistence, authentication and agent contracts are unchanged; help and visible warnings now describe existing scope, with regression proof for that account.
Change-source: pulse-maintainer
The Anthropic client logged cache_creation_input_tokens and
cache_read_input_tokens and then returned only input_tokens and
output_tokens, so every cached Patrol run reached the usage ledger, the
cost preview, the budget check and the weekly digest with its cache
buckets missing. Pulse #2350 reproduced the loss on both the Chat and
ChatStream paths, and #2118's reporter was told to trust the provider's
billing view over Pulse's totals until this was fixed.
Both response paths now carry the two buckets as fields disjoint from
InputTokens (streaming assigns them from message_start and the final
cumulative message_delta rather than summing). The agentic loops, Patrol
responses and run records sum them beside the ordinary totals, the usage
ledger and its persisted copies keep them, and cost.EstimateUsageUSD
prices each bucket at Anthropic's published cache rates (5-minute write
1.25x, read 0.1x of the input rate), falling back to the input rate for
providers without a known cache price. Spend summaries, the digest spend,
the cost projection medians and the usage export expose the buckets and
price them the same way. InputTokens keeps its meaning everywhere, so the
telemetry token buckets are unchanged.
Route Docker and FAQ readers through one credential-source-aware recovery guide. Replace only the active password, retain unrelated settings and agent tokens, and account for systemd drop-ins and container environment application. Check private prompt output, failure stops, bcrypt loading, precedence and rendered help links.
Contract-Neutral: Documentation and test coverage only; existing authentication, setup and deployment contracts are unchanged.
Change-source: pulse-maintainer
Record the #2140 node TLS-preference fix in the contracts its runtime commits touch: agent-lifecycle, api-contracts, security-privacy and the dependent storage-recovery contract, with the node API payload and persisted-field proofs in internal/api/contract_test.go and internal/config/config_load_test.go. The canonical completion history registers these contract and proof files as the completion of the auto-register and consolidation runtime commits.
Change-source: pulse-maintainer
ConsolidatePVEInstances promoted VerifySSL false->true whenever a merged duplicate or standalone had it enabled, silently re-enabling a disabled Verify SSL certificate setting on every save, load and monitor reconciliation (#2140). The add and update handlers now record an explicit operator choice in VerifySSLExplicit; a merge never overrides it, while the historical promotion is kept when neither side recorded a choice so an existing secure connection is not downgraded. A merged fingerprint still pins the peer.
Regression tests cover the standalone and duplicate-cluster merges and the reported PUT-save path through normalizePVEConfigState.
Change-source: pulse-maintainer
ConsolidatePVEInstances promoted VerifySSL false->true when folding a duplicate cluster or overlapping standalone into the canonical instance, silently re-enabling a disabled Verify SSL certificate setting on every save, load and monitor reconciliation (#2140). The canonical instance now owns the preference; a merged fingerprint still pins the peer, so verification is not downgraded.
Regression tests cover the standalone and duplicate-cluster merges and the reported PUT-save path through normalizePVEConfigState.
Change-source: pulse-maintainer
Quick security setup previously replaced system.json with defaults even for authenticated force reconfiguration. Initialize only absent settings under the persistence save mutex, preserving existing bytes and read failures. Cover credential rotation, custom and malformed settings, missing settings, and read errors without changing authorization or token scopes.
Change-source: pulse-maintainer
Persistence-only coverage does not establish that stored guest keys resolve during backup evaluation. Connect save/reopen to fresh managers for node-qualified and stable guest keys, checking inherited, explicit and disabled settings before and after global edits. This narrows the untested boundary behind discussion #1126 without guessing override provenance or changing runtime semantics.
Change-source: pulse-maintainer
Exercise disk save/load with fresh persistence objects for global-only, toggle-only and explicit backup thresholds under two guest keys. Repeat after global edits to guard sparse inheritance and intentional override preservation for the unresolved #1126 report.
Change-source: pulse-maintainer
Verify failed node-link persistence rolls back both the displaced owner and replacement, preserves report watermarks and reservations, and permits a durable retry on the same store. A rollback-omission negative control reproduces the assertion failure; no runtime behaviour changes.
Change-source: pulse-maintainer
Commit link and unlink journal updates before publishing state, preserve manual selections across report and provider refresh boundaries, and reserve dormant owners across restart. Re-evaluate known automatic associations using provider names while retaining legacy unknown links.
Change-source: pulse-maintainer
New-finding telemetry lost older activity once the run history reached its
100-entry cap. Persist a bounded daily finding tally with a separate
upgrade cursor, preserving run counts while backfilling retained findings.
Cover restart, repeated saves, upgrade, read failure and UTC-day retention.
Record the measurement boundary and retire the resolved coverage gap.
The customers most likely to lapse are the ones who stopped opening Pulse,
so the in-app "This week" card cannot reach them. Report schedules gain a
kind: the default resources kind is the existing PDF or CSV performance
report, and patrol_digest emails the same weekly "what Patrol did for you"
rollup the Patrol page shows, in plain language, for the whole workspace.
It reuses the existing scheduler, cadence, recipients, tenant email config,
and Pro advanced-reporting entitlement rather than adding a second
scheduler. Digest schedules are weekly and email-only; the server fixes
scope, format, and attachments, and a run with no AI service, no Patrol, or
no email destination records a last_error that names the missing piece
instead of sending an empty week. Nothing is written to disk.
The digest assembly moves from the HTTP handler into
AISettingsHandler.BuildPatrolDigest so the schedule runner and the
endpoint share one code path. docs/MSP.md documents the kind for
providers; the Settings > Reporting "Report type" selector follows in a
separate change once it has a Pro-licensed browser pass.
Forty-two percent of paying installs never get Patrol running, and the
support and issue evidence says the wall is model choice and cost: a
prospect asked which models Pulse recommends before trying, a Pro customer
picked Flash-Lite to save money and Patrol then could not file verdicts,
and a mispriced Opus row tripped the 20 USD budget so the circuit breaker
disabled Patrol with nothing but a log line (#1789).
Answer both questions at the point of choice. The Patrol and shared
default model pickers pin guided models in a "Suggested for Patrol"
section with recommended / suggested / caution markers and a one-line
reason: the Ollama preflight blessing is the only recommended entry,
Gemini Flash-Lite the only caution, and cloud starting points are labelled
price-driven and unqualified until this install's own readiness pass
upgrades them to "Verified on this install". A server-computed cost
preview sits under the model choice: monthly estimate from Pulse's price
table, the schedule, and the install's own median full run once three
priced runs exist (a measured 104,528-in / 4,491-out run otherwise), with
the assumption stated and a token explained once, plus 30-day spend
against budget. Each schedule preset is priced in the Schedule select.
When a per-token model is picked while the schedule is still the 6-hour
default, the cost model proposes the slowest preset that keeps scheduled
runs under half the budget (20 USD reference when none is set) and says
what the slower check costs in detection delay; a schedule the install
already chose is never changed.
A used-up budget is now a spending decision, not a provider fault: the
budget refusal is a typed sentinel with the figures, classified as the
budget_exhausted cause, excluded from circuit-breaker accounting, and
promoted into the Patrol block state, so the Patrol page shows "Patrol
paused" with the spend and limit and a "Raise the cost budget" action
instead of "Check Patrol model".
Two standalone Proxmox sites that reuse one short node name and one
pasted install token still folded into a single host record: the
token+hostname binding and the continuity store's hostname+token
fallback both resolve identity without consulting the machine ID, so
the second site's reports adopted the first site's host ID and the
record flip-flopped between machines. Removing that collapsed record
(the agent uninstaller does this on reinstall) then revoked the shared
token because the still-used guard could only see one record, leaving
every surviving agent rejected with 401 "Unauthorized access attempt"
on /api/agents/agent/report - the new symptom reported on #1753 after
the v6.4.2 retest.
Qualify host token binding slots with the reporting machine's identity
so two machines behind one token+hostname hold separate bindings,
refuse to adopt a bound or continuity identity whose recorded machine
ID disagrees with the report's, and rebuild bindings machine-qualified
after token reloads. Reports without a machine ID keep the legacy
binding shape and same-machine reinstalls keep their stable IDs.
Regression coverage: a state-layer test proving two shared-token pve01
machines keep distinct stable identities, continuity-match tests for
the machine guard, and an end-to-end router test walking the reporter's
estate - both install patterns, alternating reports, uninstall and
reinstall - asserting report auth never breaks and no token is revoked
while its sibling site still uses it.