Preserve the exact reviewed Patrol guidance and VM Discovery safety commits while incorporating upstream commits captured before coordination. No source conflict resolution or product changes are added by this integration.
Change-source: pulse-maintainer
## Problem
244d322dce, merged at 15:01:08 UTC today in #2518, made organization
deletion refuse with `503 tenant_shutdown_incomplete`, and keep the
data, when the tenant's monitoring loop hasn't exited. That guard is
right, but it reused `tenantMonitorShutdownTimeout`. That 2-second
budget dates from March, when it was only a best-effort wait before
logging a warning and carrying on.
A loop that is still starting up or finishing a tick can take longer
than that to see its cancelled context. The observed case is mock mode,
which CI uses: `Monitor.Start` runs `startMockMetricsSampler`
synchronously before its select loop, and `prepareMockMetricsHistory`
seeds seven days of fixture history without a context. The loop only
sees cancellation once seeding finishes; "Mock metrics history sampler
started" and "monitoring loop stopped" log in the same second. E2E run
[37329726585](https://github.com/rcourtman/Pulse/actions/runs/37329726585/job/111833069435),
shard 7, shows it:
| Time (UTC) | Event |
|---|---|
| 15:27:57 | Organization A's monitor initializes |
| 15:28:00 | Delete requested |
| 15:28:02 | `timed out waiting for tenant monitor loop to exit` → 503 |
| 15:28:10 | `monitoring loop stopped` |
| 15:28:20 → 15:28:29 | The retry, with a new organization, repeats the
same sequence |
So `13-api-token-scope` "org-bound token stays inside the issuing org"
failed its org cleanup on both attempts. Run 37331626618 failed the same
way: the first Core E2E failure came two minutes after #2518 merged. A
real delete shortly after a tenant was busy gets the same refusal, with
its alarming "data has been retained" message, until someone retries.
## Change
- **New budget:** `removeTenant` waits on
`tenantDeletionShutdownTimeout` (30s) when deleting. Process `Stop` and
`RemoveTenant` keep the 2-second `tenantMonitorShutdownTimeout`.
- **Still refused:** a loop that never exits, and a failed metadata
close, both still refuse and retain the data. The guard's semantics
don't change.
- **Tests:** both budgets are now variables so tests can shorten them.
`TestTenantDeletionRetainsUnfinishedMonitorLoop` uses a short deletion
budget instead of the real 30 seconds.
- **No timeout conflicts:** the server's `WriteTimeout` is disabled, the
frontend's org delete has no request timeout, and Playwright's request
timeout is 30s.
## Verification
- **New test:** `TestTenantDeletionWaitsForSlowMonitoringLoop` (in the
registered `monitor_additional_test.go`) uses a loop that exits after
300ms, with a 50ms plain budget and a 2s deletion budget. Plain removal
gives up and retains the monitor; deletion waits and completes. Giving
deletion the plain budget makes it fail, so it checks the behaviour
itself.
- **Existing tests:** the deletion safety tests in `internal/monitoring`
(`TestTenantDeletion*`, `TestMultiTenantMonitor*`) and the org-delete
handler tests in `internal/api` all pass, including
`TestOrgDeleteRetainsDataWhenGuestMetadataPersistenceFails`.
**Follow-up in a separate PR:** another session is making the mock
history seeding honour cancellation
(`internal/monitoring/mock_metrics_history.go`), so CI deletions finish
almost at once instead of waiting out the seeding. This PR keeps a
realistic deletion budget either way.
In mock mode, a tenant monitor's loop only sees its cancelled context
after `startMockMetricsSampler` returns. Seeding the 50-node demo estate
takes 4-11s on CI runners. Organization deletion cancels the loop and
waits for it, so every deletion of a just-used organization was held for
the whole seed. Under the 2s budget from #2518 it was refused with
`tenant_shutdown_incomplete`, which is the intermittent Core E2E failure
in `13-api-token-scope.spec.ts:129` (runs 37329726585 and 37331626618).
In the server logs, "monitoring loop stopped" lands in the same second
as "Mock metrics history sampler started", 5-10s after the deletion's
cancel.
- `seedMockMetricsHistory` checks the context before each resource and
returns `ctx.Err()` once cancelled. Series already sampled are recorded
whole.
- `prepareMockMetricsHistory` never stores a cancelled seed as the
shared template, and returns at once if its monitor was cancelled while
it waited on the template lock.
- `startMockMetricsSampler` skips the chart-cache warm-up and the
sampler goroutine when seeding was cancelled.
Measured on the full demo estate: cancel-to-return went from the whole
seed (about 2.7s on an M-series Mac) to 0.6ms.
#2529 separately gives deletion a longer bounded wait for any slow loop.
This PR removes the mock-mode cause of the wait. They touch different
files and land independently.
New tests in `mock_metrics_history_cancel_test.go` use a context that
cancels after a set number of checks, so they don't depend on timing.
All three fail with the cancellation checks removed.
Preserve the exact documentation and regression-test candidate; native browser follow-through and the missing UI remain owned separately.
Change-source: pulse-maintainer
TestPatrolAutopilotStoreUnavailableDoesNotChangeModeOrFabricateEvidence
built its request through patrolAutopilotSessionRequest, which writes a
session into the process-global store, but never initialized one. Run
alone it panicked in GetSessionStore; in the package run it passed only
because an earlier test left a store behind.
The rest of the file got its store from newPatrolAutopilotTestHandler,
which called InitSessionStore on a t.TempDir() and never cleared it, so
these tests were also the ones leaving a store pointed at a deleted
directory for whatever ran next.
Both now go through initPatrolAutopilotSessionStore, which follows the
package's existing resetSessionStoreForTests/t.Cleanup pattern: start
from no store, initialize one on a temp dir, and clear it when the test
ends. Every test in the file passes when run alone.
A tenant monitor's loop only sees its cancelled context after
startMockMetricsSampler returns, and seeding the 50-node demo estate
took 4-11s on CI runners (about 2.7s on an M-series Mac). Organization
deletion cancels the loop and waits for it, so in mock mode every
deletion of a just-used organization was held for the whole seed, and
under the 2s budget #2518 introduced it was refused with
tenant_shutdown_incomplete (Core E2E 13-api-token-scope since 15:03 UTC
on 2026-10-05).
Seeding now checks the context before each resource and returns
ctx.Err() once cancelled, so the loop exits within a millisecond of the
cancel. Series already sampled are recorded whole. A cancelled seed is
never stored as the shared template other tenants reuse, and a monitor
cancelled while it waited on the template lock returns without seeding.
startMockMetricsSampler then skips the chart-cache warm-up and the
sampler goroutine.
#2529 separately gives deletion a longer bounded wait for any slow
loop; this removes the mock-mode cause of the wait.
Contract-Neutral: Mock-mode seeding stops early on monitor cancellation; no route, field, schema, persistence format or provider behaviour changes.
The deletion-budget note and its test said only that a loop still
starting up can be slow to stop. The CI failures have a specific
cause: in mock mode Monitor.Start runs startMockMetricsSampler
synchronously before its select loop, and prepareMockMetricsHistory
seeds seven days of fixture history without a context, so the loop
saw its cancelled context only once seeding finished ("Mock metrics
history sampler started" and "monitoring loop stopped" log in the same
second in runs 37329726585 and 37331626618). Say so in the contract
and the test comment.
TestPatrolAutopilotStoreUnavailableDoesNotChangeModeOrFabricateEvidence
built its request through patrolAutopilotSessionRequest, which writes a
session into the process-global store, but never initialized one. Run
alone it panicked in GetSessionStore; in the package run it passed only
because an earlier test left a store behind.
The rest of the file got its store from newPatrolAutopilotTestHandler,
which called InitSessionStore on a t.TempDir() and never cleared it, so
these tests were also the ones leaving a store pointed at a deleted
directory for whatever ran next.
Both now go through initPatrolAutopilotSessionStore, which follows the
package's existing resetSessionStoreForTests/t.Cleanup pattern: start
from no store, initialize one on a temp dir, and clear it when the test
ends. Every test in the file passes when run alone.
Serve a harmless 404 for browser favicon requests and bind synthetic session requests to their recorded user agent. Retain the earlier browser setup failure.
Contract-Neutral: Synthetic proof-fixture preparation only; no runtime or rule-removal instructions changed.
Change-source: pulse-maintainer
244d322dce made organization deletion refuse, and keep the data, when
the tenant's monitoring loop has not exited. That is the right guard,
but it reused tenantMonitorShutdownTimeout, a two-second budget that
had only ever been a best-effort wait before logging and carrying on.
A loop that is still starting up or finishing a tick can take longer
to see its cancelled context. In E2E run 37329726585 a just-used
organization's loop took 8-10 seconds, so both attempts of
13-api-token-scope's org cleanup got 503 tenant_shutdown_incomplete.
Any real delete shortly after a tenant was busy hits the same refusal
until a retry.
Deletion now waits on its own tenantDeletionShutdownTimeout (30s)
before it refuses. Process shutdown and RemoveTenant keep the
two-second budget, and a loop that never exits still refuses and
retains the data. Both budgets are variables so tests can shorten
them; the never-stopping-loop test no longer waits on the real budget.
Explain the missing v6.4.5 undo control and the existing scoped API. Verify exact selection, confirmation, browser session and CSRF, failure handling and readback without copying credentials or deleting unrelated rules.
Contract-Neutral: Documentation and regression-proof coverage of existing Patrol suppression APIs only; no runtime, route, authority or subsystem contract changes.
Change-source: pulse-maintainer
## Problem
An agent installed by the cluster deploy flow starts with `--enroll` and
a one-time bootstrap token. Only the host module exchanged that token
for the runtime token. Everything else `main.go` builds at startup kept
the bootstrap token for the whole first process lifetime:
- the remote config client (initial fetch and the one-minute refresh)
- the updater
- the Docker / Podman and Kubernetes modules and their primary report
targets
The bootstrap token carries only the enrollment scope and the server
removes it on exchange, so each of those failed with 401 until the
service restarted. On a freshly deployed Proxmox node this showed up as
a remote config 401 every minute. Those modules also kept the
pre-enrollment agent ID instead of the server-assigned one.
## Fix
`main.go` now enrolls once, before it builds any authenticated client:
- `hostagent.Enroll` is a standalone entry point over the existing
enrollment loop. Payload, headers, retry and backoff, status handling,
and `runtime.token` / `agent-id` persistence are unchanged.
- It runs after self-test and agent-ID preparation and before the first
remote config fetch. Every module is then built from the runtime token
and the canonical agent ID.
- The ID is persisted to the configured agent-ID file, and the host
module's own enrollment is switched off so there is no second exchange.
- A persisted runtime token is reused without a request or host
discovery. A `403` keeps the configured token as a non-bootstrap
credential. A permanent rejection stops startup before any module is
built.
- Windows is gated off and keeps its previous behaviour. Startup there
runs before the service control dispatcher, and an enrollment retry
could outlast its 30-second deadline. No Windows installer passes
`--enroll`.
- Enrollment and reporting now derive hostname, OS and architecture
through one `resolveHostIdentity`, so the runtime token is bound to the
hostname the agent reports under.
## Tests
- `TestRunEnrollsBeforeBuildingAuthenticatedModules`: enrollment happens
before any request to Pulse. Remote config, the updater, and the host,
Docker and Kubernetes configs and their targets all carry the runtime
token and canonical ID, and the agent-ID file is written. With the new
step disabled, this test fails.
- `TestRunStopsBeforeModulesWhenEnrollmentIsRejected`,
`TestRunWithoutEnrollDoesNotExchangeTokens` and
`TestRunLeavesEnrollmentToTheHostModuleWhenGatedOff`.
- `TestEnroll*` cases for `Enroll`: payload and identity, the
monitoring-only command ceiling, reuse of a persisted token (including
when host discovery fails), the 403 keep, the 409 rejection, and a
missing token.
- `TestEnrollPresentsTheIdentityNewReports` checks that enrollment
presents the hostname, OS and architecture that `New` reports, with
Proxmox detection and the architecture fallback.
- `go test ./cmd/pulse-agent ./internal/hostagent
./internal/remoteconfig` passes. `go vet` is clean for linux, windows
and freebsd.
## Not in this change
- The health listener still starts after enrollment, so `/readyz` is
unreachable while enrollment retries on network errors. The installer
only warns in that case. With the health listener enabled, its
registration check now always sees the runtime token. With it disabled,
the process-only check can still race enrollment.
- Distinguishing a deploy binding mismatch from a non-bootstrap token,
which both return 403.
- Two older durability gaps that this change doesn't make worse. If
writing a custom `--agent-id-file` fails, the next start has no fallback
to the canonical ID saved in the state directory. If persisting
`runtime.token` fails, enrollment still succeeds, and the next restart
is left with the spent bootstrap token.
Adding the Pulse Agent to a Proxmox cluster member meant generating a
token in the UI and pasting it into an interactive prompt on the host,
even though Pulse already knows the cluster and has an agent on its
siblings. The server-side cluster deploy that avoids this was still
shipped, but its wizard was deleted as an orphan in May, and it could
not reach members discovered by hostname: targets were parsed from the
API URL, so a name like "delly2" hit the agent's literal-IP guard and
failed preflight.
Deploy targets now resolve to a literal IP in the member's API URL or
its cluster endpoint's effective IP, matched on the member's own
connection, and members with no known address come back undeployable
with a reason. The Settings > Infrastructure install actions open one
dialog that checks, installs, and confirms each node is reporting,
falling back to the scoped installer when no sibling agent is
connected or a node cannot be reached.
The deploy routes are keyed on the cluster name, so they now refuse a
name shared by two connections instead of merging two sites, and job
creation admits one install per cluster at a time under a lock, which
lets the dialog keep a run going after it is closed.
The agent-lifecycle, api-contracts, frontend-primitives, and
storage-recovery contracts record the new install path, its address
rule, and the persisted, time-bounded admission check.
Supersedes #2507, which split this change across two commits; the
per-commit canonical completion guard needs the contract updates in the
same commit.
A deployed agent's first run gave the remote config client, the updater, and
the Docker / Podman and Kubernetes modules the bootstrap token, which only the
host module exchanged. Pulse revokes that token on exchange, so those clients
failed with 401 until the service restarted. Enroll once at startup and build
every module from the runtime token and the server-assigned agent ID.
The converter-only source assertion update also matched an identical infrastructure-consumer snippet. Restore that unaffected by-value assertion; only the broadcast converter borrows its local resource. Preserve both independent canonical-contract checks.
Contract-Neutral: Verification-only correction; existing infrastructure and broadcast canonical helper checks remain enforced and runtime source is unchanged.
Change-source: pulse-maintainer
The connected suite retained a shared-contract source assertion expecting by-value helper calls. The converter now borrows its local resource and dereferences at the same shared canonical type/name/cluster APIs. Match those three exact expressions without removing a shared-helper assertion or changing runtime behavior.
Contract-Neutral: Source-structure verification only; the same canonical shared helper, REST/state contract and all runtime payload and freshness behavior remain required.
Change-source: pulse-maintainer
Current-source profiling shows repeated alias derivation reserving maps for blank and duplicate optional inputs. Scan at most eight normalized keys on the stack, then fall back to a map for larger vocabularies. Preserve exact lowercasing, first spelling, order and detached allocated-empty output with the independent original map oracle, including Unicode and large identities.
Contract-Neutral: Allocation strategy only; canonical aliases, superseded IDs, identity matching, tenant/source ownership, registry invalidation, History and broadcast wire contracts are unchanged.
Change-source: pulse-maintainer
Review of the nil fast path identified named nil collections that implement encoding.TextMarshaler rather than json.Marshaler. Keep the parent representation for both interfaces and retain matching byte-level controls.
Contract-Neutral: Conversion work only; retain the exact existing JSON and text-marshaler representations and all broadcast fields and freshness boundaries.
Change-source: pulse-maintainer
Borrow the converter-owned resource across field helpers and bypass encoding typed absent facets as discarded null. Preserve named nil collection marshalers, errors, detached output, all payload fields, metadata refresh, catalogues and same-generation History targets. Validate complete projections and encoded frames against the exact parent.
Contract-Neutral: Conversion work only; broadcast and REST payloads, identities, tenant routing, metadata and health freshness, alerts, catalogue references and History coordinates are unchanged.
Change-source: pulse-maintainer
Capture all endpoint limiters under their Router owner and seal late worker
admission. Concurrent shutdown joins each cleanup worker without resetting
request budgets or stopping another Router. Integration fixtures use the real
shutdown lifecycle before monitor teardown.
Contract-Neutral: Router worker ownership and concurrent shutdown only; HTTP payloads, limits and windows, auth, tenant selection, revocation and persistence contracts are unchanged.
Change-source: pulse-maintainer
The five predeclared diagnostic cases retain a real 1m versus 2m health-age counterexample between separate wall-clock evaluations. Give the independent content oracle and production projection the same private health clock without removing any JSON field; pin minute-boundary age advancement and original LastSeen. Production samples real UTC at the unchanged post-read evaluation point.
Contract-Neutral: Internal health-clock test seam preserves the real UTC evaluation point, public resource/history fields, source precedence, agent identity/lifecycle, tenant/revocation and safety contracts; no wire or permission delta.
Change-source: pulse-maintainer
The new complete-content oracle exposed a typed-view discrepancy. Keep that failed result and report the first differing JSON field, rather than accept an undifferentiated mismatch or normalise away evidence. Only the synthetic fixture failure message changes; equality assertions and production source are unchanged.
Change-source: pulse-maintainer
Adding the Pulse Agent to a Proxmox cluster member meant generating a
token in the UI and pasting it into an interactive prompt on the host,
even though Pulse already knows the cluster and has an agent on its
siblings. The server-side cluster deploy that avoids this was still
shipped, but its wizard was deleted as an orphan in May, and it could
not reach members discovered by hostname: targets were parsed from the
API URL, so a name like "delly2" hit the agent's literal-IP guard and
failed preflight.
Deploy targets now resolve to a literal IP in the member's API URL or
its cluster endpoint's effective IP, matched on the member's own
connection, and members with no known address come back undeployable
with a reason. The Settings > Infrastructure install actions open one
dialog that checks, installs, and confirms each node is reporting,
falling back to the scoped installer when no sibling agent is
connected or a node cannot be reached.
The deploy routes are keyed on the cluster name, so they now refuse a
name shared by two connections instead of merging two sites, and job
creation admits one install per cluster at a time under a lock, which
lets the dialog keep a run going after it is closed.
The agent-lifecycle, api-contracts, frontend-primitives, and
storage-recovery contracts record the new install path, its address
rule, and the persisted, time-bounded admission check.
The full current-state regression now requires a critical alert to reach the same freshly projected row. Continuity controls compare a healthy VM, critical transition and clear instead of using an already-offline host as a vacuous health control.
Change-source: pulse-maintainer
The independent full-state oracle retains the parent unified view watermark. The paired benchmark preprocesses active health alerts once per pass, like the exact parent, rather than adding per-row work to the baseline. Production projection and source fields are unchanged.
Change-source: pulse-maintainer
Resolve canonical source mappings once per complete projection while preserving detached resource content and target selection after host coalescing. Bind resources and targets to the same locked generation, including absence and continuity overlays, and avoid another owned outer-slice copy. No timestamp cache or native CPU/RSS claim. Retain cold, clean and dirty full-content controls and paired complete-projection measurements.
Contract-Neutral: Internal bulk-read and copy-ownership repair preserves existing public resource/history fields, source precedence, agent identity/lifecycle, tenant/revocation and safety contracts; no wire or permission delta.
Change-source: pulse-maintainer
The comment in AddClient said a broadcast racing a connect is either in
the snapshot or queued behind it. It can be both, so the client then
receives that status twice, possibly with an older in-flight stage in
between. Trusting the "either" is what led the connect-snapshot test to
assume nothing writes the recorder after AddClient returns, which made
it flaky under -race. Say what actually happens and why it is harmless:
stage progress only rises and the update modal ignores backwards stages.
Contract-Neutral: Comment-only correction in internal/updates/sse.go; no runtime or contract change.
TestSSEBroadcaster_AddClientWritesCurrentStatusBeforeReturning failed
under -race in CI on unrelated frontend PRs (2479, 2485). The test
broadcast a stage and connected straight away. When the broadcast loop
picked that message up after the client had registered, it queued the
stage for the new client too, so the client's writer goroutine wrote
the httptest recorder while the test read it without a lock.
AddClient itself is correct: it writes the preamble and current status
before returning, and a stage racing the connect can reach the client
twice, which the update modal already ignores. The test now lets the
broadcast reach an earlier client before connecting, so nothing else
writes the recorder and the status in the body can only come from the
synchronous snapshot write.
Reproduced by injecting the CI schedule through -overlay (broadcast
loop held until the client registers, caller paused as AddClient
returns): the old test failed 199/200 runs with DATA RACE, the new one
0/200. A mutant that moves the snapshot write onto the writer goroutine
still fails the new test 100/100.
Keep admission sealed and reuse a still-running close observer. A later explicit shutdown attempt may persist retained state after a completed write failure; successful close remains idempotent. This avoids a permanent offboarding dead end after an ordinary filesystem repair without an automatic spin retry or overlapping writes.
Contract-Neutral: Internal JSON writer recovery behaviour only; public routes, fields, permissions, identity format, private file mode and atomic persistence are unchanged.
Change-source: pulse-maintainer
Admit detached metadata in order, merge identity fields without restoring stale
operator edits, and persist complete snapshots with one owned writer. Seal late
writes and return shutdown failures before organisation data or offboarding is
removed. Preserve existing drain budgets, file format, tenant boundaries and
atomic persistence. Add controlled burst, ordering, write-error, blocked-close
and real deletion controls.
Contract-Neutral: Behavioural repair of existing JSON persistence and organisation deletion; no route, field, permission, identity format, schema or provider authority change. Shutdown failure uses the existing HTTP error envelope. Internal ownership methods and explanatory contract text change, not the public contract shape.
Change-source: pulse-maintainer
Preserve the reviewed candidate commits as the second parent and retain canonical one-shot, redirect, partial-response and disclosure controls in the overlapping API/PBS smoke fixtures. Keep the candidate's canonical PBS guard, scoped private temperature-file assertions and unchanged-verdict audit diagnostics.
Contract-Neutral: Integration of test-only private-help adapters; no runtime, guides, dependencies or frontend source changed.
Change-source: pulse-maintainer
Retain the three-sample baseline-plus-ten bound in an isolated copy of the same test binary. Add full-publication copying controls and exact complete admission-content parity, rather than attributing independent registry allocation variation to ignored metadata.
Contract-Neutral: Test measurement correction only; no runtime, identity, freshness, publication or allocation-limit contract changes.
Change-source: pulse-maintainer
Preserve exact Core candidate ab15c4db8a and its two prerequisite commits. Retained connected proof and complete cost samples support allocation and scaling gains, not native CPU/RSS relief.
Change-source: pulse-maintainer
Compose exact upstream a82a8e0474 additively without changing accepted commits. Resolve the append-only registry-test conflict by retaining both the reviewed metadata invalidation control and upstream stale-health control. Existing PR2486 remains unchanged while normal source-check corrections are pending.
Change-source: pulse-maintainer
The Coverage tooltip and expansion render the posture explanation
verbatim, and it read like the model: "a current subject-linked
backup", "older than the configured freshness window", "a newer
provider failure", "a stronger protection claim".
Each explanation now says what Pulse found in plain words and stays true
to its branch: the stale case names the window ("the newest confirmed
backup is older than 7 days"), the failure case no longer claims a
timing it cannot know, and partial history without a backup no longer
says a backup exists. The window formatter never rounds a sub-hour or
fractional window up. The frontend fallbacks for checking, unrated and
unknown rows and the snapshot-only tooltip use the same voice.
Repair deterministic governance and script-smoke failures against the reviewed guides without restoring public response output, literal credential commands or clobbering downloads. Exercise the actual shared API helper and independently verify private, unique response files, methods, bodies and hostile curl defaults.
Contract-Neutral: Update test adapters for the existing reviewed credential-private guide contract; no runtime or guide change.
Change-source: pulse-maintainer
Maximum-priority roots cannot be better than any group in a snapshot, so leave them out of the temporary owner indexes while retaining every resource and group. Correct new fixtures to respect existing attachment semantics: differently ranked hosts may already merge, and an agent's first-pass IP attachment must remain ambiguous until its two owners merge. Cover equal-priority Proxmox cost before any timing run.
Contract-Neutral: Candidate-only priority pruning and faithful regression fixtures preserve all stored/published resources, identity, strict attachment ambiguity, evidence precedence, public schemas and counted-system admission.
Change-source: pulse-maintainer
Rename the parameterised benchmark bridge so Go does not discover it as a benchmark entry point. Preserve the reference's exact-owner keys even for empty internal sets while retaining nonempty hostname-form admission. Pin unique versus ambiguous host attachment in the real monitored-count/prospective-admission path without mutating stored inventory.
Contract-Neutral: Test harness correction and faithful internal owner representation only; complete inventory, canonical identity, grouping evidence, counted-system admission and public schemas stay unchanged.
Change-source: pulse-maintainer
Build complete hostname/IP/form owner indexes once in priority order, visit only strictly better owners and reject once two distinct qualifying roots prove ambiguity. Preserve exact/short/IP evidence precedence, metadata freshness, exclusions and all resources. Bind the immediate-parent algorithm and enumerate targets independently; exercise real provider grouping and connected Infrastructure output.
Contract-Neutral: Pass-local representation and cost only; complete inventory, strict better-priority and ambiguity rules, evidence ordering, identity, public schemas, observations and alert policy are unchanged and covered by exhaustive and connected reference controls.
Change-source: pulse-maintainer
Assert refreshed status and CPU on the same canonical VM instead of requiring a Proxmox rename that the existing source-priority merge intentionally rejects. Use the mock runtime's supported maximum cadence. The initial fixture and assertion failures remain retained; no production behaviour is changed.
Refs: #2199
Contract-Neutral: Correct independent test assumptions only; no runtime or resource contract delta.
Change-source: pulse-maintainer
Keep the assigned-base typed-view rebuild as an independent benchmark reference. Measure warm reads, dirty List-only reads and dirty List plus typed views rather than claiming that moving derivation removes cold cost. Pin stale and between-record freshness, canonical policy detachment and arbitrary clone refresh.
Refs: #2199
Contract-Neutral: Extend independent validation of unchanged resource schemas and existing freshness/ownership guarantees; runtime source is unchanged.
Change-source: pulse-maintainer
Keep full inventory and arbitrary clone refresh. Detach materialized identity and policy under the existing registry lock, invalidate between batch records and on stale-source transitions, and keep List-only reads independent of typed view construction. Compare every field and wire payload with an independent fresh-clone oracle across all demo providers.
Refs: #2199
Change-source: pulse-maintainer
Contract-Neutral: Registry-internal copying and invalidation repair existing freshness and ownership guarantees; canonical resource fields, wire schemas, identity rules, policy derivation, telemetry and alert evaluation are unchanged.
Retain the pre-change fresh-clone oracle for every derived field, incremental ingestion and detached reads. Expose repeated canonical derivation in a 1000-resource bulk read before altering production code.
Refs: #2199
Contract-Neutral: Add independent regression controls and paired measurements; runtime and public contracts are unchanged.
Change-source: pulse-maintainer