Issue #1895 reports parity alerts when mdNumDisks=0 on a pool-only Unraid system. Array service state alone does not establish that a parity array exists.
Preserve the optional disk count from collection through canonical runtime conversion and suppress only the no-parity warning for an explicit zero. Missing or malformed counts retain legacy behaviour, and disk failure reasons remain active.
Validated focused Unraid tests in hostagent, storagehealth, monitoring, unifiedresources and alerts, including JSON zero preservation and canonical round trip. The new pool-only regression fails against the previous warning condition. Both agent and server need this change; no release or reporter retest is claimed.
Change-source: pulse-maintainer
Issue #1890 reports macOS agent updates stopping because the root group does not exist. Use numeric superuser ownership in the two shared lifecycle writes without relaxing failure handling or the least-privilege group boundary. Add a regression fixture that rejects named root ownership and checks that chown failures still prevent replacement.
Change-source: pulse-maintainer
Near-synchronous host and Docker reports consumed a shared CPU baseline, measuring collection bursts rather than each module's reporting interval. Retain a collector per host collector and a separate Docker module collector while preserving the package-level convenience API and disk filters.
Add an interleaved-counter regression covering both collection entry points. It fails when routed through the shared baseline and passes with isolated state. All hostmetrics, hostagent and dockeragent tests pass, as do the focused CPU regression tests under the race detector.
Change-source: pulse-maintainer
Credential slots froze the configured-node snapshot when the editor mounted, so a later server refresh could leave an untouched form stale even after the poll-clobber remount was removed. Keep node and security inputs reactive while the existing dirty guard protects operator edits, and cover both component refresh and browser polling paths.
Contract-Neutral: frontend polling-state bugfix; no API or persisted-data change
Change-source: pulse-maintainer
The retry hardening added in #1885 bounded attempts but not time. npm's own
fetch-timeout defaults to five minutes and it retries internally, so three
"attempts" against a hanging advisory endpoint ran for 10m56s on job
100986651307, and a second audit step added 3m36s. The Frontend job was
cancelled 31s into type-check with all 1183 test files already passing, and a
cancelled job reports as a failed required check, so a green run blocked every
pull request. #1888 raised the job timeout to 40 minutes to unblock delivery;
this decides the policy instead.
Each attempt now runs under a hard wall-clock bound and the sequence stops at
a total deadline (60s and 240s by default). npm's internal retry loop is
disabled in favour of this one, since it was the hidden multiplier. The bound
is enforced by a watchdog subshell rather than timeout(1), which is not
present on every developer machine.
What happens when the endpoint stays unreachable is unchanged, because that
split was already right: the run fails when the change touches the dependency
graph and the answer is genuinely unknown, and warns without failing when it
does not, because the graph is then identical to a base commit that already
produced a passing answer. Any advisory at any severity still fails.
Also drops the production-only audit from the per-pull-request path. It audits
a subset of the same packages, so it reports a subset of the same advisories,
and because the complete audit fails the job on any finding, the production
step could only ever execute in the cases where it was already guaranteed
clean. The dev-versus-production split still runs for every npm workspace in
the scheduled security-scan job, where it informs rather than blocks delivery,
and Dependabot security updates remain the route for advisories published
against unchanged dependencies.
With the audit bounded to 4 minutes against an ~11 minute baseline, the job
timeout returns to 30: a stalled endpoint should surface as a warning, not be
absorbed by a budget large enough to hide it.
The status audit requires coverage_gaps to be sorted by recorded_at then id,
and main currently fails it: patrol-investigation-rate-metric-invalid (#1883)
was appended after telemetry-test-binary-production-pings (#1878), which share
a recorded_at of 2026-09-03 and are the wrong way round on id.
The pre-commit hook runs that audit, so every commit from a local checkout is
blocked until this is corrected, whatever it touches.
This swaps the two adjacent entries and nothing else; no gap content changes.
Replace the remaining Node 20 action pins before GitHub removes that runtime, and make the reviewed Node 24 pins a workflow trust invariant.
Change-source: pulse-maintainer
Retire the unused self-hosted live qualification workflow and reject future secret- or write-capable jobs on persistent or dynamically selected runners. Keep live Patrol qualification as a disposable lab operation.
Change-source: pulse-maintainer
The gap recorded that the receiver could not filter these installs on
version_is_development, which was true when it was written and is no
longer. The emitter now classifies every 0.0.0 sentinel build as
development, so pings sent after 2026-09-04 carry the flag.
Stored rows keep the values they were sent with and are not backfilled,
so the note now states the boundary rather than the old blanket warning,
and still points reads whose window reaches earlier at
version_is_published_release. Merging main also interleaved this gap with
a peer's, so the list is re-sorted by recorded_at then id.
Canonical registry rebuilds were assigning fresh timestamps to unchanged PBS datastore values, so unrelated poll completions wrote duplicate history rows. Preserve the metric source observation time so repeated rebuilds collapse onto the one upstream sample.
Change-source: pulse-maintainer
On 2026-09-03 registry.npmjs.org returned 503s and then timeouts from its
bulk advisory endpoint for over an hour. `npm audit` exits non-zero both
for a real advisory and for an endpoint it cannot reach, so the Frontend
job failed four times running and, because it is a required check, no
pull request could land at all - including Go-only ones that touch no
JavaScript. Every one of those failures was the outage. In two of the
runs the other audit call in the same job succeeded and reported zero
vulnerabilities.
The audits now run through scripts/npm-audit-retry.sh, which separates
the two cases and does nothing else. A conclusive result is acted on
immediately, and the gate stays exactly as strict as before: any
vulnerability at any severity still fails, and no severity threshold is
introduced. Only an unreachable endpoint is retried, with backoff.
When retries are exhausted the outcome depends on whether the answer is
actually unknown. A change that touches frontend-modern/package.json,
frontend-modern/package-lock.json, or the runner itself fails, because
the dependency graph moved and no result means no answer. A change that
touches none of them warns and continues, because the graph is then
identical to the base commit that already produced a passing answer.
Advisories published later against unchanged dependencies are what
Dependabot security updates are for, not a per-pull-request audit.
Deliberately not done: relaxing the severity threshold. That was my first
instinct, but the evidence does not support it. None of the four failures
was an advisory, the lockfile reports zero vulnerabilities at every
severity, and a threshold would have weakened the gate without fixing
anything. The contract's rule that audit suppression is not a valid
closure stands.
The pinning test now requires the runner's invocation and the dependency
detection wiring, and additionally asserts the runner carries no
--audit-level flag, so the strictness cannot be quietly traded away
later.
normalizeVersionString assigns 0.0.0-<sanitized> to any build string it
cannot parse as a release version — a branch name, an ad-hoc test or
qualification label, an empty VERSION file — but usageDataVersionChannel
only called a build "dev" when it carried git build metadata or its
prerelease was exactly "dev" or "dev.*". Everything else fell through to
"prerelease" with version_is_development clear, so the flag did not mean
what its name says: on 2026-09-03 the receiver held 317 installs of
0.0.0-test-version and 18 of 0.0.0-dev-pro, all flagged as ordinary
prereleases, while only 0.0.0-dev was marked development. A receiver-side
read that filtered on that flag would have excluded none of them.
Pulse never publishes a 0.0.0 release, so the sentinel itself is the
signal and it now decides the channel ahead of the prerelease-text rules.
The existing test named "source branch falls back to prerelease dev
identity" asserted wantDevelopment false, encoding the defect; it is
corrected and joined by the test-version, dev-pro, qual, and empty-string
cases that reached the receiver.
A development build is also no longer reported as a published release at
the same time. The sentinel 0.0.0-rc.1 satisfies
IsPublishedReleaseAssetVersion on prerelease spelling alone, which would
have set both flags for a build that no install can be running. The guard
lives in DescribeUsageDataVersion rather than in
IsPublishedReleaseAssetVersion, which agent update logic in
internal/api/unified_agent.go also relies on and which this change should
not disturb.
Historical rows keep the values they were sent with; only builds
reporting after this lands carry the corrected identity.
A three-week decline in investigations/new_findings (7.2% to 5.0%) looked
like a Patrol regression. It is not one. The ratio is not a rate at all:
the two counters come from different stores, cover different spans, and
are drawn from populations that barely overlap.
new_findings_30d sums run.NewFindings over history.Runs, which
SavePatrolRunHistory caps at MaxPatrolRunHistory, so it covers at most the
last hundred runs rather than thirty days. investigations_30d instead
scans the current findings store and counts surviving finding records
investigated in-window, including findings created before it, which is how
the paid cohort read 128.57% in the week to 2026-08-25. Finding
ShouldInvestigate returns false at monitor autonomy and effective autonomy
is licence-gated, so free installs produced 4384 findings and 1
investigation while 67 paid installs produced 242.
The decline was composition: flat in version-stable installs, and fleet
investigations rose once the single install that swung the total by 38 was
excluded. Finding-detection code is identical between v6.3.2 and v6.4.1.
The new test pins the asymmetry behind the bad denominator. Its twin
already asserts that runs_30d ignores the history cap after 63c40ebe5e;
nothing asserted that the findings loop immediately below it does not, so
the truncation could regress or be mistaken for a thirty-day total
unnoticed. Fixing it needs a per-day findings tally alongside DailyRuns,
which the coverage gap tracks as its own slice.
Go 1.26.8 supersedes the prior patch release, so every release builder and local toolchain guard must move together to prevent candidate artifacts from retaining an older compiler and runtime.
Contract-Neutral: toolchain-only patch update; no product or runtime contract changed
Change-source: pulse-maintainer
Retry only explicit registry failures with bounded one-minute attempts while preserving immediate advisory failures. Defer the aggregate audit verdict so frontend tests and builds still report during npm service incidents.
The browser compatibility coalescer could undo the server's provider-scoped split after websocket reconciliation. Honour machine and provider identity before merging same-hostname rows.
Keep convergence recoverable after short-lived Actions artifacts expire by preserving the exact digest-bound package already verified from GHCR.
Change-source: pulse-maintainer
The stable-row focus fallback ran after the shared dialog cleanup and could scroll a lower Manage trigger into view. Preserve the existing focus return without changing the operator's viewport, and cover the real desktop and narrow browser path.
Contract-Neutral: frontend focus-restoration bugfix; no API or persisted-data change.
Change-source: pulse-maintainer
A committed release with an unchanged operator-owned containment failure cannot converge through unattended retries. Classify that evidence without weakening the block, and rearm only when the relevant private inputs or public controls change.
Change-source: pulse-maintainer
pkg/server tests boot the real server through Run() with the version
literal "test-version", which internal/updates normalizes to
0.0.0-test-version. Each test runs against its own t.TempDir(), so every
run minted a fresh install ID. The startup ping waits two minutes and so
never fired inside a short test, but the service-health failure reporter
added on 2026-08-29 sends synchronously from a deferred handler as soon
as Run() returns an error, so every CI shard containing pkg/server posted
one ping.
The licence server recorded 317 single-ping installs between 2026-08-29
and 2026-09-03 - 311 from linux/amd64 CI runners, 3 from a maintainer
workstation - still arriving at roughly 60 a day. The canonical clean
denominator excludes single-ping installs and was unaffected, but raw
install counts and the operator-evidence blocked-cause read counted them
as real installations.
A test binary is not an installation, which is the same reason mock mode
already suppresses pings, so the guard belongs beside it in the telemetry
package rather than at the four call sites: send() now refuses the
production endpoint whenever testing.Testing() reports true. The check
compares against productionPingEndpoint, so telemetry's own tests keep
asserting on real ping content through a redirected endpoint, and the
server tests additionally opt out at the config layer to say so locally.
72 installs delivered no notification at all in the week to 2026-09-03 and
between them burned 196,562 attempts for 62,618 dead letters. Their
categorised failures are 79% authentication (11,693), configuration
(9,554) and rejected (5,954) against 930 connectivity: deterministic
verdicts about the request, not conditions that clear. The queue retried
each of them the full ladder anyway, because the retry decision never
consulted the failure class.
Those three classes now dead-letter on the attempt that produced them.
The same payload, sent again to the same destination with the same
credentials, gets the same answer; spending two more attempts on it only
delays the dead letter the operator needs to act on. Connectivity, rate
limiting, server errors and unclassified failures keep the full ladder.
TLS deliberately stays on the retrying side. A handshake can fail
transiently during a rotation, and one wasted ladder is cheaper than
dropping a recoverable notification.
This is the same call webhook delivery already made for HTTP 4xx in
isRetryableWebhookError, now generalised to every destination type and
owned in one place. It was not safely expressible before the class became
sender-declared rather than guessed from error prose.
Nothing is lost by giving up sooner: RetryTerminalFailures still returns
retained terminal failures to the queue with a fresh budget once the
operator fixes the credentials or the configuration, and dead-letter rows
now record the failure class and whether the cause was an exhausted
ladder or a non-retryable class.
Note for telemetry reads: notification_attempts_7d will fall sharply in
the blackout cohort while notification_failures_7d is unchanged, because
the same terminal failures now cost one attempt instead of three. That is
the intended effect and not a drop in notification volume.
Terminal notification failures are 36% of resolved delivery outcomes
fleet-wide (126,337 dead-lettered against 224,692 delivered in the week
to 2026-09-03, over 6,668 clean installs), and the category breakdown
could not say why: unknown was the modal bucket at 31,218.
The class was being derived by substring-matching the Go error message.
That fails in two ways. Any failure whose text carries none of the ~50
recognised tokens falls through to unknown, which is most of what SMTP
produces: net/smtp reports the server's verdict as a reply code, and only
535 was ever matched, so a 550 relay refusal and a 451 temporary failure
both recorded as unknown. Worse, the text being matched includes the
destination's own response body, so a third party can choose the reason
code Pulse records and shows the operator - a 500 whose body contains
"rate limit" was recorded as rate_limited rather than server_error.
Senders now declare the class where they already know it, and the
classifier reads Go's own error types before it reads any prose:
*textproto.Error for SMTP reply codes, x509 and tls for certificate
failures, net.DNSError and timeouts for connectivity. HTTP status codes
set the class at the five sites that build a status error, so the
response body is preserved for the operator's audit row but can no longer
influence the classification. Prose matching remains only as the last
resort for paths that declare nothing.
SMTP 5xx is deliberately not mapped the way HTTP 5xx is: a 550 is the
destination refusing the message, not the destination breaking, so only
the transient 4xx replies count as server_error.
Registers the wider finding as a coverage gap. The 36% is concentration,
not breadth - 72 installs that delivered nothing at all in seven days
account for half of all terminal failures, and Pulse neither backs off
nor tells those operators the destination has never once succeeded.
A cluster name is an operator-selected display label, not global machine identity. Two independently configured estates using the same label could share one linked agent and then collapse into one presentation row after delayed cluster detection. Require node identity, exact endpoint, or host corroboration across provider instances instead.
Refs #1753
Change-source: pulse-maintainer
Shared subtabs used scrollIntoView for horizontal visibility, allowing remounted or programmatically changed drawers to scroll the whole application shell. Reuse the horizontal rail controller instead, and restore disclosure focus without scrolling when a detail row closes.
Record the shared primitive contract and its registry-approved guardrails, with desktop and narrow Backups browser coverage for visible Overview and Manage states, off-screen tab changes, and non-scrolling focus return.
Change-source: pulse-maintainer
CORE 12 reports versions such as TrueNAS-12.0-U5, which the legacy transport gate did not recognise. Admit that bounded version family and retain redirect negotiation coverage for both supported CORE generations.
Change-source: pulse-maintainer
The setup wizard's telemetry card only ever offered a way out: it led
with "enabled by default", gave no reason the data exists, and told the
reader to set PULSE_TELEMETRY=false before starting a process that had
already sent its first ping two minutes after boot. The payload-update
banner paired "we now collect more" with a one-click Disable button, was
keyed to schema v2 from July and never re-triggered across fifteen later
bumps, and its text was rewritten in August so anyone who had dismissed
it never saw the new wording. Nothing on either surface said what the
data is for or what it is never used for. No GitHub issue or discussion
has ever complained about the default-on posture, so the defensive
framing was answering a question nobody asked while quietly nudging
people to opt out.
Setup now leads with what the daily summary is for (development effort
follows real use; the features and platforms the operator relies on get
priority), names concrete exclusions (hostnames, credentials, IP
addresses), states what it is never used for (not sold or shared, not
used for advertising, not linked to a Pulse account or license), and
puts a real Usage statistics toggle on the admin-account step. The
toggle defaults to on and, when switched off, is applied through the
canonical system-settings endpoint once the admin token exists, so there
is no setup-only side channel and the account is created either way.
Neither setup screen tells the reader how to turn it off; the switch is
the control. The env-var instruction moves to PRIVACY.md where a reader
can still act on it, alongside a note that the first ping fires about
two minutes after start.
The payload-update banner is retired along with its telemetryAction deep
link that changed the preference on arrival. Payload changes are now
disclosed in a dated changelog in PRIVACY.md (back-filled from schema v2
to v17 from the telemetry package's own version notes) and in release
notes; an in-app notice is reserved for a change in kind. PRIVACY.md
gains a "What it is not used for" section whose statements are facts
about the license-server path, which never joins telemetry rows to
license or customer records; the contract treats any change to that path
as a change in kind. Settings leads with what the data is for and makes
Preview payload the primary action, because the exact runtime payload is
the disclosure an operator can verify. The security-privacy,
deployment-installability, and frontend-primitives contracts record the
new rules. Telemetry and i18n proof tests pin the setup choice, the
purpose-first and never-sold wording in every locale, and the changelog
row for the current schema so a future bump cannot land undisclosed.
Demand ledger: repos/pulse-pro FEATURE_REQUESTS.md "Telemetry consent as
a real setup choice" (named bet, pulse-pro PR #40). Supersedes the
three-commit branch behind Pulse PR #1873, rebuilt on current main.
Retain the exact delivery-trust candidate commit and integrate its current-control retry and scheduled reconciliation behavior on canonical main.
Change-source: pulse-maintainer