A globally observed second cluster can leave a PBS source apparently mapped to the first while both clusters match a shared-node snapshot. Do not promote a positive but tied guest match into the VMID-only inference path. Regression covers both VM and CT completed and in-flight snapshots with the second cluster visible on another PBS instance.
Contract-Neutral: Preserve existing PBS backup attribution safety without changing API fields, persisted shapes, or source contracts.
Change-source: pulse-maintainer
A newer snapshot from another cluster can share the same typed VMID and node namespace. Compare each positive match with competing guests before selecting by recency; a stronger competitor excludes it and a tie needs independent attribution. Cover VM and CT completed and in-flight snapshots while preserving the newer weaker unique match.
Contract-Neutral: Repair existing backup badge attribution and age selection without changing API fields, persisted shapes, or PBS source contracts.
Change-source: pulse-maintainer
A stronger namespace or comment score currently makes an old snapshot beat a newer, still attributable snapshot. Keep the existing typed-VMID and cross-instance eligibility guards, then choose the latest eligible backup for LastBackup and in-flight state. Add VM and CT regressions for unique fallback and colliding IDs.
Contract-Neutral: Repair existing backup-age selection without changing API fields, persisted shapes, or PBS attribution eligibility.
Change-source: pulse-maintainer
## What changed and why
Keep queued notifications from being cancelled while saved destinations
load, and make stable-channel update discovery work with GitHub's large,
prerelease-heavy release list (#2282). Update download selection now
requires the exact server archive for the runtime architecture; ARMv6
and ARMv7 are distinguished by the build variant rather than silently
choosing ARMv7. The batch also retains the reviewed disk-monitoring
round-trip correction.
## Reviewed source
`b1e75fb9e4693c5706b2242760d6277e5291e7f9..18f2dc1f19416bd83ab72e7f82f2287063b0bedc`
(notification test isolation and startup gate, integration of update
metadata, exact-archive selection, ARM variant correction).
## Validation
Provenance and diff checks passed for the complete range. Prior
committed-source offline proofs passed for the update package through
`ab0aa1ef02698f7727b134a6f6b5ed520930cc61` and for the notification
startup race scenario. The new ARM mapping has a focused regression test
in this head; exact-head protected CI remains required before landing.
These source checks do not establish installed update or alert-delivery
results, nor release readiness.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
Combine the protected schema-v18 sender with the exact-archive updater. Require installable archives in positive fixtures, classify missing archives and unavailable Pro checks without claiming an offer, and avoid caching an unactivated Pro result. Record the contract and exact combined-source browser receipt.
Change-source: pulse-maintainer
Combine the reviewed main-line sender with the in-flight archive correction so the interaction can be repaired and proved at exact source.
Change-source: pulse-maintainer
This fixes#2285. A failed update check never shows up in the update
counters because nothing gets applied, so telemetry couldn't tell an
install whose check is broken from one that was offered an update and
ignored it. That's how the 6.4.3-rc.1 installs looked fine while their
check failed on every call.
The update manager now keeps a small record of the last check on the
install's own channel, just a fixed outcome like up_to_date, available,
no_release, rate_limited, network_error, metadata_error, skipped or
error, plus whether an update was offered. Lookup failures get tagged
with their category without changing the error text people see, and
previewing the other channel from Settings doesn't overwrite it.
Telemetry schema 18 sends that as update_channel, update_check_outcome
and update_available, and nothing else leaves the install, no versions,
URLs or error text.
The Settings preview type, the privacy page, the adoption report and the
subsystem contracts are updated to match. The receiver side is in the
pulse-pro repo and the schema parity check passes against it. Old
receivers just drop the new fields, so the two can deploy in either
order.
A failed update check never reaches the update history, because nothing
was applied, so telemetry could not tell an install whose check is broken
from one that was offered an update and ignored it. The 6.4.3-rc.1 installs
looked healthy while their check failed on every call (#2282).
The update manager now records a closed outcome for every check on the
install's effective channel (up_to_date, available, no_release,
rate_limited, network_error, metadata_error, skipped, error), tagging
lookup failures without changing their error text, and ignores previews of
another channel from Settings. Telemetry schema v18 exports update_channel,
update_check_outcome and update_available. No version, URL or error text
leaves the install. The Settings preview type, privacy disclosure, adoption
report and subsystem contracts are updated to match; the Pulse Pro receiver
change lands separately and the parity check passes against it.
ARMv6 and ARMv7 server binaries both report GOARCH=arm. Read the release binary GOARM build setting and fail closed when it does not name a published server archive, rather than offering an ARMv7 update to ARMv6 installations. Cover the architecture mapping and document the installability boundary.
Change-source: pulse-maintainer
Release metadata contains agent, MCP and multiple architecture archives. An incomplete release must not offer an unrelated tarball or advertise an update that cannot be applied. Retain server archive names only, require the exact tag and runtime architecture, and record this installability boundary with regression coverage.
Change-source: pulse-maintainer
This fixes#2282. The update check read GitHub's whole release list into
memory behind a 1 MiB bound, but that list is around 9.6 MB now because
every 6.4.x release carries 349 assets. So the main path always failed
and fell through to the Atom feed, which only sees the last three or so
real releases. On main the stable channel check was failing outright,
and 6.4.5 stable would have shipped relying on the feed.
The list and the single release notes response are now decoded as a
token stream. Only the tag, name, body, draft and prerelease flags,
publish time and the Pulse linux server archives are kept, so memory
follows what the updater actually uses rather than the size of the
response. That lets the byte bound go up to 64 MiB and it still returns
the typed error, so the feed stays as a last resort for that case only
and a custom update server is never swapped for github.com. Malformed
metadata is still a hard error.
I ran it against live GitHub and the stable channel resolves v6.4.1 and
the preview channel resolves v6.4.5-rc.3 through the normal path without
touching the feed. The new tests in internal/updates build a
multi-megabyte release list shaped like the real one and cover null
fields, trailing data and the byte bound. The deployment-installability
contract is updated to say the feed can't be the primary source.
GitHub's release list is now ~9.6 MB because every 6.4.x release embeds
349 assets (~600 KB of JSON each). The update check buffered the whole
response behind a 1 MiB bound, so the primary path always failed and fell
through to the Atom feed. That feed holds ten entries and each release now
spends three of them, so stable-channel checks on main failed outright
("no suitable release found for channel stable") and 6.4.5 stable would
have shipped relying on it.
Decode the release list and the single-release notes response as a token
stream, keeping only tag, name, body, draft, prerelease, publish time and
the Pulse linux server archives. Memory now tracks what the updater keeps,
so the byte bound rises to 64 MiB while staying typed; the Atom feed stays
as the last resort for that over-limit condition only.
Verified against live GitHub: stable resolves v6.4.1 and rc resolves
v6.4.5-rc.3 through the primary path with no feed fallback.
The monitor restored notification destinations after constructing a queue whose five-second ticker could consume due jobs. Defer processor installation through destination restore, then wake the backlog before external alert callbacks admit first agent reports. Cover both forced pre-configuration processing and the actual monitor restart path; update the notifications, monitoring and agent-lifecycle contracts.
Change-source: pulse-maintainer
SetProcessor wakes the live queue worker while this test processes a fetched row manually. Defer the row beyond GetPending before installing the processor so a competing worker cannot decide the synchronous assertion.
Change-source: pulse-maintainer
## What and why
Keep Vitest and its V8 coverage provider on matching major versions.
Dependabot's isolated @vitest/coverage-v8 5 proposal (#2277) cannot
install alongside Vitest 4; defer only major updates for both packages
until the pair can be migrated and verified together. Minor and patch
updates, including security updates, remain eligible.
## Included source
- Pulse main:
`6a9db329a7ee88fd890c303812beb9da18b9d178..c0cfa8ef7e00650aade0c35e9b5bf3b717384e07`
(one reviewed commit).
## Validation
- Focused Dependabot configuration tests: `python3 -m unittest
scripts.tests.test_dependabot_config` (7 passed).
- Publication metadata and ancestry checked against current main.
Exact-head hosted checks remain required before merge.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
The isolated coverage-v8 5 proposal cannot install beside Vitest 4 because its peer range requires Vitest 5. Ignore only version majors for both packages until a reviewed frontend migration can update and prove the pair together; retain grouped security updates and minor/patch updates.
Change-source: pulse-maintainer
## What changed and why
This batch restores reliable physical-disk wearout alert recovery
without muting other disks (Pulse #2112), shows linked guest disks,
SMART and RAID in VM details (#2263), preserves spaced SSO group
mappings (discussion #2266), and keeps Patrol attention visible when
setup is blocked (#2257). It also defers incompatible major ESLint and
lucide-solid Dependabot updates (#2190, #2191) pending explicit
migrations, while leaving minor, patch and security updates enabled.
The initial proposal's Frontend job exceeded the WorkloadsSurface bundle
budget: 73.93 kB against a 70.23-kB cap. The final additive correction
loads SMART detail and history only when a disk expands, retaining a
loading state and keyboard behavior. This addresses the measured source
defect without raising the budget.
## Reviewed source
`pulse` main:
`27036a244550f3095aa883a74716374107aaa6cd..48af6960a5cfcaa67bf6a5476d8902d9710664fa`
(12 commits). The prior proposal head
`cd43b6c692383865592c17a0853d33a229ae46d6` remains in the ancestry;
`48af6960a5cfcaa67bf6a5476d8902d9710664fa` is one additive correction.
It includes the reviewed Web guest-storage, SSO and Patrol candidates;
Core wearout and physical-disk alert-policy repairs; Dependabot policy
and tests; and the Frontend bundle correction.
## Validation and limits
Focused offline source receipts cover frontend type-check, tests, lint
and build (`source-h1hs29xj`); affected Go packages (`source-0xmvnka7`,
`source-e1_2kz49`); Dependabot policy tests (`source-18_2qa8u`); and the
corrected final head's six targeted UI tests, type-check, Prettier,
production build and bundle-size gate (`source-_y63de_a`). The final
WorkloadsSurface measures 67.46 kB against the 70.23-kB cap, with an
8.09-kB on-demand detail chunk. Browser fixtures cover desktop and phone
collapsed/loading/SMART/History, no-agent and 503 states; earlier
combined SSO/Patrol browser fixtures also passed. `git diff --check` and
provenance checks passed.
The failed GitHub Frontend job was at the prior head, run `36279247432`,
job `108509096089`; its backend, E2E, governance and security checks
passed. Fresh exact-head hosted checks and independent review are
required before merge. This PR does not establish an installed fix,
reporter acceptance or release readiness. Frozen v6.4.5-rc.3 does not
contain this main-line batch.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
Keep linked guest physical-disk summaries in the initial Workloads bundle while loading SMART cards and history only when a disk opens. Show a loading status, preserve keyboard and pointer behaviour, and cover desktop and narrow browser states with a same-commit verification receipt.
Change-source: pulse-maintainer
The exact Pulse main proposal failed its required frontend formatting step. Apply the repository Prettier style to the reviewed Patrol attention surface without changing behavior.
Change-source: pulse-maintainer
Keep ESLint 10 and lucide-solid 1.x out of automatic version updates while their lockfile and icon-import contracts fail. Preserve minor and patch updates and guard the rule in Dependabot policy tests.
Change-source: pulse-maintainer
Keep the persisted empty-device alert ID distinct from the root-device ID while excluding incomplete disks from alias claims. Extend the unified-resources contract and identity tests in the same commit.
Change-source: pulse-maintainer
Keep persisted PVE disk alert identities stable while indexing them as exact physical-disk aliases. A muted disk now clears and suppresses its wearout alert without suppressing neighbouring disks. Update both subsystem contracts and add alert-identity, canonical-alias and registry-backed policy proofs.
Change-source: pulse-maintainer
The SSO mapping and Patrol changes were verified together in Chromium at desktop and phone widths. Bind the receipt to all three final frontend source blobs so candidate-wide preflight checks the actual composed source.
Change-source: pulse-maintainer
Retain the focused Patrol setup task while mounting the independent attention inbox when Patrol is off or model readiness is blocked. Reconcile the existing page test and cover responsive list, decision detail, return and remount states.
Refs #2257
Change-source: pulse-maintainer
Parse group-to-role entries by comma or newline, retaining spaces in IdP group names while leaving scopes and allowed-list parsing unchanged. Cover provider edit/save/reload in model, panel, architecture and desktop/narrow browser checks.
Refs #2266
Change-source: pulse-maintainer
A single healthy-looking disk poll could resolve a worn SSD alert, so the next
low reading re-fired its occurrence and notification even after the host-agent
SMART merge correction. Feed reported wearout through the canonical reducer
with three recovery confirmations, retain the failing value during a pending
recovery, and leave absent evidence unchanged. Repeated alternating polls now
keep one active occurrence and one dispatch; sustained recovery still clears it.
Refs #2112
Change-source: pulse-maintainer
Scope physical-disk reads to the opened guest, preserve agent RAID in the drawer, and prove the desktop and narrow interaction states with the performance contract updated in this same commit.
Change-source: pulse-maintainer
A zero-star GitHub fork, which still carries the old repository
description, has been outranking Pulse for Proxmox monitoring searches.
The README headline did not say what Pulse monitors, and search engines
weight the top of the README heavily.
This changes the headline from "Infrastructure monitoring that finds
what needs attention." to "Real-time Proxmox, Docker and Kubernetes
monitoring that finds what needs attention.". It keeps the existing
promise and leads with the platforms people search for. The repository
description was updated to match.
Documentation only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_015AHsrZFUgqmohpszCMpaPx
A zero-star fork with the old repository description outranked Pulse for
Proxmox monitoring searches. The README headline did not say what Pulse
monitors, and search engines weight the top of the README heavily. Keep the
existing promise and lead with the platforms people search for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AHsrZFUgqmohpszCMpaPx
The first #2260 benchmark run exposed eager alias indexing during
ingest.
The revised commit passed CI Benchmarks after index construction moved
to
alias lookup. Keep the owning subsystem contract aligned with that
result.
Refs #2199
The first #2260 benchmark run exposed eager alias indexing during ingest.
The revised commit passed CI Benchmarks after index construction moved to
alias lookup. Keep the owning subsystem contract aligned with that result.
Refs #2199
Carry changed resource IDs into the Workloads projection so periodic
deltas
reuse unchanged rows. Index canonical aliases after ingest and resolve
identity-only links without cloning resources. Remove repeated group
payloads
from windowed Storage rows, and keep fleet notices and Agent Doctor
usable at
narrow widths.
Record the browser proof and remaining payload and benchmark
observations in
the owning subsystem contracts.
Refs #2199
Carry changed resource IDs into the Workloads projection so periodic deltas
reuse unchanged rows. Index canonical aliases after ingest and resolve
identity-only links without cloning resources. Remove repeated group payloads
from windowed Storage rows, and keep fleet notices and Agent Doctor usable at
narrow widths.
Record the browser proof and remaining payload and benchmark observations in
the owning subsystem contracts.
Refs #2199
Suppressed alert writes previously returned without insertion while
callers continued adding history, recording firing events and requesting
notification delivery. Make acceptance explicit across detector and
restore paths. Repeated muted, retired and maintenance observations now
stop before those side effects, while expiry and resumed monitoring
admit the condition once.
Startup now reconciles restored alerts as soon as persisted resource
policy becomes available, refreshes shared alert state and persists that
resolution. Reconciliation also runs after resource publication, when
restored native aliases first resolve to their canonical agent policy.
Previously muted or retired resources could retain alerts restored
before either boundary.
Retained alert occurrences now reopen when the reducer explicitly
re-fires. Canonical history records occurrence identity separately from
transition time, preserving one incident after checkpoint loss. Delayed
initial firing, resolution replay and historical read repair cannot
override a newer transition. Older canonical records without the
occurrence key retain their existing matching behavior.
Shared table and card renderers preserve components retained across
scrolling windows. Previously a window shift rebuilt those items,
resetting the node Manage tab and losing unsaved lifecycle edits while
scrolling to Save. Mounted regressions cover retained input identity,
text, global position and the existing row budget. Maintenance banners
now show actual local start and end times instead of formatting future
timestamps as “just now”, and describe the paused attention correctly.
The synthetic host fixtures now keep stable machine identities across
graph rebuilds, so saved policies remain attached through restart
verification. The existing stability regression now checks machine IDs
per host, Docker agent IDs and unified canonical agent IDs. Production
conflict checks for a genuinely changed machine identity are unchanged.
Generated installation and rollback guidance now identifies the
Pulse-owned update helper and directs other installations to the
versioned signed installer flow. Both signed installer examples stop on
download or signature failure and run in a fresh temporary directory.
Validation:
- Suppression regression reproduced on the original source across 15
cases, then passed with the change. Full alert package tests pass.
Extended expiry, reconciliation and restore cases pass with the race
detector.
- Signed installer recipes fail six baseline cases and pass all eight
corrected cases. Seven existing installer pinning/signature/rollback
tests pass.
- Release renderer (39 tests), promotion resolver (47 tests),
release-promotion policy (57 tests), documentation mirrors and links,
and governance audits pass. Installer policy assertions cover helper
ownership, the signed fallback, exact version pinning, rollback
applicability and post-restart version verification. The complete
repository pre-commit hook passes on the worker against the matching
staged source, including repository tooling tests, release-control
helper tests and frontend lint. The global sensitivity check passes
locally.
- Startup restore and late native-alias resolution both fail on their
baselines. Final focused proof, race proof and the full monitoring
package pass.
- Retained-occurrence refire regression reproduced in both incident
projections. The complete memory suite passes on final source, full
monitoring and resource-store suites pass, and final focused
replay/refire proof passes under the race detector.
- Shared window regression failed for both table and card renderers on
the baseline, then all nine tests passed. Related component checks
passed 88 tests across seven files. Typecheck and canonical platform
audit pass.
- Maintenance rendering reproduces both incorrect future-time cases on
the baseline. Final operator-state tests pass 22 cases, and targeted
lint and typecheck pass.
- The fixture identity regression fails on the committed generator and
passes after deterministic IDs, together with four related host-link
tests and the Docker host fixture test.
- Production frontend and backend builds pass. Playwright inspection
covers Proxmox Manage and Alerts at 390x844 and 1440x900, 900px History
controls, and shipped install/upgrade docs. Saved maintenance and mute
survive a graceful same-data restart with stable canonical identity.
Cancelling maintenance preserves mute, resuming monitoring reopens the
retained incident with ordered fire/resolve/refire events.
- Authentication and credential baseline passes, including all 41
required API security regressions. All 11 selected Linux peer-identity,
command approval and observer-isolation regressions pass.
- The existing WebSocket connection-cap test now waits for actual per-IP
slot release before testing slot reuse. It still requires HTTP 429 at
capacity and successful registration after release. The corrected test
passes 100 runs under the race detector, and the full agent-execution
package passes with the race detector on the CI merge source.
The existing resource-payload performance gap remains open. The initial
50-node measurement recorded a 1,037 ms maximum desktop idle long task.
A final-source run still observed multi-second tasks, but its elevated
host load prevents a controlled comparison. Both observations and their
limits are retained. This change does not claim to resolve cross-client
payload or browser responsiveness work.
Refs #2237
Agent removal occurs inside read-loop cleanup before the handler releases
its per-IP connection slot. The flood test could observe the first state
and retry while the slot was still occupied, causing a spurious rejection.
Wait for the guarded admission count to reach zero before reconnecting,
while preserving rejection at capacity and successful registration after
release.
The advisory Benchmarks check failed four pull requests on
`NormalizeSegment` alone in the last 48 paired runs: #2217, #2225, #2252
and #2253. None of them touched the normaliser. Nine other runs moved
the same rows down by more than 10%. The values are quantized (EPYC
7763: `short_name` 2.50 or 3.12ns, `uuid` about 29.6 or 40.5ns). Go
aligns these functions to 32 bytes, so a change anywhere earlier in the
binary moves them between the halves of a 64-byte block.
This drops `BenchmarkNormalizeSegment`. Its guarantees move to
deterministic tests:
- Classification was already pinned by `TestNormalizeSegment`,
`TestLooksLikeUUID` and `TestIsNumeric`.
- The new `TestNormalizeSegment_DoesNotAllocate` pins zero allocations
for the five former inputs. It stores each result in a package-level
sink. Without the sink, an inlinable `normalizeSegment` could keep a
short-segment copy on the stack, and the test would pass.
Timing regressions still show in `BenchmarkNormalizeRoute`, because
every path runs its segments through `normalizeSegment`.
### Proof
These are paired runs of the CI runner's recipe on pulse-dev. The
package list is narrowed to `./internal/api/` and the sample count
raised to 30, because the shared host's load spread these rows by up to
25%.
| Run | `check-bench-regression.sh` | Flagged |
|---|---|---|
| `looksLikeUUID` scans twice | exit 1 | only
`NormalizeRoute/with_uuid`, +13.7% (p=0.005) |
| `normalizeSegment` does its work twice | exit 1 | only
`NormalizeRoute/with_uuid`, +21.9% (p=0.030) |
| candidate vs copy of itself | exit 0 | none |
| candidate vs itself + unrelated init-time function (32-byte shift) |
exit 0 | none (`with_uuid` +9.2%, under threshold) |
| old tree vs itself + same function | exit 1 | all five
`NormalizeSegment` rows, +25% to +40% (p=0.000) |
The deterministic test fails with one allocation per call when the
pass-through return uses `strings.Clone` or `string([]byte(seg))`.
The margin for a UUID-scan slowdown is thin. Doubling the scan reads as
roughly +11% to +14% on `with_uuid`. Layout alone has moved that row by
up to 9.2%, both on CI and in the probe above.
The release-promotion guard still expected the old installer-link wording,
so it rejected the corrected helper-ownership and version-pinning guidance.
Separate the installer safety assertions from support-pack coverage and
check the signed fallback, rollback applicability and version verification.
Rejected active-alert writes still reached history, firing events and
notification dispatch. Make admission explicit across detector and restore
paths so repeated suppressed observations cannot create those side effects.
Reconcile restored alerts when persisted resource policy attaches at startup,
reconcile again after native aliases become canonical, and refresh shared
state before those alerts reach clients.
Preserve occurrence identity when retained alerts re-fire, including durable
history reconstruction and delayed lifecycle replay.
Keep retained table and card components mounted when scrolling shifts the
visible window, so policy editors do not lose unsaved state. Show actual
future maintenance times and the paused-attention effect.
Keep synthetic host machine identities stable across restarts so saved
operator policy remains attached during verification.
Qualify server update and rollback instructions by helper ownership, and
stop signed installer recipes before execution if a download or signature
check fails.
Refs #2237
The advisory Benchmarks check failed four pull requests on
NormalizeSegment alone in the last 48 paired runs: #2217, #2225, #2252
and #2253. None of them touched the normaliser. On #2252 all five
sub-benchmarks rose 25-39% together. Nine other runs moved the rows
down by more than 10% instead, -20% to -28% on #2228, #2230 and #2242.
Those 13 runs used EPYC 7763, EPYC 9V74 and Intel Xeon runners.
The values are quantized. On EPYC 7763 short_name is 2.50ns or 3.12ns
and uuid is about 29.6ns or 40.5ns, a cycle or two per call, or one
per byte of the UUID scan. Go aligns these functions to 32 bytes, so a
change anywhere earlier in the binary can move them between the halves
of a 64-byte block. A 32-byte shift is enough: on pulse-dev, adding an
unrelated init-time function ahead of the normaliser flipped that
parity and failed the old tree the way #2252 did, all five rows +25% to
+40% at p=0.000 and nothing else flagged. More samples cannot separate
that from a real change, and batching does not help, because every
call repeats the same instructions.
The route-level rows do not behave this way. Over the same 48 runs no
NormalizeRoute row except the root case retired in #2255 moved by more
than 10%. with_uuid came closest at 9.2%.
The guarantees the benchmark carried now live in deterministic tests.
TestNormalizeSegment, TestLooksLikeUUID and TestIsNumeric already pin
classification. TestNormalizeSegment_DoesNotAllocate pins zero
allocations for the five former inputs, and fails with one allocation
per call when the pass-through return copies the segment. It stores
each result in a package-level sink: when normalizeSegment was cut
down to an inlinable copy, a discarded result let the compiler keep
short segments on the stack, and the test missed the allocation.
Timing regressions still show in BenchmarkNormalizeRoute, since every
path runs its segments through normalizeSegment. The proofs below are
paired runs of the CI runner's recipe on pulse-dev, narrowed to
./internal/api/ and raised to 30 samples, because the shared host's
load spread these rows by up to 25%, against about 1% on CI runners.
- looksLikeUUID scanning twice: check-bench-regression.sh exited 1,
flagging only NormalizeRoute/with_uuid, +13.7% (p=0.005).
- normalizeSegment doing its work twice: exited 1, flagging only
NormalizeRoute/with_uuid, +21.9% (p=0.030).
- the candidate against a copy of itself: exited 0.
- the candidate against itself plus the same init-time function:
exited 0. The route rows still moved 4-5%, and with_uuid 9.2%
(p=0.000).
The margin for a UUID-scan slowdown is thin. On EPYC 7763 the scan is
about 30ns of a 280ns with_uuid call, so doubling it reads as roughly
+11% to +14%, while layout alone has moved with_uuid by up to 9.2% both
on CI and in the probe above. Giving UUIDs a larger share of a route
would make that row flip the way this one did; a smaller share would
hide the slowdown.
## What changed
Make provider Plan checkout and billing returns report an active paid
plan only after the running control plane serves the matching refreshed
licence. Handle already-applied, trailing, and cancelled billing returns
without falsely claiming a charge or losing a valid plan confirmation.
The portal tests cover these return states.
## Why
A checkout return alone is not proof that the provider's refreshed
licence is active. The Plan page must tell providers what is actually
running, particularly when a payment callback is delayed, repeated, or
cancelled. This is the reviewed provider Plan return correction and its
follow-up verification.
## Included source
`Pulse` main after `ab0ab381a0f1b4e1ffbcc38537667870dec46960` through
`2dd856e9aa7b155032f102bb96fa59e08f582599`. The reviewed local tip
`cb26fb96ec002a40c52b618a6247fd931ff64b4a` remains an ancestor. Its
provider Plan return changes include `d1ea9ce0`, `8a59a8df`, `ac721a26`,
`a631fc80` and their review/integration merges. Commit `2dd856e9`
additively merges current upstream and regenerates the portal
bundle/manifest from the combined source. No previously reviewed commit
was rewritten.
## Validation and limits
The reviewed Plan candidate passed an offline exact-lock VM check: npm
installation, 25 focused Plan tests, typecheck, all 129 portal frontend
tests, build, and generated-file byte comparison. Its Go portal test
passed in a separate exact-source VM, and earlier offline browser
inspection covered its unchanged Plan paths. For this upstream merge,
the publisher verified a clean worktree, no diff whitespace errors, and
that the committed manifest hash matches every combined portal build
input. The exact post-merge source-proof VM was busy on two attempts; no
post-merge VM pass is claimed. Required hosted checks and exact-head
landing validation must complete before merge. This source proposal is
not live payment acceptance, deployment, or release readiness.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
Preserve the reviewed provider Plan return history and incorporate current upstream changes. Rebuild the portal bundle and manifest from the combined source.
Change-source: pulse-maintainer
The advisory Benchmarks check keeps failing unrelated pull requests on
`NormalizeRoute/root`: alone on #2228, #2242, #2245 and #2250 (and PR
1973 on 2026-09-08), and beside other results on #2230 and #2244.
**Why it isn't a signal.** The `"/"` path is a length compare and a
constant return. In the last 19 paired PR runs, the 11 on AMD EPYC 7763
measured it at exactly 2.18 ns or 2.50 ns on both sides, each within
0–1%. That is about one CPU cycle. In five of those runs, base and
candidate landed on different values: +14% four times and −13% once
(#2252), though none touched the normaliser. A stable one-cycle shift
passes p<0.05 every time, so more samples can't filter it. A looser
threshold would blunt the gate for every other benchmark.
**Why batching doesn't fix it.** On an amd64 host, a 64-call batch
spread as widely in relative terms as a single call, because every call
repeats the same few instructions.
**Change.**
- Drop the `root` sub-benchmark.
- Add `TestNormalizeRoute_RootFastPathDoesNotAllocate`, which pins the
property that matters: `"/"` and `""` return before splitting. With the
fast path removed, `TestNormalizeRoute` still passes, but the new test
fails with one allocation per call. It also passes under `-race`.
**Real regressions stay detectable.** I ran a paired comparison through
`scripts/run-ci-benchmarks.sh` (10 alternating samples, 100 ms, scoped
to the HTTP metrics benchmarks), with `normalizeRoute` made to do its
work twice:
| Benchmark | Result |
|---|---|
| `NormalizeRoute/*` (6 sub-benchmarks) | +89% to +107%, p=0.000 |
| `NormalizeRoute_Parallel` | +87% |
| `FullMiddlewarePath` | +54% |
| B/op and allocs/op | +100% |
`check-bench-regression.sh` exited 1. The same tree compared with itself
exited 0 with no changes.
**Not in this change.** `NormalizeSegment` shows the same layout flip.
It failed #2252 on its own (+25% to +39%), and in the paired run above
it moved −20% to −38% even though its code was untouched. Its uuid flip
is about 10 ns, so it needs its own fix rather than a small ns floor.
The advisory Benchmarks check kept failing unrelated pull requests on
NormalizeRoute/root alone (#2228, #2242, #2245, #2250, and PR 1973 on
2026-09-08), and flagged it beside other results on #2230 and #2244.
The "/" path is a length compare and a constant return.
In the last 19 paired PR runs, the 11 on AMD EPYC 7763 measured root
at exactly 2.18ns or 2.50ns on both sides, each within 0-1%. The two
values are about one CPU cycle apart. In five of those runs base and
candidate landed on different values, four times up (+14%) and once
down (-13%), though none touched the normaliser. The earlier
investigation found the benchmark loop identical and the normaliser
differing only in relocated addresses. More samples cannot separate a
stable one-cycle shift from a real change, and a looser threshold
would blunt the gate for everything else.
Batching root calls does not escape it. On an amd64 host a 64-call
batch spread as widely in relative terms as a single call, because
every call repeats the same few instructions.
What the path must guarantee is that "/" and "" return before
splitting. TestNormalizeRoute_RootFastPathDoesNotAllocate pins that
deterministically: with the fast path removed, TestNormalizeRoute
still passes, but the new test fails with one allocation per call.
Real regressions stay visible. A paired run using the CI runner's own
alternating recipe (10 samples, 100ms), with normalizeRoute made to do
its work twice, flagged every remaining NormalizeRoute sub-benchmark
at +89% to +107%, FullMiddlewarePath at +54%, and bytes and allocs at
+100%. check-bench-regression.sh exited 1. The same tree compared with
itself exited 0.
A provider-hosted control plane refused to start once its licence was
past
expiry and the 7-day grace, because startup validated it with
ValidateLicense. So at the next restart after a 60-day evaluation ran
out,
or after a paying provider's subscription lapsed, the portal went down,
and
the portal's Plan tab is the only place a provider can buy or renew.
Reproduced on the walkthrough lab.
Startup now accepts an authentic, key-bound licence even when lapsed
(ValidateLicenseAllowingLapse), preferring a current licence and
otherwise
the one that expired later, and logs the lapse. It unlocks nothing:
client
runtimes still verify the provider licence in every lease with
ValidateLicense and drop MSP capabilities, new clients are refused with
provider_msp_license_lapsed, and the refresher still refuses a lapsed
licence. The Plan panel says the evaluation or plan ended and offers the
plans, and provider-msp status reports license_lapsed without failing so
a
lapsed install can still upgrade.
The same run showed the portal printing control plane error codes such
as
provider_msp_license_lapsed instead of their message. The portal now
prefers an error's message over its code, which also fixes
already_subscribed and checkout_unavailable in the Plan tab.
Replaces #2241 and #2250, replayed onto current main as one commit.
Every accepted agent report refreshed the canonical resource store, and
every refresh covers the whole estate, so refresh work grew with agent
count × estate size (#2199). #2235 made each pass linear and cheaper.
This PR bounds how many passes reports can trigger.
**Behaviour.** Accepted host, Docker and Kubernetes reports, including
rejected-report liveness, refresh through
`refreshUnifiedResourceStoreAfterAgentReport`:
- The first report after a quiet window is published before its handler
returns, the same as today.
- Reports within 2 s of the previous refresh fold into one trailing
refresh at the window's end. It reads the latest state and broadcasts it
to websocket clients.
- 2 s matches `readPathRegistryFreshness`, the staleness the read paths
already accept.
- Agent removal and host-agent evaluation still refresh immediately, so
removed inventory retires at once.
- `Monitor.Stop` cancels a pending trailing refresh and waits for a
running one before closing stores.
- Monitors built without `New` keep a zero window and refresh on every
report.
**Cost bound.** At 2,080 resources one refresh costs about 82 ms (after
#2235). 200 agents reporting every 30 s drive 6.7 refreshes/s, roughly
55% of a core; coalesced, reports trigger at most one refresh per 2 s,
about 4%, however many agents report.
**Contract change.** The agent-lifecycle contract promised that an
accepted report is visible when its handler returns. It is now visible
within the 2 s window. The founder approved that trade on 24 September
2026, and the contract records it.
**Tests**
- `TestAgentReportRefreshFoldsReportsWithinWindow`: the leading report
is published synchronously; a burst inside the window is deferred; one
trailing refresh publishes the burst's latest value; after a quiet
window the next report is synchronous again. It fails when the throttle
is removed.
- `TestStopCancelsPendingAgentReportRefresh`: fails if stop does not
cancel the pending refresh.
- The issue-1753 end-to-end test posts two reports back to back, so it
now waits for the published rows instead of reading them at once.
**Verification on pulse-dev**
- `internal/monitoring` runs in 112 s against 107 s on #2235's head. No
test is slower by more than 1.2 s.
- `internal/unifiedresources`, `internal/websocket` and
`internal/api/...` pass. `TestServerInfoEndpointReportsDevelopment`
fails in that source export with and without this change, because the
export has no git checkout.
Refs #2199
Follow-up to #2234. Patrol stopped calling
`ChangeDetector.DetectChanges` when it moved to agentic execution
(1b1c9bb2a3). Recent-change context comes from the unified-resource
timeline, and the detector survives only as a fallback reader of the
legacy `ai_changes.json` that older releases wrote. #2234 removed the
detector's unjoined save. The detection code itself was still there, and
only tests used it.
## Removed
- `DetectChanges` and `detectResourceChanges`, which diffed snapshots.
- `ResourceSnapshot` and its `internal/ai` alias.
- Change-ID generation, and the seven description formatters with their
`formatBytes`/`formatFloat` helpers.
- The detector's lock. The history is loaded once at construction and
never modified, so reads need no lock.
- Tests of the detection logic, its heap stability
(`change_detector_memory_regression_test.go`) and ID uniqueness
(`changes_concurrency_test.go`).
## Unchanged
- `NewChangeDetector`, legacy loading and trimming.
- The read APIs: `GetChangesForResource`, `GetRecentChanges` and
`GetChangesSummary`.
- The `ChangeType` constants, which label the legacy entries.
- `intToString`, which the incident and remediation code share.
## Tests
The `internal/ai` tests that used detection to seed the fallback now
write a legacy `ai_changes.json` and load it. That is the path
production reads.
To check that the new seeding really exercises the load path, I broke
legacy loading through an overlay. 10 of the 12 re-seeded tests then
fail. The other two assert that the fallback is left out of a
type-scoped patrol, or that it loses to the canonical timeline, so empty
history also satisfies them.
That run also exposed three weak assertions, now tightened:
- Two formatter comparisons passed against an empty detector because `""
== ""`. They now reject empty output.
- One summary test used `t.Error` and then indexed `[0]`. It now uses
`t.Fatal`.
The ai-runtime contract now describes the detector as a legacy-history
reader.