Commit Graph

7724 Commits

Author SHA1 Message Date
pulse-triage[bot] a91d1ed89d Keep incomplete backup entries out of completed restore totals
Use the same completion predicate for latest backups, restore-point and byte totals, PBS coverage and per-day count/volume charts. Keep running and failed entries visible, with separate counts and honest inventory-entry labels. Completion is not verification, restore or guest thaw.

Add registered model and consuming-table regressions for mixed and incomplete-only inventories, unknown chronology and completion transitions. Guest operations and server policy are unchanged.

Change-source: pulse-maintainer
Contract-Neutral: Apply established completion semantics and inventory-entry wording only; resource transport, identity, persistence and shared table contracts are unchanged.
2026-10-24 18:49:17 +01:00
pulse-triage[bot] 105fb05cc3 Decode native retained metric numbers without per-point reflection
Keep database/sql conversion for atypical values and rejection of NULL, malformed and overflowing observations. Compare native and fallback scans against primitive destinations and protect the allocation-free native path.

Contract-Neutral: Retained observations, numeric acceptance and rejection, SQL, sampling, source choice, extrema and access boundaries are unchanged; only application-side numeric assignment avoids per-observation reflection.
Change-source: pulse-maintainer
2026-10-24 17:32:56 +01:00
pulse-triage[bot] 994e9388b5 Keep deleted webhook work bound to its original destination (#3210)
**Keep deleted webhook work bound to its original destination**

Require an enabled saved ID before queued firing or recovery delivery. A
sibling at the same URL cannot release a deleted destination’s retained
payload and credentials. Keep the URL fallback for legacy ID-less work,
preserve admitted snapshots and cancellation history, and prove
restart/operator-retry isolation with real receivers.

**Acknowledge persisted webhook deletion to the destination editor**

Return the explicit success boolean the existing editor requires after
durable and live removal, while retaining the legacy status field. Pin
the response contract so a successful deletion is not displayed as
failed and does not encourage blind retries.

---
Fleet candidate `20261024T150012Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `94a50e75b017901ac31e15e27881aac46abce32c` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 16:18:28 +00:00
pulse-triage[bot] 94a50e75b0 Acknowledge persisted webhook deletion to the destination editor
Return the explicit success boolean the existing editor requires after durable and live removal, while retaining the legacy status field. Pin the response contract so a successful deletion is not displayed as failed and does not encourage blind retries.

Change-source: pulse-maintainer
2026-10-24 16:21:39 +01:00
pulse-triage[bot] fbdef9ac69 Record counted-navigation semantics at the paid shell boundary
Document the actual shared AppLayout accessibility delta and its maintained tests, without reinstating the superseded portal graph contract. Keep visible label and badge numbers together in desktop/mobile accessible names while retaining severity descriptions, routes, counts and permissions. Runtime bytes and the existing own-parent browser receipt are unchanged.

Change-source: pulse-maintainer
2026-10-24 16:14:01 +01:00
pulse-triage[bot] 5e6ae65f52 Complete counted-navigation repair without replacing the landed portal fix
Compose retained 4ac1d7ba15 and its exact imported history on current main. Retain the proved WCAG label/count correction, its complete fixture coverage and the matching integration checker graph. Do not re-parent or repeat the browser proof.

Main e570ad4132 already patches portal Vitest/mocker to 4.1.12, Vite to 7.3.7 and Rollup to 4.65.0. Resolve all portal inputs, generated manifest and cloud-paid contract to those exact landed bytes, removing the superseded graph and its test instead of downgrading or extending it. No portal delta remains to validate. Preserve the original uncertain portal execution separately; this merge neither replays nor clears it.

Change-source: pulse-maintainer
2026-10-24 16:12:59 +01:00
pulse-triage[bot] 5c6811f93d Keep deleted webhook work bound to its original destination
Require an enabled saved ID before queued firing or recovery delivery. A sibling at the same URL cannot release a deleted destination’s retained payload and credentials. Keep the URL fallback for legacy ID-less work, preserve admitted snapshots and cancellation history, and prove restart/operator-retry isolation with real receivers.

Change-source: pulse-maintainer
2026-10-24 16:08:19 +01:00
pulse-triage[bot] ea99456ffd Assemble retained metric series directly by statement-local number (#3205)
**Assemble retained metric series directly by statement-local number**

Avoid per-observation string resolution and nested-map flushes for
interleaved filtered fleets. Bind only nonempty series after each read
and bound the direct buffers, preserving the existing wide-sparse
fallback, SQL snapshots, tiers and exact observations. Verify against
single-resource readers on both layouts.

**Cap retained chart observations before allocating wire points**

Share the existing time-nearest selection by original indices, so chart
conversion materialises only selected observations. Preserve exact
timestamps, values, tie breaks, degenerate spans and oldest-source
metadata. Check the frozen former sampler across 1,440 cases and the
exported fallback contract.

Contract-Neutral: Exact chart observations, wire schema, sampling policy
and guest/storage lifecycle contracts are unchanged; only index
selection and allocation order change.

---
Fleet candidate `20261024T123325Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `bf9f0ed9822c8ee72ca25b5caecb35a2cd4a5881` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 14:51:32 +00:00
pulse-triage[bot] 5e30e90357 Compose retained portal and counted-navigation repair
Change-source: pulse-maintainer
2026-10-24 15:28:46 +01:00
rcourtman 97f55638c9 Prove demo auto sign-in in the public browser smoke (#3204)
Since v6.5.0 demo mode signs a fresh visitor in with the published
demo credentials, so the login form never stays on screen for a first
visit. The browser smoke still waited two minutes for that form and
failed, which blocked every stable demo convergence once the Tailscale
step worked again. It went unnoticed because the demo update had not
got past Tailscale since v6.5.0 shipped.

The smoke now proves the visitor's real path, a fresh tab reaching the
connected workspace, when the server reports demo mode. A second
context sets the frontend's own pulse-demo-auto-login marker to keep
proving the login form and a manual demo sign-in. Against the live
demo the old script times out and the new one passes both phases. The
policy test pins the marker to localStorage.ts so a rename fails in
source checks rather than on the live demo.
2026-10-24 15:06:41 +01:00
rcourtman 572930fded Make release Highlights name concrete changes (#3203)
Make release Highlights name concrete changes

The generator only asked for "one to three plain-text outcomes", so it
produced slogans such as "See problems, recent trends and longer
history together" that tell a user nothing about what changed. These
lines are what the update banner shows before someone installs.

The generator prompt and the release-notes template now ask for the
release's biggest specific changes and where users find them, with an
example and named anti-examples, and the release-assets test pins that
wording. Their description of the post-update modal also follows the
new plain changelog, which no longer collapses upgrade notes or puts
Highlights above the changes.
2026-10-24 14:45:36 +01:00
rcourtman cdd3022d68 Name demo Patrol findings by their unified resource (#3202)
## Problem

Mock and demo mode synthesize Patrol findings from the legacy state
snapshot (`synthesizeDemoPatrolFindings`) and stamped each one with the
snapshot's source ID: `nebula-1-mock`, `mock-cluster-1-pve1-local`,
`pmg-edge`, `atlas-4-<hex>`, `mock-cluster-2-pve12-319`. Real findings,
Inbox subjects (since #3188), the resource-scoped findings list and
resource intelligence all use the unified resource ID, and they join by
exact ID.

Seen on main (82df77f2f) on a private mock stack and a DEMO_MODE stack:

- The Inbox item "Docker host offline on Field Office Edge 01" (subject
`agent-b9070eb05b18bb77`) said "This is an alert and Patrol has not
raised a finding about it". It offered no "Browse Patrol findings for
this resource", although the demo finding "Docker host "Field Office
Edge 01" is offline" was open.
- In DEMO_MODE (the public demo's configuration, between scheduled
Patrol passes), "Storage usage on west-a-iso-library" said the same next
to the open finding "Storage "west-a-iso-library" is 88% full".
- The host finding never folded under its `docker:nebula-1-mock` alert.
The storage finding folded only because that alert's raw reference
happens to equal the legacy storage ID.
- `GET /api/ai/intelligence?resource_id=agent-b9070eb05b18bb77` (the
resource drawer's read) returned grade A with no findings.
- The LXC memory finding was typed `container`, which
`normalizeFindingResourceTypes` reads back as `node`.

## Change

- `internal/ai/demo.go`: `demoResourceResolver` resolves the reference
an alert on each record would carry, using Patrol's alert reference
resolver. That is the same resolution `AttentionSubjectResourceID` gives
Inbox subjects, so a demo finding names the resource that an Inbox item
about the same record names.
- App containers resolve through the container views by host and
container ID. Mock container IDs are too short to bind a Docker alert
reference.
- Without a read state, or before its registry has ingested anything,
the resolver derives a registry from the snapshot, as the real producers
do. That way a first cycle cannot pin source IDs for the life of the
process.
- Once the registry holds resources it is the only authority. A record
it lacks or refuses, or one that binds another type of resource, keeps
its source ID. Guests are typed `vm` or `system-container`.
- Finding IDs and keys are unchanged, because demo run history
references them.
- Contract note added in `ai-runtime.md` under the alert-mirror section.

## Verification

- `TestPatrolService_RunDemoPatrolCycle_NamesUnifiedResources` in
`internal/ai/demo_test.go` covers:
- the unified ID and type per kind, checked against the Inbox's subject
resolution;
  - lookup by unified ID (`GetFindingsForResource`);
  - both alert folds;
  - the same IDs with no read state;
  - a registry that has not ingested yet;
  - a registry that lacks a record.
- Negative controls via `go test -overlay` each fail at the expected
assertion: main's `demo.go`, no snapshot fallback, always derive, and
the legacy guest type. The whole `internal/ai` package passes.
- Live check of an OSS build from this branch on pulse-dev, mock and
DEMO_MODE:
- Both Inbox items now say "Patrol also raised … for this resource" and
offer Browse.
  - Browse lists "Patrol found 1 issue on Field Office Edge 01".
- After a Patrol run both findings fold under their alerts (lasting
decisions in the Inbox; Activity shows "3 findings mirror an active
alert").
  - The host's intelligence now reports grade C with the finding.
  - Checked at 1440 and 390 wide, with no console errors.

## Not changed

- Mock Docker container IDs are `<host>-<12 hex>`, so mock container
alerts keep their raw `docker:<host>/<id>` subject and cannot join
container findings. Real Docker IDs are 64-hex.
- Suppression rules created against legacy-ID demo findings in earlier
mock sessions no longer match.
- The findings store keeps a finding's first resource ID for the life of
the process. If a demo cycle races the registry's first ingest and a
saved operator link folds one of its records into another resource, that
finding keeps the derived ID until restart.
2026-10-24 14:38:28 +01:00
rcourtman df79b7716a Prove demo auto sign-in in the public browser smoke
Since v6.5.0 demo mode signs a fresh visitor in with the published
demo credentials, so the login form never stays on screen for a first
visit. The browser smoke still waited two minutes for that form and
failed, which blocked every stable demo convergence once the Tailscale
step worked again. It went unnoticed because the demo update had not
got past Tailscale since v6.5.0 shipped.

The smoke now proves the visitor's real path, a fresh tab reaching the
connected workspace, when the server reports demo mode. A second
context sets the frontend's own pulse-demo-auto-login marker to keep
proving the login form and a manual demo sign-in. Against the live
demo the old script times out and the new one passes both phases. The
policy test pins the marker to localStorage.ts so a rename fails in
source checks rather than on the live demo.
2026-10-24 14:31:21 +01:00
pulse-triage[bot] bf9f0ed982 Cap retained chart observations before allocating wire points
Share the existing time-nearest selection by original indices, so chart conversion materialises only selected observations. Preserve exact timestamps, values, tie breaks, degenerate spans and oldest-source metadata. Check the frozen former sampler across 1,440 cases and the exported fallback contract.

Change-source: pulse-maintainer
Contract-Neutral: Exact chart observations, wire schema, sampling policy and guest/storage lifecycle contracts are unchanged; only index selection and allocation order change.
2026-10-24 14:08:51 +01:00
rcourtman ebfee8737f Show post-update What's New as a plain changelog (#3201)
Show post-update What's New as a plain changelog

The dialog opened with three vague Highlights cards, hid the actual
changes behind a collapsed "All changes" disclosure and tucked upgrade
notes into a second one. That is not what people expect from a
changelog, and the Highlights repeat the pre-update banner's summary.

The dialog now reads like a conventional changelog: version and release
date in the header, Known issues and Breaking changes first, optional
screenshots, then every change group as headed bullet lists and the
upgrade notes last, with nothing collapsed. Highlights stay in the
pre-update banner and only stand in for the change list when a release
has no change groups.
2026-10-24 14:07:52 +01:00
courtmanr@gmail.com 842dbda6b8 Make release Highlights name concrete changes
The generator only asked for "one to three plain-text outcomes", so it
produced slogans such as "See problems, recent trends and longer
history together" that tell a user nothing about what changed. These
lines are what the update banner shows before someone installs.

The generator prompt and the release-notes template now ask for the
release's biggest specific changes and where users find them, with an
example and named anti-examples, and the release-assets test pins that
wording. Their description of the post-update modal also follows the
new plain changelog, which no longer collapses upgrade notes or puts
Highlights above the changes.
2026-10-24 14:06:27 +01:00
rcourtman b45b5562fa Name demo Patrol findings by their unified resource
Mock and demo mode synthesize Patrol findings from the legacy state
snapshot and stamped each one with the snapshot's source ID
(nebula-1-mock, mock-cluster-1-pve1-local). Everything that joins a
finding to its resource uses the unified ID: since #3188 an Inbox item
names its subject by the unified resource its alert resolves to, and the
resource-scoped findings list and resource intelligence look findings up
by that ID. On a mock or DEMO_MODE stack the Docker host offline item
therefore said "Patrol has not raised a finding about it" beside an open
finding on that host, offered no Browse button, the host's intelligence
showed no findings, and the finding never folded under its alert. The
LXC memory finding was also typed "container", which reads back as
"node".

Resolve the reference an alert on each record carries the way Inbox
subjects resolve, app containers through the container views (mock
container IDs are too short to bind a Docker alert reference), and the
snapshot's own registry when the read state has not ingested the record
yet, so a first cycle cannot pin source IDs for the life of the process.
Guests are typed vm or system-container.
2026-10-24 13:49:11 +01:00
courtmanr@gmail.com 1eb0ae5261 Merge remote-tracking branch 'origin/main' into fix/whats-new-plain-changelog 2026-10-24 13:40:17 +01:00
pulse-triage[bot] c36672b2a7 Assemble retained metric series directly by statement-local number
Avoid per-observation string resolution and nested-map flushes for interleaved filtered fleets. Bind only nonempty series after each read and bound the direct buffers, preserving the existing wide-sparse fallback, SQL snapshots, tiers and exact observations. Verify against single-resource readers on both layouts.

Change-source: pulse-maintainer
2026-10-24 13:38:25 +01:00
courtmanr@gmail.com bbdcfa95f4 Show post-update What's New as a plain changelog
The dialog opened with three vague Highlights cards, hid the actual
changes behind a collapsed "All changes" disclosure and tucked upgrade
notes into a second one. That is not what people expect from a
changelog, and the Highlights repeat the pre-update banner's summary.

The dialog now reads like a conventional changelog: version and release
date in the header, Known issues and Breaking changes first, optional
screenshots, then every change group as headed bullet lists and the
upgrade notes last, with nothing collapsed. Highlights stay in the
pre-update banner and only stand in for the change list when a release
has no change groups.
2026-10-24 13:36:26 +01:00
rcourtman 1ffa17a10c Merge remote-tracking branch 'origin/main' into fix/demo-tailnet-ci-deploy-client 2026-10-24 13:28:42 +01:00
pulse-triage[bot] 06475a9d37 Keep bounded hosted provisioning overhead in chart SLO verdicts (#3199)
Retained hosted samples include both an over-budget failure and an
under-budget pass with one provisioning agent occupying about one
logical CPU. Attribute only that bounded Actions overhead; retain raw
CPU and absolute 90 ms latency, and keep other or unattributed CPU,
guest, steal and swap invalidating. Exercise the real CLI and workflow
shells with adverse controls; no timed workload or release acceptance is
claimed.

---
Fleet candidate `20261024T104012Z-delivery-trust` from the
`delivery-trust` lane. Reviewed tip
`fbe410e244f3d44b44de258c6f3db68dabd257da` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 12:27:42 +00:00
pulse-triage[bot] 088a46bf48 Transport filtered retained metrics by statement-local series number (#3198)
Reuse bound resource and metric identities instead of decoding a metric
string and a resource number for every raw fleet observation. Preserve
scoped tier reconciliation, nullable values, caller ownership and
display aggregation. Verify reordered bindings, both layouts/projections
and filtered chunk boundaries; keep raw reads as one consistent
statement.

---
Fleet candidate `20261024T102334Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `35af9de7f572032d73b05bba81d63a1461a58f88` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 12:26:46 +00:00
rcourtman bc39aa5908 Move demo deploys to the tag:ci-deploy Tailscale client
The demo update and recovery workflows still joined the tailnet with
the July 2026 tag:infra OAuth client. That client was revoked on 30
September, when the private repositories moved to narrow clients, and
the public repository was missed. Since then every demo convergence
step has failed with an OAuth 401, so stable releases cannot converge
and the hourly reconcile keeps retrying v6.5.0.

Both workflows now use the TS_CI_DEPLOY_OAUTH_* binding that the
browser update qualification already expects, restricted to
tag:ci-deploy. The tailnet policy already grants that tag SSH to
pulse-relay, the demo host, so no policy change is needed. One narrow
client now serves all three workflows, and the broad tag:infra
identity is gone from this repository.
2026-10-24 13:25:23 +01:00
pulse-triage[bot] 55bee44d30 Patch portal Vitest and preserve counted navigation names
Align the portal runner and mocker with the main UI's 4.1.12 security floor and retain its generated source binding. Keep the already repaired main-UI dependency graph unchanged. Use the updated integration checker without dropping WCAG assertions: desktop and mobile labels now contain their visible badge text, with mixed alert severity retained in the accessible description. Add maintained floor and naming coverage and bind the consuming desktop/phone proof to this parent.

Change-source: pulse-maintainer
2026-10-24 13:17:03 +01:00
rcourtman 82df77f2f5 Treat a deleted convergence owner as terminal (#3197)
Treat a deleted convergence owner as terminal

The hourly "Reconcile release convergence debt" job has failed every run
since 13 Oct. The v6.5.0 activation marker names convergence run
37395942598 as its owner, and that run was deleted from Actions with the
rest of the 30 Sep - 8 Oct release history. The immutable marker
outlives
Actions history, so the reconciler and release-convergence.yml both
waited
forever on an owner that can no longer be read, and the permanently red
job hid real failures such as the rc.6 paid-broker miss.

GitHub only deletes a run that has completed or is older than two weeks,
which is beyond every convergence job timeout, so a definite 404 is a
terminal owner. Both the reconciler and the successor's adoption wait
now
proceed from the validated marker in that case. Any other read failure
still waits or fails.

Also remove the workflow_run retry job. Convergence runs are dispatched
with GITHUB_TOKEN, and GitHub does not deliver workflow_run for them:
the
job fired seven times ever, each for a run an operator started by hand,
and never after 29 Aug while dozens of bot-dispatched failures were
recovered by the hourly schedule. The schedule is documented as the
retry
mechanism.

v6.5.0 is served on every surface (Docker aliases at the marker digest,
Helm index entry, paid broker, demo /api/version), but no convergence
run
has proved it. The next scheduled pass dispatches a fresh v6.5.0
convergence; its demo surface will fail until the Tailscale OAuth client
behind TS_OAUTH_CLIENT_ID/TS_OAUTH_SECRET is rotated, as it returns 401.
2026-10-24 12:45:42 +01:00
rcourtman 5e30ff791a Re-dispatch Pro promotion refused by a moved pulse-pro main (#3193)
pulse-pro gives a promotion run credentials only while that run is
still its current main. Any pulse-pro merge that lands while the run is
queued or validating, roughly 20 minutes, makes the refusal certain.
The convergence caller treated it like any other failure, so the
release stayed off the Pro broker until the hourly reconcile happened
to retry. That race caused five of the last six Pro promotion failures
(12, 17 and 24 October); on 24 October it held v6.6.0-rc.6 back from
Pro installs on the RC channel.

The refusal happens before credentials, so nothing was mutated. The
caller now re-dispatches on the new main when every failed job failed
only the source check and pulse-pro main has moved off the run's head,
up to five attempts in the existing one-hour budget. Every other
failure stays terminal, and only the terminal failure is annotated so
reconciliation still finds exactly one private run URL.
2026-10-24 12:12:46 +01:00
pulse-triage[bot] 35af9de7f5 Transport filtered retained metrics by statement-local series number
Reuse bound resource and metric identities instead of decoding a metric string and a resource number for every raw fleet observation. Preserve scoped tier reconciliation, nullable values, caller ownership and display aggregation. Verify reordered bindings, both layouts/projections and filtered chunk boundaries; keep raw reads as one consistent statement.

Change-source: pulse-maintainer
2026-10-24 11:51:02 +01:00
pulse-triage[bot] fbe410e244 Compose chart SLO repair without dropping current administrative guarantees
Resolve adjacent contract append conflicts by keeping bounded hosted-agent classification in its chart section and preserving all newer SSO, RBAC and uninstall guarantees. Runtime, workflow and synthetic chart controls merge without conflict; no latency budget or customer journey changes.

Change-source: pulse-maintainer
2026-10-24 11:42:23 +01:00
rcourtman ca3812e5ed Treat a deleted convergence owner as terminal
The hourly "Reconcile release convergence debt" job has failed every run
since 13 Oct. The v6.5.0 activation marker names convergence run
37395942598 as its owner, and that run was deleted from Actions with the
rest of the 30 Sep - 8 Oct release history. The immutable marker outlives
Actions history, so the reconciler and release-convergence.yml both waited
forever on an owner that can no longer be read, and the permanently red
job hid real failures such as the rc.6 paid-broker miss.

GitHub only deletes a run that has completed or is older than two weeks,
which is beyond every convergence job timeout, so a definite 404 is a
terminal owner. Both the reconciler and the successor's adoption wait now
proceed from the validated marker in that case. Any other read failure
still waits or fails.

Also remove the workflow_run retry job. Convergence runs are dispatched
with GITHUB_TOKEN, and GitHub does not deliver workflow_run for them: the
job fired seven times ever, each for a run an operator started by hand,
and never after 29 Aug while dozens of bot-dispatched failures were
recovered by the hourly schedule. The schedule is documented as the retry
mechanism.

v6.5.0 is served on every surface (Docker aliases at the marker digest,
Helm index entry, paid broker, demo /api/version), but no convergence run
has proved it. The next scheduled pass dispatches a fresh v6.5.0
convergence; its demo surface will fail until the Tailscale OAuth client
behind TS_OAUTH_CLIENT_ID/TS_OAUTH_SECRET is rotated, as it returns 401.
2026-10-24 11:40:23 +01:00
rcourtman db6e4a28ea Merge origin/main into fix/patrol-scoped-open-work 2026-10-24 11:22:58 +01:00
rcourtman 85621feabe Scope the Patrol Open work summary to the browsed resource
An Inbox alert's "Browse Patrol findings for this resource" narrows the
Activity findings list to that resource, but the Open work line, its work
groups, the approval banner and the filter chip counts above the list
still described every open finding. On a live stack the afs-web scope
read "Patrol found 3 issues on 2 affected resources — 2 recurring" above
its two rows, so the list looked incomplete.

In a resource scope the workspace now counts the summary and work types
over that resource's open findings, the rows its Active filter shows,
and names it ("Patrol found 2 issues on afs-web — 2 recurring"). The
approvals work group and the banner count and show only approvals for
its findings, so one about to expire keeps its countdown. FindingsPanel's
Needs Attention, Approvals and Overdue chips count only that resource's
findings. The latest check's health and coverage-stale groups describe
every resource, so a scope leaves them to the unscoped view and the
page header.

The banner's context line now ellipsizes inside the banner at phone
width instead of running past its edge, which the scoped single-approval
banner made easy to hit.

Checked on a private mock stack at 1440 and 390 px: main showed "12
issues on 10 affected resources", "2 actions awaiting your approval" and
Needs Attention (1) above two postgres-replica-01 rows; this shows "2
issues on postgres-replica-01 — 2 recurring" and that resource's one
approval. The new component tests fail on main.
2026-10-24 11:22:48 +01:00
pulse-triage[bot] 7c0fc7f79e Reject competing audit webhook destination fields (#3192)
**Reject competing audit webhook destination fields**

Parse one bounded update object without discarding repeated urls
members, including escaped and case-folded aliases. Preserve deliberate
clears, singleton aliases and unknown fields; reject ambiguous or
incomplete selections before validation and mutation.

**Bind audit guidance smoke checks to the unambiguous decoder**

The complete-selection admission moved out of the inline struct. Retain
the route/access and organisation assertions, and require decoding
before URL validation instead of the obsolete struct spelling.

Contract-Neutral: Align a static documentation check with the reviewed
decoder; runtime and published contracts are unchanged.

**Reject competing public SSO and recovery request fields**

Validate complete bounded admin objects before decoding or applying
them. Preserve singleton aliases, unknown fields, case-sensitive role
map keys, explicit access clears, saved secrets and existing tenant and
one-use token boundaries. Add public producer, persistence and API
contract controls.

**Preserve published audit smoke bindings in the SSO candidate**

Resolve the overlapping audit-doc test correction against published main
52457777c4 by retaining its stronger
bounded decoder and mutation-order assertions. No runtime or frontend
source changes; the reviewed SSO and recovery proof remains applicable.

Contract-Neutral: Integration-only static test reconciliation; API,
runtime, frontend and documented contracts are unchanged.

---
Fleet candidate `20261024T084021Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `7a50adc5ac6924829c1e871c638db9b461b40011` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 10:17:29 +00:00
rcourtman 2d498a35d2 Re-dispatch Pro promotion refused by a moved pulse-pro main
pulse-pro gives a promotion run credentials only while that run is
still its current main. Any pulse-pro merge that lands while the run is
queued or validating, roughly 20 minutes, makes the refusal certain.
The convergence caller treated it like any other failure, so the
release stayed off the Pro broker until the hourly reconcile happened
to retry. That race caused five of the last six Pro promotion failures
(12, 17 and 24 October); on 24 October it held v6.6.0-rc.6 back from
Pro installs on the RC channel.

The refusal happens before credentials, so nothing was mutated. The
caller now re-dispatches on the new main when every failed job failed
only the source check and pulse-pro main has moved off the run's head,
up to five attempts in the existing one-hour budget. Every other
failure stays terminal, and only the terminal failure is annotated so
reconciliation still finds exactly one private run URL.
2026-10-24 11:07:44 +01:00
rcourtman 18611ec79c Refuse Unraid uninstall while the rc.local go shell is live (#3191)
Uninstall on Unraid refuses while a live go shell exists, because that
shell may still hold later boot lines in memory. Stopping the watchdog
would release it to run them. Commit 5dad231852 added that refusal, but
its check looked only for `/boot/config/go` in a process named like a
shell.

Unraid's `rc.local` never runs that path. From unraid/webgui
`etc/rc.d/rc.local` ("Invoke the 'go' script"):

```
fromdos <$CONFIG/$GO >/var/tmp/$GO
chmod +x /var/tmp/$GO
/var/tmp/$GO
```

`$GO` is `go`, or `go.safemode` in safe mode. Because the copy is
executed directly, the kernel names the process after the script. On
pulse-dev (Linux 6.8) a shebang script run that way shows `comm=go` and
`args=/bin/bash /var/tmp/go`, so the old check never matched. Hosts
installed before #3162 still have that shell blocked on the old
foreground `bash <wrapper>` line. On those hosts uninstall treated it as
unrelated, stopped the watchdog, and let go run the rest of its script.

## Change
- `read_nas_agent_removal_processes`: on Unraid, a process named `go` or
`go.safemode`, or a shell, that has `/boot/config/go`, `/var/tmp/go` or
a `.safemode` form as a whole argument is an unknown boot owner. This
replaces the old substring match, so near-miss paths
(`/var/tmp/gopher.sh`, `/boot/config/go~`) no longer block removal. A Go
toolchain process named `go` still reads as `other`.
- `agent_nas_teardown.py`:
`test_live_unraid_boot_shell_is_unknown_not_a_signal_target` now also
covers the rc.local spellings (`go /bin/bash /var/tmp/go`, `go.safemode
/bin/bash /var/tmp/go.safemode`). Every model `ps` read also includes a
Go toolchain process, so every passing Unraid control shows that process
does not block removal.
- `TestInstallSHNASRemovalTreatsLiveUnraidGoShellAsUnknownOwner`: a role
table for Unraid and QNAP run through the real function.
- agent-lifecycle and deployment-installability contracts: "Unraid/QNAP
removal" sections updated.

## Verification (pulse-dev)
- `TestInstallSHNASBootOwnersQuiesceBeforeRecoveryErasure`, the new role
test and `TestInstallSHUnraidGoEntry*` pass.
- With `origin/main`'s `install.sh` restored, the new role test and the
rc.local subcases of the live-shell control fail.

## Not covered
A go file without a `#!` line still escapes. Bash runs it in a forked
copy of `rc.local`, which keeps rc.local's name and arguments. Stock
Unraid go files start with `#!/bin/bash`. The contracts state this
limit.
2026-10-24 10:52:43 +01:00
pulse-triage[bot] b25c22c00f Keep bounded hosted provisioning overhead in chart SLO verdicts
Retained hosted samples include both an over-budget failure and an under-budget pass with one provisioning agent occupying about one logical CPU. Attribute only that bounded Actions overhead; retain raw CPU and absolute 90 ms latency, and keep other or unattributed CPU, guest, steal and swap invalidating. Exercise the real CLI and workflow shells with adverse controls; no timed workload or release acceptance is claimed.

Change-source: pulse-maintainer
2026-10-24 10:48:40 +01:00
pulse-triage[bot] 52457777c4 Reject competing audit webhook destination fields (#3190)
**Reject competing audit webhook destination fields**

Parse one bounded update object without discarding repeated urls
members, including escaped and case-folded aliases. Preserve deliberate
clears, singleton aliases and unknown fields; reject ambiguous or
incomplete selections before validation and mutation.

**Bind audit help checks to the bounded selection decoder**

Repair the containing CI failure from PR 3186 without restoring
last-member-wins parsing. Keep the explicit array, request bound, URL
validation before mutation and organisation-scoped replacement checks;
runtime selection semantics remain covered by the existing handler
tests.

Contract-Neutral: Static guidance/source-binding test repair only; the
composed audit runtime and documented API, access, storage and delivery
contracts are unchanged.

---
Fleet candidate `20261024T082008Z-delivery-trust` from the
`delivery-trust` lane. Reviewed tip
`58e46e21d7c9bd265e88485d26bfed38e03fb31e` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 09:34:36 +00:00
rcourtman 27f7e11e9b Refuse Unraid uninstall while the rc.local go shell is live
Uninstall treats a live Unraid go shell as an unknown boot owner and
refuses, because that shell may still hold later boot lines in memory.
Signalling the watchdog would release it to run them. The check only
looked for /boot/config/go in a process whose name is a shell. Unraid's
rc.local never runs that path: it writes a fromdos copy to /var/tmp/go
(go.safemode in safe mode) and executes that copy directly. The kernel
then names the process "go", with argv "/bin/bash /var/tmp/go", so the
check never matched it.

Hosts installed before the detached go entry still have this shell
blocked on the old foreground wrapper line. On those hosts uninstall
classified the shell as unrelated, stopped the watchdog, and let go run
the rest of its in-memory script. That is the case the live-shell
refusal was added to prevent.

The role check now covers a go or go.safemode process, or a shell, that
has /boot/config/go, /var/tmp/go or a .safemode form as a whole
argument. The old substring match is gone, so near-miss paths such as
/var/tmp/gopher.sh no longer block removal, and a Go toolchain process
named go still reads as unrelated. The NAS teardown control covers the
rc.local spellings, and a new role-table test pins Unraid and QNAP.
Both fail against the previous install.sh.

A go file without a #! line is still not recognised. Bash runs it in a
forked copy of rc.local, and the contracts now say so.
2026-10-24 10:26:45 +01:00
pulse-triage[bot] 3d05fd3cda Reject competing public SSO and recovery request fields
Validate complete bounded admin objects before decoding or applying them. Preserve singleton aliases, unknown fields, case-sensitive role map keys, explicit access clears, saved secrets and existing tenant and one-use token boundaries. Add public producer, persistence and API contract controls.

Change-source: pulse-maintainer
2026-10-24 09:48:15 +01:00
rcourtman 1120502a1d Name Patrol inbox alert subjects by their unified resource
The Patrol inbox passed each alert's own resource reference through as the
item subject: docker:<host>/<container> for a Docker container,
<instance>:<node>:<vmid> for a guest, agent:<host>/disk:<label> for a
filesystem. Patrol findings, protection postures and resource actions carry
the unified resource ID instead, so "Browse Patrol findings for this
resource" opened an empty list, postures never attached, and the alert
detail said Patrol had raised no finding while it had one on that exact
container.

The attention projection now resolves each record's subject through the
org registry's alert reference resolver and names the unified resource. The
operational record keeps the alert's reference, and a reference inventory
cannot bind stays as before. The resolver gains Docker container references
with a complete ID, the key RecordChange already joins their history to, so
container alerts resolve like every other kind and alert-triggered Patrol
scopes for them keep the triggering alert. It also reads source targets
from the registry's view cache rather than scanning every mapping per
reference, which a page of alerts would otherwise multiply.

The alert-mirror reconcile compared the same mismatched strings and never
folded a finding under its alert; it now matches either identity. A
filesystem, sensor or ZFS pool alert resolves only to the owner its siblings
share, so the resolver marks it as a component and the reconcile does not
match on that owner. The inbox links a finding to the exact alert it
restates by the stamped alert ID, not by resource and kind, which several
alerts on one host share. The restart offer checks evidence against the
record's own reference, since the item subject is now the unified container
its registry lookup needs.

When no finding restates the alert but Patrol has open findings on the same
resource, the detail names them instead of saying Patrol raised nothing,
and the browse button appears only when there is something to browse.
2026-10-24 09:46:33 +01:00
pulse-triage[bot] 62b6b821a7 Compose reviewed audit destination protection on current main
Change-source: pulse-maintainer
2026-10-24 09:41:54 +01:00
pulse-triage[bot] 942285b7ca Merge commit '3883796a9b4f820842335b85de33c0731149c932' into maintainer-async/20261024T082008Z-delivery-trust/pulse
Change-source: pulse-maintainer
2026-10-24 09:24:37 +01:00
rcourtman bee3a94ce6 Stop Patrol resolving Docker alerts the snapshot still shows
Patrol's post-run alert review skipped the model only for metric and
offline alerts that were still firing. Every other aged alert went to
the model with a context line of CPU, memory and status. On a private
Pro stack on 24 Oct 2026 the review resolved a docker-container-health
alert for a container Docker still reported unhealthy ("running with
normal CPU/Memory, no unhealthy indicator present"): the context never
showed health. The alert engine raised the alert again on the agent's
next report about 24 seconds later, so every Patrol pass flapped it,
with a resolved and a new alert and their notifications.

The still-firing gate now keeps, without a model call, the Docker
container alerts whose condition the container's current report still
carries, mirroring the alert engine's checks: a health result other
than healthy, starting or none while running; a runtime state the
state alert covers; exited or dead with OOM-killed set. A restart-loop
alert is kept while the container is caught in Docker's restart
backoff; the snapshot has no restart window, so a running container's
loop still goes to the model. The gate finds the container by the
alert's docker:<host>/<container> ID. Matching the display name, as the
lookup did, judged an alert against whichever same-named container
came first, possibly on another host.

The review context for a Docker container now carries its health,
restart count and OOM kill (yes, no or unknown), so a recovered
container's alert reaches the model with that evidence and still
resolves on its verdict. The prompt makes a condition the shown state
does not cover a KEEP, for alert types the gate and context do not
model yet. And an alert whose subject the context lookup cannot read
but inventory still holds, or holds the owner of, by durable identity
(a ZFS pool's storage, a guest's backup, a Swarm service) is described
as present, not as "not found (may have been removed)", which invited
a RESOLVE.
2026-10-24 09:13:30 +01:00
pulse-triage[bot] 84e3d596d8 Reconcile the separated metrics and HTTP dependency graph (#3184)
Carry the non-storage managed proposal without its checksum pruning or
unrelated dependency reclassification. Update the graph admission floors
and the deployment contract; exact-graph acquisition and affected
runtime/build proof remain required before acceptance.

Source-PR: https://github.com/rcourtman/Pulse/pull/3181
Source-Commit: b33dee6d7f
Co-authored-by: dependabot[bot]
<49699333+dependabot[bot]@users.noreply.github.com>
Contract-Neutral: Dependency versions and internal graph-admission
floors only; no runtime API, wire format, permission, rendered state,
deployment interface or installer contract changes.

---
Fleet candidate `20261024T063743Z-delivery-trust` from the
`delivery-trust` lane. Reviewed tip
`ec816e849104d141bc6650c01b5d6d9303441fe5` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 07:34:40 +00:00
pulse-triage[bot] 3883796a9b Reject competing audit webhook destination fields
Parse one bounded update object without discarding repeated urls members, including escaped and case-folded aliases. Preserve deliberate clears, singleton aliases and unknown fields; reject ambiguous or incomplete selections before validation and mutation.

Change-source: pulse-maintainer
2026-10-24 08:04:34 +01:00
pulse-triage[bot] a8ee6de7e5 Keep missing vzdump guest verdicts unknown (#3183)
An ended parent job does not establish the result of a guest whose log
section has no verdict. Preserve explicit results, leave missing results
unknown and keep unqualified job logs retryable until attribution is
complete. Cover parser, poll/cache, served state and recovery mapping
with offline fixtures.

---
Fleet candidate `20261024T055150Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `93bcdbbd7c2271580aba389e0766a78ab20182ce` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 06:58:12 +00:00
pulse-triage[bot] ec816e8491 Reconcile the separated metrics and HTTP dependency graph
Carry the non-storage managed proposal without its checksum pruning or unrelated dependency reclassification. Update the graph admission floors and the deployment contract; exact-graph acquisition and affected runtime/build proof remain required before acceptance.

Source-PR: https://github.com/rcourtman/Pulse/pull/3181
Source-Commit: b33dee6d7f
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Change-source: pulse-maintainer
Contract-Neutral: Dependency versions and internal graph-admission floors only; no runtime API, wire format, permission, rendered state, deployment interface or installer contract changes.
2026-10-24 07:11:14 +01:00
pulse-triage[bot] 6b3504a1e9 Separate storage runtime dependency proposals from other Go patches (#3180)
Keep modernc driver and runtime updates together while allowing
unrelated minor and patch updates to progress independently. Preserve
all security-update coverage and individual major updates. Add the
current SQLite/libc and Prometheus grouping regression to the maintained
smoke suite.

---
Fleet candidate `20261024T051259Z-delivery-trust` from the
`delivery-trust` lane. Reviewed tip
`5b25d801bca6c22a080b2e7c14e545c941d20dac` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 05:57:06 +00:00
pulse-triage[bot] 93bcdbbd7c Keep missing vzdump guest verdicts unknown
An ended parent job does not establish the result of a guest whose log section has no verdict. Preserve explicit results, leave missing results unknown and keep unqualified job logs retryable until attribution is complete. Cover parser, poll/cache, served state and recovery mapping with offline fixtures.

Change-source: pulse-maintainer
2026-10-24 06:56:44 +01:00
pulse-triage[bot] 4505c00250 Preserve explicit audit selections before reads and webhook updates (#3179)
**Preserve explicit audit selections before reads and webhook updates**

Reject damaged query encoding and repeated singleton selectors in the
canonical list, export and summary handlers before reading events.
Require an explicit webhook urls array so absent selections cannot
silently disable forwarding; intentional empty arrays, URL validation
and tenant boundaries remain intact. Add executable handler/exporter and
mutation preservation controls and update the owning contracts.

**Compare tenant audit mutations with each request starting state**

The second tenant control must preserve the first tenant deliberate
clear, not require that the earlier update never happened. Capture both
update counters before the rejected request and require them to stay
unchanged. No production behaviour changes.

Contract-Neutral: Correct the synthetic tenant control baseline;
production audit handlers and contracts are unchanged.

---
Fleet candidate `20261024T041512Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `f98c1eae7886507e5dbc4b6e763191e0b813e652` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 05:49:21 +00:00