allowHostAgentReenrollLocked trusted only the durable continuity store
when one was attached, so a removal block living only in persisted
monitor state (written by releases predating the store) could never be
cleared: every fresh re-enroll read as already-consumed and the host
was rejected with 400 on every report indefinitely, regardless of new
tokens or reinstalls.
Refs #1772
With the adaptive scheduler disabled (the default), every planning pass
on the main poll tick rebuilt each instance task with NextRun=now and
Upsert overwrote the pending slot, so availability targets polled at
the tick cadence regardless of their configured interval. Reproduced
at exactly 10s polling on a 60s ICMP target; c70431caa only covered
the adaptive-scheduler path. Planning now preserves a pending future
slot, tightening it only when a freshly shortened interval justifies
an earlier run.
Refs #1745
Every candidate_lanes record now carries demand_evidence: dated pointers
to the demand signals justifying the proposed lane (issues, discussions,
support threads, telemetry findings, or a demand-ledger entry in
pulse-pro/FEATURE_REQUESTS.md), or an explicit named-bet declaration for
maintainer-originated lanes with no external signal yet. The schema
requires the field, status_audit rejects missing or empty lists and
surfaces the evidence in the candidate_lanes and candidate_lane_queue
pretty blocks, and the canonical development protocol documents the
rule. No data migration needed; candidate_lanes is currently empty.
PR #1715 fixed the discovery symptom of forked host identities
(0e8f63cdd): lookups now resolve across the '<base>-<hex>' fork
spellings. The identity defect underneath remains: a re-enrollment
under a new token forks the host and the only heal path
(hostRenameHealSource) requires the same token plus a changed hostname
plus three missed health windows, so a re-enrollment fork fails two of
the three conditions structurally and is held open forever. Every
identity consumer must resolve equivalent spellings indefinitely or
silently miss records.
Healing at the source is clone-safety territory (#1584), so it is
registered as a triaged coverage gap for an owned agent-lifecycle
slice rather than an opportunistic patch. The evidence record carries
the verified heal-path conditions and the field-observed fork shapes,
including the truncated double fork.
Host identities fork onto "<base>-<hex>" when an agent re-enrolls under
a new token (re-running an install with --proxmox is enough), and
discovery then stores records under the forked host ID while the PVE
node keeps reporting the base agent UUID as its linked agent.
Store.ListByTarget matches TargetID byte-for-byte and
ListDiscoveriesByTarget did no alias resolution, so the resource
drawer's list call returned an empty 200 and the Discovery tab reported
"no saved discovery run" for hosts that had been analyzed.
GetDiscoveryByResource's normalization missed too: a base UUID matches
neither the forked host ID nor its hostname.
Expand the requested target into every spelling that provably names the
same target (equivalentDiscoveryTargetIDs): a matched node's linked
agent ID and name, a matched host's own ID/hostname pairing, and hex
fork suffixes of any of those, including the twice-forked
truncated-base shape. Filter the list path against that set and seed
the same spellings into the single-record alias path.
Trimmed from PR #1715 as submitted: the transitive hostname closure
(node name -> hostnames -> other hosts' IDs) is dropped. Multi-estate
deployments legitimately reuse node names and hostnames, and the hop
would have surfaced another estate's records in the drawer, discovery
readiness, and AI context. A regression test pins the non-bridging
behavior.
6417319b1 split frontend-modern/vite.config.ts out of the
dev-runtime-orchestration path policy into its own frontend-build-output
policy so production build output counts as proof. The
test-hot-dev-bg.sh manifest governance check still pinned the old
policy id, so the Script smoke tests job on main has failed since that
commit. Pin vite.config.ts to frontend-build-output; every other
manifest expectation is unchanged.
Reproduced the [FAIL] locally before the change; the full
test-hot-dev-bg.sh suite passes after it.
Follow-up to 6686cdce2: the 100-default/500-max list page bounds existed
as separate untyped constants in internal/api and internal/recovery/store,
relying on a contract clause to keep them aligned. Export
DefaultListPageLimit / MaxListPageLimit from internal/recovery/model
(re-exported through internal/recovery) and alias both consumers to them,
so drift is impossible by construction. store_test.go and
recovery_handlers_test.go pin both consumers to the shared constants;
the three pagination-meta contract clauses now name the single source.
Telemetry could see only saved connections, so an install that tried to
reach a node and could not was indistinguishable from one that never
opened the add-node dialog. Both report zero configured connections and
stall at the same activation stage. Fleet data shows that population is
real and concentrated three to one in container deployments, and nothing
recorded whether those installs attempted a connection at all.
Record node connection test attempts and failures in a bounded,
day-bucketed tally in the config directory, pruned to a 31-day retention
window, and report both over the install-ID rotation window as
node_test_attempts_30d and node_test_failures_30d.
Recording starts only once a request carries a target and credentials, so
an incomplete form is never counted as a node that could not be reached.
A host string that turns out to be unusable does count, because the
attempt was made and it failed. Only the add-node dialog endpoint is
instrumented: instrumenting the unused test-config endpoint as well would
double-count a single operator action.
The tally holds counts alone. Hosts, credentials, and error text never
enter it, which is why it is plain JSON rather than encrypted history.
The previous commit unintentionally flipped the mode to 100755 via a
temp-index cacheinfo; the script is invoked as 'node scripts/...'
everywhere, so keep the non-executable mode it has always had.
Vite injects modulepreload links for the entry chunk's transitive
static import graph, not just its direct imports. The posture check in
check-bundle-size.mjs allowed only direct imports, so a legitimate
future chunk-graph change (a preloaded vendor chunk statically
importing another chunk) would have false-failed the build and invited
a hasty weakening of the check. Compute the allowed preload set as the
static-import closure reachable from the entry instead; lazy route
chunks stay excluded because they are only reachable dynamically.
Re-verified: healthy build passes; injected lazy-chunk preload fails;
rebuilding with preloadDynamicChunks: true fails with 47 lazy-chunk
violations.
The deployment-installability verification policy routed
frontend-modern/vite.config.ts through the dev-runtime orchestration
proof set, all of which exercise the unbuilt hot-dev runtime. No
accepted proof could observe production build output, which is why
c4af728c0 (preload posture change) needed
PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT.
Split vite.config.ts into its own frontend-build-output path policy:
the accepted set keeps every dev-runtime proof, so dev-server-facing
edits are unchanged, and adds
frontend-modern/scripts/check-bundle-size.mjs, which now also asserts
the built index.html posture the contract clause pins: modulepreload
links limited to the entry's static imports (no lazy route chunks) and
import map integrity coverage of every built JS asset. The guard test
pins the new policy's accepted set.
Verified against the built output: flipping preloadDynamicChunks to
true fails the check with 47 lazy-chunk preload violations; the
healthy build passes. Full canonical-governance chain run locally, all
exit 0.
/api/recovery/points and /api/recovery/rollups clamp the requested page
size to [100 default, 500 max] in both the mock paginators and the store
paths, but the meta block was computed from the raw query value. A client
requesting limit=1000 with 1200 rollups was told totalPages=2 while the
server served 3 pages of 500, so iterating totalPages silently dropped
rollups; limit<=0 reported totalPages=1 at an effective limit of 100.
Normalize page and limit once at parse time, compute meta from the
normalized values, and echo the effective limit. Contract clause 34 in
api-contracts.md pins the obligation; storage-recovery and agent-lifecycle
record the boundary alignment and adjacency; recovery_handlers_test.go
pins above-max and non-positive limit meta.
The browser performance audit measured the client-facing resource
stream as a primary scalability cost at estate scale: a 4.73MB
single-frame snapshot at 1,508 resources, ~3KB of mostly-static
metadata per resource (only 10 distinct capabilities blobs across the
whole estate, identity alias history, aiSafeSummary prose, and policy
routing shipped to every browser), and ~1s of blocked main thread per
delta on desktop hardware. Slimming the stream is a wire-format change
with consumers beyond frontend-modern (pulse-mobile OTA compatibility,
Pro surfaces, AI runtime), so it is registered as a triaged coverage
gap for an owned slice instead of an opportunistic patch. Evidence
record carries the measured payload composition.
The SRI plugin's default preloadDynamicChunks injected modulepreload
links for all ~50 lazy chunks into the built index.html — 3.1MB raw
(4.2MB decoded): every Settings panel, Alerts, Chat, AI Intelligence,
all six platform pages, and both locale bundles fetched and compiled up
front, defeating route-level code splitting exactly where it matters
(slow devices, tab-evicted mobile browsers that cold start often).
Disable dynamic-chunk preloading; the entry keeps its three static
vendor preloads and dynamic-import integrity stays enforced through the
generated import map integrity block (112 entries verified in the built
output). Measured on a 4x-CPU-throttled mobile profile against the
50-node mock estate: cold-load long tasks 32s -> 18s, scripts fetched
99 -> 62, decoded JS 4.2MB -> 2.3MB.
Contract-Neutral: build-output preload posture change: deployment-installability's dev-runtime orchestration proof files all run the unbuilt hot-dev runtime and cannot observe built index.html preloads; verified instead by direct built-output inspection (0 route-chunk preloads, 112 importmap integrity entries) and 4x-throttle before/after measurement; substantive contract clause ships in this same commit
The primaryTabs/utilityTabs memos in AppLayout rebuild their arrays from
live store reads; activeAlerts is replaced wholesale whenever a state
frame carries it, so every reference-keyed <For> consumer (desktop tab
strip and the mobile bottom bar) tore down and recreated all nav buttons
even when nothing visible changed. Probed on the 50-node mock estate:
3 of 4 mobile bottom-bar buttons and 4 of 10 desktop tabs were detached
within 40s of websocket frames; taps landing mid-rebuild die silently.
Reuse previous tab object references (and the previous array identity)
when a rebuilt list is structurally unchanged, so downstream <For>s keep
their DOM. With the fix the same 40s probe keeps every nav element
connected on both form factors. MobileNavBar's proof suite now pins the
DOM-identity behavior and App.architecture pins the stabilizer wiring;
contract notes recorded in frontend-primitives, cloud-paid, and
ai-runtime.
The larger demo estate overflowed the rollups mock test's single
500-row page and pushed internal/api past the 25m -race budget, and
the alert history tests raced TempDir cleanup by never joining the
periodic save worker.
Walk every rollups page in the integration test, raise the shard
budget to 50m, and stop history managers through Stop() so the save
worker is joined before cleanup.
Contract-Neutral: test-only CI fix: paginate rollups mock test, join history save worker in test teardown, raise shard go test timeout for the 50-node demo estate; no runtime or contract delta
The hosted-tenant mint site (GenerateHostedTenantAgentInstallCommand)
issued binding-eligible install tokens without the command_policy_intent
metadata that 87ee3cd44 added to the self-hosted mint sites, so a hosted
tenant reinstalling an agent never got the stale-disabled-policy
reconciliation on first report (#1728). Hosted installs have no
enableCommands toggle, so the intent follows the install type the same
way the self-hosted flow does: pve enabled, pbs disabled.
Project freshly minted installer command intent onto the stable host before returning remote config, and consume that intent once so later admin policy changes remain authoritative.