Preserve source provenance in REST-loaded resources so owning-snapshot
consumers retain provider context and host placement. Keep visible control wording in accessible names and retain readable command
and navigation text in both themes. Token fixtures now await mounted browser
bootstrap before handing control to the next journey.
Measure bounded platform table items together with their inline details so
scrolling down a tall drawer cannot unmount the resource being inspected.
Retain regression coverage for bootstrap timing, drawer state and scroll
extent, responsive controls, Patrol and availability, and align navigation
assertions with current read-only capabilities and composed resource queries.
Provider fixtures retain one authoritative inventory across transport updates.
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.
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.
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.
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.
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)
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.
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.
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.
Enforce the captured-reader check in the shared fixture, not only
byte-bound callers. Observe exit through its identity-checked pidfd,
retain the 500 ms bound and recheck the final boundary without another
delay. A survivor still fails after separately observed emergency
cleanup, reporting the copied-shell exit and reader state without log
bytes.
Exercise a deliberately leaked synthetic reader, last-boundary exit, PID
reuse and unavailable cleanup. The historical Docker stderr failure
remains unexplained; no copied recipe, privacy/byte/deadline limit or
rendered help changes.
Contract-Neutral: Test fixture observation and diagnostics only;
documented readers, runtime and public contracts are unchanged.
---
Fleet candidate `20261024T093936Z-web-product` from the `web-product`
lane. Reviewed tip `c83df572334b19e5dde668088e763dc57489701a` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
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
The promotion caller now owns the terminal failure annotation, so a
re-dispatched refusal leaves none behind. The release-asset contract
test still pinned the old annotation inside wait_for_workflow. It now
pins the caller's annotation and the source-check refusal gate.
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.
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.
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.
Enforce the captured-reader check in the shared fixture, not only byte-bound callers. Observe exit through its identity-checked pidfd, retain the 500 ms bound and recheck the final boundary without another delay. A survivor still fails after separately observed emergency cleanup, reporting the copied-shell exit and reader state without log bytes.
Exercise a deliberately leaked synthetic reader, last-boundary exit, PID reuse and unavailable cleanup. The historical Docker stderr failure remains unexplained; no copied recipe, privacy/byte/deadline limit or rendered help changes.
Contract-Neutral: Test fixture observation and diagnostics only; documented readers, runtime and public contracts are unchanged.
Change-source: pulse-maintainer
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
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.
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.
Change-source: pulse-maintainer
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.
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.
Change-source: pulse-maintainer
Select the ten outstanding non-storage updates from managed proposal 3056 and their seven transitive updates without importing its compiler downgrade, storage changes or checksum pruning. Preserve Go 1.26.10, x/net 0.60.0 and the aligned Kubernetes graph. Extend existing manifest-floor proof; exact-graph runtime acceptance remains required after dependency acquisition.
Contract-Neutral: Selected dependency versions and existing internal graph-admission floors only; no runtime API, wire format, permission, deployment interface or installer contract changes.
Change-source: pulse-maintainer
Isolate the retained PR3095 completion (7f4ec192 and 93d87118) without
importing unrelated history. Keep the exact proposed Python classifier
and maintained chart shells, real CLI/workflow controls and dependent
contracts. Inconclusive capacity never establishes the 90 ms budget;
child, cleanup and malformed-proof failures remain errors.
Source-PR: https://github.com/rcourtman/Pulse/pull/3095
Source-Commit: b31fabd778
Co-authored-by: rcourtman <8825017+rcourtman@users.noreply.github.com>
---
Fleet candidate `20261024T010002Z-delivery-trust` from the
`delivery-trust` lane. Reviewed tip
`7e7cea87a6bce2e7a2e00e888a676c571e141d92` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
Compose retained 8478eac3 additively on current main. Preserve both independently appended contract sections at each of the three documentation conflicts; workflow, registered executable controls and classifier stay unchanged.
Change-source: pulse-maintainer
The Unraid installer appended `bash <wrapper>` to /boot/config/go. The
wrapper is an endless watchdog loop, so that line never returned, and
rc.local never reached anything the user had placed after the Pulse
block. On a maintainer host a reboot skipped swap, a memory logger and a
hosts entry the agent needed to reach its server, and Pulse showed the
host as down for two days while it was healthy.
The entry now backgrounds the wrapper with its output redirected, so go
returns and holds no boot output pipe open. Install-time cleanup used to
delete every go line containing "pulse-agent", including log directories,
hosts entries and a co-installed agent. It now removes only the
installer's marker and command lines that name a start-pulse-agent.sh
wrapper or this runtime binary outside a comment, so upgrades from the
foreground or hand-edited entry still leave exactly one launch. Kept
lines lose a trailing CR, as Unraid's fromdos does before running go, and
a removal that would leave go unparseable is refused with a warning.
Uninstall admits the detached entry as well as the earlier foreground
form.
The new tests execute the go file the installer writes and require it to
return, release its output pipe and run the next line, with a planted
foreground entry as the control, and pin the narrowed cleanup against
the removal filter. The NAS teardown controls now render their Unraid
entry through the same emitter.
Fleet lanes committed a fixture page, build script and check script
alongside each fix to prove it once in a browser. This removes the 168
such files (about 19,850 lines) that nothing in the repository names: no
subsystem contract, release verification matrix, test, workflow, package
script or live harness. Harnesses the contracts and the high-risk
verification matrix describe, and the modules they import, stay. The
browser verification guard covers `frontend-modern/src` only, so no
receipt is involved.
Isolate the retained PR3095 completion (7f4ec192 and 93d87118) without importing unrelated history. Keep the exact proposed Python classifier and maintained chart shells, real CLI/workflow controls and dependent contracts. Inconclusive capacity never establishes the 90 ms budget; child, cleanup and malformed-proof failures remain errors.
Source-PR: https://github.com/rcourtman/Pulse/pull/3095
Source-Commit: b31fabd778
Co-authored-by: rcourtman <8825017+rcourtman@users.noreply.github.com>
Change-source: pulse-maintainer
Fleet lanes committed a fixture page, build script and check script
alongside each fix to prove it in a browser once. 168 of those files,
about 19,850 lines, are named by nothing in the repository: no
subsystem contract, release verification matrix, test, workflow,
package script or live harness. Harnesses the contracts and the
high-risk verification matrix describe, and the modules they import,
stay. The lanes no longer commit proof-only fixtures.
Contract-Neutral: removes proof fixtures no contract, registry entry or test references; no runtime or contract change
Reconcile the retained snapshot publication repair with current main without importing unrelated containing history. Unique private staging, no-clobber publication and failure-before-mutation protect earlier recovery copies; retain zero-retention and hardened-unit fallback semantics.
Retained-source: 854ea3c81a94ffe88145d663d25d5040d4637a47, 91bd7393fc4413b6629ad851f90c2c7f6c2c0991
Change-source: pulse-maintainer
**Reproduce PVE partial-scan recovery history loss**
Drive ordinary backup polling through durable recovery ingest after live
inventory replacement, with partial, complete, isolation and
cancellation controls.
**Retain PVE recovery history across incomplete backup scans**
Use the poll's existing completeness evidence to admit destructive
reconciliation. Failed or skipped reads may update accepted rows without
deleting unseen durable facts, and cancellation cannot publish an empty
success. Complete deletions remain scoped and tested.
Contract-Neutral: Restore collection truth without changing APIs,
schemas, configuration or protection policy.
**Keep carried PVE rows from renewing recovery observations**
Retain their existing durable point evidence under the partial provider
observation instead of presenting a failed refresh as a fresh artifact
observation. Cover warm inventory alongside the cold-start
reconciliation regression.
Contract-Neutral: No API, schema, configuration or protection-policy
delta.
**Match the full workload identity in the PVE browser journey**
Use the production disclosure's guest name and VM identity, and bring
its provider evidence into the captured viewport. The initial selector
timeout is retained as a harness failure, not a product result.
**Avoid the published monitoring contract insertion conflict**
Relocate the PVE partial-backup collection contract without changing its
text so it composes with the published snapshot-listing contract at
15ea29fe2b. Runtime, regression tests and
browser-proof inputs remain unchanged.
---
Fleet candidate `20261023T194015Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `d39d65fede2a6afc535c0387b0c0e6683868b22e` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
**Cover failed snapshot listings without treating errors as deletion**
Exercise both guest kinds, partial error results, same-VMID peer
isolation, warning continuity, accepted removal and explicit disablement
through the actual monitor. Include opt-in synthetic frames for the
production alert consumer.
**Use the served alert model in snapshot collection proofs**
The monitor exposes models.Alert, not the internal manager alert type.
Correct the fixture-only type mismatch before establishing the
unchanged-runtime control.
**Keep known LXC snapshots when their listing is unavailable**
Treat every failed snapshot read as incomplete rather than interpreting
596 or unavailable text as an empty successful listing. Preserve known
membership and warnings while independent accepted listings and
confirmed removal continue normally. Correct the isolation fixture to
use cluster-unique VMIDs, keeping the same-VMID cross-instance control.
Contract-Neutral: Restores existing failed-listing semantics for LXC; no
public API, schema, configuration, identity or alert policy delta.
Dedicated collection and producer/browser regressions are outside the
registry's fixed verification filenames.
---
Fleet candidate `20261023T190018Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `850a6310446030354d29787b84e794e0b37852db` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
Use the production disclosure's guest name and VM identity, and bring its provider evidence into the captured viewport. The initial selector timeout is retained as a harness failure, not a product result.
Change-source: pulse-maintainer
Retain their existing durable point evidence under the partial provider observation instead of presenting a failed refresh as a fresh artifact observation. Cover warm inventory alongside the cold-start reconciliation regression.
Contract-Neutral: No API, schema, configuration or protection-policy delta.
Change-source: pulse-maintainer
Use the poll's existing completeness evidence to admit destructive reconciliation. Failed or skipped reads may update accepted rows without deleting unseen durable facts, and cancellation cannot publish an empty success. Complete deletions remain scoped and tested.
Contract-Neutral: Restore collection truth without changing APIs, schemas, configuration or protection policy.
Change-source: pulse-maintainer
Treat every failed snapshot read as incomplete rather than interpreting 596 or unavailable text as an empty successful listing. Preserve known membership and warnings while independent accepted listings and confirmed removal continue normally. Correct the isolation fixture to use cluster-unique VMIDs, keeping the same-VMID cross-instance control.
Contract-Neutral: Restores existing failed-listing semantics for LXC; no public API, schema, configuration, identity or alert policy delta. Dedicated collection and producer/browser regressions are outside the registry's fixed verification filenames.
Change-source: pulse-maintainer
Resolve the shared contract insertion by retaining both complete subsections. Preserve the reviewed snapshot creation-date repair and current main's independent backup success-date repair, tests and fixtures.
Contract-Neutral: Compose existing snapshot and backup warning preservation repairs without changing their public API, configuration, schema or canonical identity contracts. Retain both internal contract clarifications and executable verification.\nChange-source: pulse-maintainer
Change-source: pulse-maintainer
Improvement: backup-age false-recovery repair now has executable and
desktop/phone pixel proof.
**Continuation — truthful monitoring and backup evidence.** Candidate
**6fb43d9302d6673bc65193f3e17944098394ad6d**, above **c67f8bd500**,
contains **208f0cbf1e** plus an additive fixture correction.
Original-runtime control reproduces false recovery; full alert race/vet,
final consumer type checks, **1,404 tests**, compiled real Overview and
both browser journeys pass. Complete output hashes match, source guests
stopped, and all eight captures were natively opened and inspected.
Coordinator/reviewer/publisher own containing CI, independent review and
landing; their containing result or repair return is next. **Not landed,
released or installed recovery.** [Receipts](RECEIPTS.md),
[source](SOURCE.json), [continuation](CONTINUATION.json).
This raises the highest-ranked **guest/backup safety** weakness: a
still-listed backup's missing/zero/future date no longer clears an
established warning or hides valid provider evidence. Last trusted
occurrence/evidence survives; valid recent success, exclusions and
authoritative removal still clear normally. The executed baseline
establishes the defect, not a field incident's cause or native thaw
safety. [Reproduction](REPRODUCTION.json), [scope and original
jobs](RECONCILIATION.json).
The first consumer proof exposed a fixture-only type error; the additive
correction uses the current AI capability store without changing
production code or weakening checks. Both commits pass canonical
preflight; the frontend-file guard skip is not the separately completed
browser proof. [Validation](VALIDATION.json), [adverse
evidence](ADVERSE.json), [pixels](PIXEL-REVIEW.json).
Received continuations **416bb1c9 / 58e731f1** are completed at the
source-validation step. Previous node candidate **3c66ab14e7** and
Delivery/Pro/Host/PI's original installed guest, dashboard, SSO and
notification recoveries remain separately retained. No duplicate
handoff, infrastructure, guest probe, excluded investigation, release or
public action.
---
Fleet candidate `20261023T172026Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `6fb43d9302d6673bc65193f3e17944098394ad6d` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
Distinguish a listed snapshot with an unusable date from removal. Retain the last trusted canonical occurrence and evidence after current policy resolution, including Proxmox's missing-snaptime Unix epoch representation. Add lifecycle, exclusion, identity and real-consumer browser proof fixtures.
Contract-Neutral: Repair existing snapshot warning preservation on incomplete evidence; no API, configuration, schema or canonical identity change. Dedicated regression and browser-producer tests accompany the internal contract clarification.
Change-source: pulse-maintainer
Since 22 October the E2E jobs often time out installing browsers before
any test runs: 29 installs failed that day and 30 on 23 October, against
none in the week before. Each job downloads about 119 MB of Ubuntu
packages for Chromium and WebKit from the runner's Azure mirror, and
some GitHub-hosted runners now get tens of kB/s from it
(actions/runner-images#14823 and #15040). The same package set on the
same image versions installed in under a minute before then.
The three browser jobs now restore the newest saved .deb set with
actions/cache/restore and admit it through scripts/browser_os_packages.py.
apt reuses a file already in its archive directory after checking only
its size, so the helper refreshes the index and installs a private copy
of a cached file only when that copy's SHA256 matches a signed index
stanza for the package, version and architecture its filename names.
After a successful install, and before any journey can fail the job,
the helper rebuilds the cache from apt's archives and the job saves it
under a run-unique key only when the set changed, so Ubuntu updates
refresh the cache and an empty or unchanged set is never saved.
The transport bounds and install budgets are unchanged, and the jobs
hold no secrets or write tokens, as the workflow trust contract requires
for caches.
A still-listed backup with a missing, zero or future success date is not recovery evidence. Keep its existing occurrence and prefer the newest usable date across provider rollups, without changing explicit exclusions or inventory removal.
Add manager regressions and a real Overview consumer fixture. Exact-source execution and browser/pixel verification remain pending the fresh launch required by the completed dependency acquisitions.
Change-source: pulse-maintainer
## Why
The web chat window and its backend routes are retired (#3126, #3137),
so the `pulse_assistant` surface now only serves Pulse Mobile's chat.
Its manifest description still called it "the contextual explanation,
approval, and handoff surface for Patrol findings". That text is public
in three places: the overview at the top of `docs/AI.md`, the MCP README
surface block, and the External agents settings panel.
## What changes
- **Manifest description.** It now reads "The chat surface Pulse Mobile
uses for operator questions over the same governed tools, approvals, and
verification; the in-app web chat window is retired." The generated docs
blocks follow it. The surface ID, label, affordances and tools are
unchanged.
- **Browser fallback.** `frontend-modern/src/api/agentCapabilities.ts`
no longer gives an undeclared Assistant surface `interactiveQuestions`.
That matches the backend default of tools only.
- **Tests and contracts.** The Go, Python docs-policy and frontend tests
that pinned the old text and fallback are updated. The settings panel
test now asserts that no "Interactive questions" badge appears. The
ai-runtime and api-contracts contracts carry dated notes.
Checked on a private stack built from this branch, at desktop and phone
sizes:
- **What it shows:** under External agents → Developer details → Patrol
access model, Pulse Assistant has "Native surface" and "Actions" badges
and the new description, and no "Interactive questions" badge appears.
- **Layout and errors:** no horizontal overflow and no console errors.
The receipt is included.
## Not in this PR
- The dead `highlight.js` chunk rule in `vite.config.ts`. It matches
nothing since the package was removed. The commit guard requires a
build-proof file change for any edit there, which isn't worth it for a
no-op.
With the web chat window and its backend routes retired (#3126,
#3137), the pulse_assistant surface only serves Pulse Mobile's chat.
Its manifest description still called it the explanation and handoff
surface for Patrol findings, and that text is public: the docs/AI.md
overview, the MCP README surface block, and the External agents
settings panel all show it. It now names the Pulse Mobile role and says
the web chat is retired.
The browser's fallback affordances for an Assistant surface with none
declared still offered interactive questions. They now match the
backend default of tools only, so no client path advertises a question
tool nobody can answer.
The macOS Intel native job killed the DSM Upstart suite at its single
one-minute alarm after sixteen controls had passed (main 39883120153,
PR 39952586718). Passing runs take 41s on Intel, 23s on ARM and 14s on
Linux, so the budget measured runner speed, not a stuck teardown. The
Legacy suite sits at 35s against the same alarm.
Run each discovered systemd, OpenRC, legacy and Upstart control through
the shared helper under its existing bound, as the NAS and TrueNAS suites
already do. Required controls stay mandatory, and skipped, absent or
failed controls still fail. Installer deadlines are unchanged.
## Why
The in-app Assistant chat window was retired in #3126. Several backend
routes existed only for that window, and Pulse Mobile, the remaining
chat client, never called them. This removes them and the code that only
they reached.
## What changes
- **Routes removed:**
- `GET /api/ai/assistant/surface-tools`
- `POST /api/ai/workflow-prompts/render`
- `POST /api/ai/question/{id}/answer`
- the `/summarize`, `/fork`, `/undo`, `/redo`, `/steer`, `/diff`,
`/revert` and `/unrevert` sub-resources under `/api/ai/sessions/{id}`.
These now return `404` after the existing `ai:chat` scope check.
- **Code removed behind them:**
- the handlers and their `AIService` methods;
- the chat service's fork, undo, redo, steer, summarize, answer-question
and surface-tool methods;
- the session store's fork and turn-undo operations and the persisted
redo stack (`can_redo` leaves session summaries);
- session compaction, the agent loop's steering inbox and
question-answer wait, the `steered` message flag, and the `question` and
`steer_applied` stream events.
- **The question tool is no longer offered.** Interactive chat still
offered `pulse_question`, and Pulse Mobile has no way to answer it, so a
Mobile turn that called it waited until the request ended. Now:
- no profile offers the tool;
- the agent loop answers any call with a refusal telling the model to
ask in its reply text, the same path Patrol runs already took;
- the chat system prompt no longer mentions a clarification tool;
- the agent capabilities manifest's Pulse Assistant surface no longer
declares the `interactiveQuestions` affordance, so the prompt's
affordance text, the fallback governance names, and the generated
`docs/AI.md` and `cmd/pulse-mcp/README.md` surface blocks drop it. The
External agents settings panel stops showing an "Interactive questions"
affordance for Pulse Assistant. The shared `pulse_question` declaration
stays.
- **Docs and contracts:**
- `docs/API.md` lists the remaining routes under "Chat Sessions (Pulse
Mobile)", including the rename route Mobile already used.
- The ai-runtime and api-contracts rules for the removed workflows are
rewritten, and the dependent contracts carry dated notes.
## Unchanged
- `/api/ai/chat` and the session list, create, rename, delete, messages
and abort routes, which are the Pulse Mobile relay route list.
- `/api/ai/status`, which the dev health check uses.
- `/api/ai/workflow-prompts/activity`, which Patrol uses.
- Patrol investigations, provider routing, model-boundary sanitizing and
approvals.
- Old session files: they load as before and drop the redo stack and
`steered` flags on their next write.
## Not in this PR
- The Pulse Assistant surface description in the manifest, which still
calls it a handoff surface for Patrol findings. It now describes Pulse
Mobile chat and can be reworded with the surface contract when Mobile
retires.
- The browser's legacy fallback in
`frontend-modern/src/api/agentCapabilities.ts`. It still gives Pulse
Assistant `interactiveQuestions` when a manifest declares no affordances
at all. A same-version server always declares them, so that path is
unused; it changes with the surface description.
- The dead `highlight.js` chunk rule in `vite.config.ts`.
## Release note
> Removed API routes the retired in-app chat window used: Assistant
surface tools, workflow-prompt rendering, question answers, and the
session summarize, fork, undo, redo, steer, diff, revert and unrevert
routes. Pulse Mobile's chat is unaffected.
The in-app Assistant chat window was retired in #3126. Several backend
routes existed only for it, and Pulse Mobile, the remaining chat client,
never called them: assistant surface-tools, workflow-prompt render,
question answers, and the session summarize, fork, undo, redo, steer,
diff, revert and unrevert sub-resources. This removes them along with
the handlers, service and session-store methods, the persisted redo
stack, session compaction, mid-turn steering and the question-answer
wait behind them.
The question tool needed more than a route removal. Interactive chat
still offered pulse_question, and Pulse Mobile has no way to answer it,
so a Mobile turn that called it waited until the request ended. No
profile offers it now. The agentic loop answers any call with a refusal
that tells the model to ask in its reply text, the path Patrol runs
already took, and the chat prompt drops its clarification-tool advice.
The capabilities manifest's Assistant surface stops declaring
interactive questions, so the generated docs and fallback governance
names follow it.
/api/ai/chat and the session list, create, rename, delete, messages and
abort routes Pulse Mobile uses are unchanged, as is /api/ai/status. Old
session files load as before and drop the redo stack and steered flag
on their next write. docs/API.md lists the remaining chat routes, and
the subsystem contracts record the removal.
The agent installer checked an --action-token-file's permissions with
GNU-only stat -c. BSD stat on macOS rejects it, the mode read came back
empty, and every action token file was refused as group- or
world-accessible. It now uses the existing portable_path_mode helper,
which the collector token path already relies on.
The ingress test runs both token options through the real installer
functions and the compiled token reader; on the macOS runner it is the
proof for this fix.
Contract-Neutral: portability fix restoring the documented --action-token-file option; no contract change
Pulse Assistant was a chat drawer beside the monitoring UI. Operators who
want to talk to their infrastructure already use Claude Code, Codex, or
another MCP client, and Pulse MCP gives those agents the same governed
tools. Keeping a second, weaker chat in the web app cost a 66 kB chunk,
a launcher on every page, hand-off buttons on alerts, findings, approvals
and drawers, and thousands of lines of OpenCode-parity drawer code, for a
feature the maintainer judged ineffective.
This removes the browser chat and everything that only fed it:
- The drawer, its launcher (desktop and phone), the command-palette
Assistant commands, the Escape and blocking-dialog shell plumbing, and
the chat store, client, dev stream fixture and generated SSE types.
- Every "Open in Assistant", "Explain with Assistant", "Discuss" and
"Ask Assistant" hand-off on alerts, incident timelines, Patrol findings,
approvals, run history, the attention workbench and resource drawers,
with their context models and the chat-only helpers they used.
- The Assistant settings page and its session maintenance section. The
page becomes "External agents" at /settings/pulse-intelligence/agents;
the old /assistant path, ?tab=assistant and system-ai-assistant redirect
there with the hash kept.
Alert Investigate now starts a targeted Patrol run (resource, alert ID and
type) and says where the findings land, and it ignores repeat clicks while
the request is in flight. The run route needs an admin with ai:execute, so
security status gains settingsCapabilities.patrolRun, derived from the same
snapshot as the other admin capabilities but following the route for
org-scoped admins, organization-bound tokens and a cookie beside a token,
and Investigate is offered only where it is true. The Patrol
tool transcript block moves to components/patrol. A small aiAvailability
store holds the session assistantEnabled fact that gates Patrol entry
points and Investigate; it now clears and refreshes on organization switch,
refreshes when organization loading settles on another org and after
provider removal, and ignores answers that arrive after a newer one.
The backend chat service stays: Patrol investigations run on it and
Pulse Mobile chat uses it until 31 March 2027, so /api/ai/chat, the
session endpoints and the stream contract are unchanged. A new contract
snapshot pins the steer_applied event that only the deleted TypeScript
projection used to cover.
highlight.js, used only by the chat's code highlighter, leaves the
dependencies, as does the DOMPurify carve-out that kept its fence hints.
Contracts, registry and readiness proofs drop the retired surfaces; the
ai-runtime contract loses about 1,600 lines of drawer rules while keeping
every backend session, stream and provider rule. The production build
ships about 100 kB less gzipped JavaScript.
The PMG help says to compare the same time window in PMG, and its docs
check pinned the old drawer's `stats()?.timeframe` read. The Mail tab is
now headless mail-flow content inside the shared resource drawer and
names the window on its Mail flow card from the same field
(`stats?.timeframe`); the check now accepts either spelling, so the
help still has to describe a window the drawer actually shows.
The earlier drawer changes left a few surfaces that still looked or
behaved differently, plus code nothing used any more:
- Proxmox Mail Gateway had two drawers for one gateway; the row now
opens the shared resource drawer with mail flow as its Mail tab. The
Ceph cluster drawer gains the canonical header, Overview (alerts,
Worth checking, capacity history, cards, closed Details) and Manage,
and its dot follows the row instead of reading grey on missing health.
- The Kubernetes cluster drawer's Deployments tab repeated the Workloads
table with dead-end rows; it is gone, and Namespaces opens the
Workloads tab scoped to the cluster and namespace with one action.
The Docker Swarm drawer tab could not be reached from any page; gone.
- On phones, windowed tables (Kubernetes deployments, pods) and the
Storage tab's grouped pools stopped at 70-90% of the width: a
colspan=100 spacer or colspan=99 group row created phantom columns
that took the width hidden columns left. Those rows now span exactly
the visible header cells.
- The node and Docker host drawers gain the guest drawer's Open action;
the Docker host withholds readings while offline or stale, lists
operator state first in Manage and shows its action history. A
guest's physical disks open the same disk Overview as the Storage tab,
with the guest agent's disk temperature alerts.
- One "No active alerts" line, one timestamp row and wrapping alert
subjects replace per-drawer copies; PBS and PMG alerts match on their
instance id so drawers find them without a metrics target.
- Removes the old disk detail and its presentation code, the Swarm and
Kubernetes Deployments drawers and their presentation helpers, and
drawer state nothing read.