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.
**Show agents awaiting a report after server restart**
Keep retained enrollments unknown while their accepted-report lease is
current, then show offline if no fresh report arrives. Preserve
historical
timestamps, removals and report ordering without manufacturing
telemetry.
Use the shared first-report state in Machines, resource details and
Settings.
Refs #2980
**Keep unreported chart fallbacks empty after lease expiry**
An expired saved enrollment still has no current telemetry. Keep summary
charts from turning that deadline into zero-valued live samples, while
preserving real stored history. Cover both sides of the reporting lease
and align the remaining pending-label assertions with the shared
wording.
Refs #2980
**Record current restart browser verification**
Bind the final drawer query content to desktop, tablet and phone checks
of startup, reporting, server restart and the original lease deadline.
---
Fleet candidate `20261024T172928Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `aebe81555ecfaf2ce12d6867348fd6dc20a83164` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
After a server restart, Pulse showed saved agents offline before they
had a chance to report. Recent enrollments now show “Waiting for first
report” until an accepted report arrives or their existing reporting
lease expires.
The shared read model preserves identity and the historical receipt
without inventing metrics. Live updates carry the same source delivery
state, so a browser left open during an update withdraws its previous
current readings. The expanded drawer keeps stored history available and
shows an informational wait without a fabricated zero-value reading.
Expired agents remain offline, rejected buffered reports cannot end the
wait, removed agents remain absent, and provider errors retain their own
status. Machines, resource details and Settings consume the same state.
Validation: targeted frontend and backend regressions, broader registry
and connection checks, type checking, build, governance checks, and
desktop/mobile browser verification of startup, first report, restart
and expiry.
Refs #2980. This change addresses the startup display and does not
establish the cause of the cluster DNS failure reported there.
**Complete managed npm graph refresh on counted-navigation repair**
Retain PR3208's main UI package graph on the landed accessibility
correction. Integration axe/ws/types already match the protected graph.
Advance the portal's locked ws to 8.22.0 while retaining its compatible
override range and newer Vitest, Vite and Rollup; no production source
or portal source-manifest input changes. Exact-graph build and browser
acceptance remain pending dependency selection.
Contract-Neutral: Patch and minor npm graph refresh only; existing
rendering, counted navigation, portal, entitlement and authorization
contracts are unchanged.
**Preserve unchanged optional WASI lock representation**
PR3208 hydrates six bundled child metadata records beneath the unchanged
optional wasm32 Tailwind tarball. They add no new package payload, but
lack standalone registry URL/SHA512 required by assigned graph
acquisition. Restore main's representation of that unchanged subtree,
retaining its official parent URL, whole-tarball SHA512 and bundle
dependency declarations. Every intended main-UI update remains; exact
offline installation and browser proof are still required.
Contract-Neutral: Lock metadata normalization only; no package payload,
runtime contract, security floor or acquisition check changes.
---
Fleet candidate `20261024T162402Z-web-product` from the `web-product`
lane. Reviewed tip `30e76af97c5296154d7867fe19d3d7b17155518e` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
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.
---
Fleet candidate `20261024T162019Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `105fb05cc3f13e5a3f67a688c5ccd7135a6fd208` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
An open drawer could retain a current-only reply after its agent entered
the first-report wait. Withdraw live snapshots immediately and refresh on
reporting transitions while preserving real stored observations.
Integrate published main while retaining the webhook acknowledgement and
chart optimizations. Keep guest metric admission under provider ownership.
Refs #2980
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
An expired saved enrollment still has no current telemetry. Keep summary
charts from turning that deadline into zero-valued live samples, while
preserving real stored history. Cover both sides of the reporting lease
and align the remaining pending-label assertions with the shared wording.
Refs #2980
**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)
PR3208 hydrates six bundled child metadata records beneath the unchanged optional wasm32 Tailwind tarball. They add no new package payload, but lack standalone registry URL/SHA512 required by assigned graph acquisition. Restore main's representation of that unchanged subtree, retaining its official parent URL, whole-tarball SHA512 and bundle dependency declarations. Every intended main-UI update remains; exact offline installation and browser proof are still required.
Contract-Neutral: Lock metadata normalization only; no package payload, runtime contract, security floor or acquisition check changes.
Change-source: pulse-maintainer
Retain PR3208's main UI package graph on the landed accessibility correction. Integration axe/ws/types already match the protected graph. Advance the portal's locked ws to 8.22.0 while retaining its compatible override range and newer Vitest, Vite and Rollup; no production source or portal source-manifest input changes. Exact-graph build and browser acceptance remain pending dependency selection.
Contract-Neutral: Patch and minor npm graph refresh only; existing rendering, counted navigation, portal, entitlement and authorization contracts are unchanged.
Change-source: pulse-maintainer
**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.
**Correct counted-navigation fixtures without weakening names**
Keep the canonical shared label owner and assert the visible label/badge
word boundary instead of jsdom textContent that also includes CSS-hidden
captions, SVG titles and the decorative Icon stub. Preserve the
accessible-name, severity, route and focus assertions; runtime and
adequate own-parent browser bytes are unchanged.
Contract-Neutral: Test-fixture alignment only; no runtime or subsystem
contract delta.
**Finish shared-name fixtures and assert exact visible counts**
The first focused committed-source proof still found the removed
count-first helper and decorative Patrol SVG title in raw-text
expectations. Test shared ownership instead and assert actual label
nodes, word boundaries and exact badge values independently of their
accessible names on desktop and mobile. Runtime source and existing
browser proof stay unchanged.
Contract-Neutral: Test assertions only; no runtime or subsystem contract
delta.
**Keep platform setup assertions independent of decorative icon text**
The complete focused fixture now also reaches the primary-platform menu.
Check its exact accessible name, label and Setup badge separately, as
for counted utility links, rather than counting the hidden test icon as
visible text. Preserve every route, count, severity and keyboard
assertion and all runtime bytes.
Contract-Neutral: Test-fixture correction only; no runtime or subsystem
contract delta.
**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.
---
Fleet candidate `20261024T151031Z-web-product` from the `web-product`
lane. Reviewed tip `fbdef9ac69d8b610c3027aaac4fe0c04d60df350` on `main`.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
Keep retained enrollments unknown while their accepted-report lease is
current, then show offline if no fresh report arrives. Preserve historical
timestamps, removals and report ordering without manufacturing telemetry.
Use the shared first-report state in Machines, resource details and Settings.
Refs #2980
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
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
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
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
Raise the portal Vitest minimum to 4.1.12 and the Vite override to 7.3.7
to address seven development dependency advisories. Refresh the lockfile
and rebuild the source manifest. Emitted application JavaScript and CSS
are byte-identical.
Validation on pulse-dev: npm audit reports zero vulnerabilities, type
checking passed, all 132 frontend tests passed, the frontend build and
Go portal tests passed. The main frontend lint and type checking also
passed for this exact commit through the shared validation worker.
Canonical status and ownership audits passed.
**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)
The complete focused fixture now also reaches the primary-platform menu. Check its exact accessible name, label and Setup badge separately, as for counted utility links, rather than counting the hidden test icon as visible text. Preserve every route, count, severity and keyboard assertion and all runtime bytes.
Contract-Neutral: Test-fixture correction only; no runtime or subsystem contract delta.
Change-source: pulse-maintainer
The first focused committed-source proof still found the removed count-first helper and decorative Patrol SVG title in raw-text expectations. Test shared ownership instead and assert actual label nodes, word boundaries and exact badge values independently of their accessible names on desktop and mobile. Runtime source and existing browser proof stay unchanged.
Contract-Neutral: Test assertions only; no runtime or subsystem contract delta.
Change-source: pulse-maintainer
Keep the canonical shared label owner and assert the visible label/badge word boundary instead of jsdom textContent that also includes CSS-hidden captions, SVG titles and the decorative Icon stub. Preserve the accessible-name, severity, route and focus assertions; runtime and adequate own-parent browser bytes are unchanged.
Contract-Neutral: Test-fixture alignment only; no runtime or subsystem contract delta.
Change-source: pulse-maintainer
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.
## 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.
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.
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.
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.
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.
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.
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.
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
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.
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)
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)
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.
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
Raise Vitest to 4.1.12 and Vite to 7.3.7 to close the portal tooling advisories. Refresh the lockfile and regenerate the frontend source manifest.
Contract-Neutral: Development dependency security fixes; emitted application bundles are byte-identical and the portal product contract is unchanged.
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)
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
## Problem
From a Patrol Inbox alert, "Browse Patrol findings for this resource"
opens Activity → Finding options and history scoped to that resource.
The rows were scoped, but everything above them still described every
open finding: the Open work line, its work groups, the approval banner
and the filter chip counts. On a live stack the afs-web scope read
"Patrol found 3 issues on 2 affected resources — 2 recurring" over its
two rows. On the private stack used for this PR, the postgres-replica-01
scope read "Patrol found 12 issues on 10 affected resources — 1 failed
fix, 6 recurring", "2 actions awaiting your approval", "1 failed
action", "Needs Attention (1)" and "Approvals (2)" over two rows.
## Change
- `PatrolIntelligenceWorkspace`: in a resource scope the summary and
work types are counted over that resource's open findings (the rows its
Active filter shows), and the line names the resource: "Patrol found 2
issues on postgres-replica-01 — 2 recurring".
- `ApprovalBanner` takes the scoped finding IDs: in a scope it shows
only approvals for that resource's findings, so one about to expire
keeps its countdown, and the approvals work group counts the same set.
- The latest check's health and coverage-stale groups describe every
resource, so a scope leaves them to the unscoped view; the page header
keeps the last check.
- `FindingsPanel`: Needs Attention, Approvals and Overdue chips count
only the scoped resource's findings. Whether the filter bar shows is
unchanged, so a resource with no open findings can still reach Resolved
and All.
- The banner's context line now ellipsizes inside the banner at phone
width instead of running past its edge.
- Contract notes: patrol-intelligence (owner), api-contracts,
frontend-primitives.
## Verification
- Private mock stack (backend from main, this frontend via Vite) with
two seeded findings on postgres-replica-01, whose powered-off alert is
an Inbox item. Inbox → Browse → scoped panel in headless Chromium at
1440×900 and 390×844, before (main's files) and after, with and without
two pending approvals answered for the page (pending Patrol approvals
need the Pro orchestrator). After: "Action awaiting approval · medium
risk · expires 8m 58s · Grow the WAL volume on postgres-replica-01…", "1
approval waiting", "2 recurring issues", "Approvals (1)", two rows.
Receipt: `frontend-modern/browser-verification/4c5b985630a42042.json`.
- New tests: `PatrolIntelligenceWorkspace.resourceScope.test.tsx` (real
workspace, FindingsPanel and ApprovalBanner: scoped counts, a findings
refresh, the end of the scope), the `FindingsPanel resource scope`
cases, the `ApprovalBanner` targetIds cases and two
`patrolControlPresentation` cases. Each scoped test fails on main's
code; the unscoped controls pass on both.
- pulse-dev: Prettier, ESLint, the frontend audits, `tsc`, 112 test
files (1,988 tests) across the Patrol, AI, page, store and shared
suites, and an earlier full-suite run (1,268 files; its one failure, a
docs-heading test timing out under load, passes on its own).
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