New *_branchcov0718_test.go files extend the Go coverage wave to three more
areas of previously-uncovered pure logic. Covered areas include the domain-type
NormalizeCollections normalizers in models.go, the cloud control-plane pure
helpers for hosted-entitlement org-id normalization, magic-link URL redaction
and default MSP backup paths, and the actionlifecycle dispatch-attempt context
round-trip plus the typed-error Error and Unwrap methods.
Test-only, with no runtime or subsystem-contract change. Impure targets that
need a live Docker manager or tenant registry were skipped per the purity gate
and noted in the wave reports. Verified in a clean worktree at HEAD with go vet
and package tests green, gofmt clean, and every covered target moved from 0
percent toward full coverage.
New *_branchcov0718_test.go files raise coverage of previously-uncovered pure
functions across ten packages. Covered areas include securityutil SSRF and URL
validation, truenas path and telemetry parse helpers, storagehealth SMART and
physical-disk risk assessment, vmware inventory sort keys and error classifiers,
servicediscovery token filtering and readiness, telemetry evidence-from-history,
models ToFrontend converters and frontend NormalizeCollections normalizers,
actionplanner type predicates and canonical resource-id sort, config API-token
accessors, and licensing state accessors.
Test-only, with no runtime or subsystem-contract change. Verified in a clean
worktree at HEAD with go vet and package tests green, gofmt clean, and every
named target function moved from 0 percent to covered.
Four more modules had a hand-written test covering only the happy path.
These branchcov tests drive the previously-untaken arms and assert real
behaviour, the onboarding 409 fall-throughs (mismatched code, non-JSON
body, string body) and error-message fallbacks, the action-audit and
monitoring optional query variants, and the apt action presentation
status arms.
Ten more modules had a hand-written test but left request functions and
query or body branches unexercised. These new branchcov tests mock the
transport and assert the exact request shaping (path, method, query, body)
and parsed response for the previously-uncovered functions and arms.
API clients patrol, agentProfiles, monitoredSystemLedger, license, ai,
aiChat, connections, vmware and notifications, plus the securityScore
presentation helper.
Twelve pure modules had a hand-written test but left functions and
conditional arms unexercised. These new branchcov tests drive the
previously-uncovered inputs and assert real returned values and request
shaping, taking each module's uncovered functions to zero.
Presentation helpers alertDestinations, alertEmail, alertResourceTable,
aiSettings, auditWebhook, nodeModal, record, swarm and k8sNamespace, plus
API clients charts, discovery and truenas.
getAlertThresholdsDockerUpdatePresentation was the one uncovered function
in alertThresholdsPresentation after the module grew a Docker update
presenter. A single deep-equal parity test wires each returned key to its
exported ALERT_THRESHOLDS_DOCKER_UPDATES_* constant, matching the sibling
getAlertThresholdsSectionTitles test. Module function coverage moves from
8/9 to 9/9.
Render the same Backup health strip in both views instead of swapping a
one-line summary for the taller strip, which shifted the By date and
Coverage toggle 56px on every click. Also replace the hand-rolled row
counter with the shared PlatformResourceCounter (now exported).
Contract-Neutral: Presentational fix on the Proxmox backups surface header/counter; no public contract delta
Promote Type, Target ID, and Node out of the stacked workload cell into
their own columns so Coverage rows are single-line and read the same as
the by-date recoverable table.
Contract-Neutral: Presentational table-row layout change on the Proxmox Coverage table; no public contract delta
2ba6c66e3 raised WORKLOAD_TABLE_WIDE_LAYOUT_WIDTH from 1440 to 1536 but
only updated the Workloads GuestRow test. The Docker containers spec
pinned its viewport at a hardcoded 1500px, which now resolves to the
compact stage, so the wide-gated networks cell (frontend 172.18.0.2)
never rendered and the API-fields test failed on main. Import the
canonical constant instead of a magic width so the spec tracks any
future breakpoint move.
A routine Docker container update took five clicks across two in-row
confirmations and a two-phase Approve then Run dialog, and the review
screen said "Rollback declared: No" while its own intended-change text
promised automatic backup and rollback (#1564 feedback).
- Capabilities can declare rollback support; the planner carries it
into plan.rollbackAvailable instead of hard-coding false, so the
container update review now truthfully says rollback is available.
- Container update is classed AutoAuthorizeLowRisk (it ships backup and
rollback; restart was already low_risk). The action projection
exposes the read-time capability class plus resolved blast-radius
names, none of which enter plan identity.
- The review dialog collapses pending approval for low-risk
capabilities into one Approve and run control that records the
approval and dispatches execution from a single confirmation; other
capabilities keep the two-phase flow. Blast radius renders resource
names with IDs demoted.
- The in-row Update button plans on first click; the dialog is the
confirmation surface, so the Confirm hop and its cancel affordance
are gone.
Routine update ceremony is now Update, Approve and run, Close, with
the durable action record unchanged.
The discovery-config surface changed: with discovery disabled by default the
empty state offers a 'Configure discovery' button (which opens the Discovery
settings dialog), and the 'Run discovery' scan button only renders once
discovery is enabled.
- Landing test: assert 'Configure discovery' and that 'Run discovery' is absent,
instead of the stale 'Run discovery' / 'Discovery settings' button names.
- Discovery settings dialog test: open it via 'Configure discovery' (the dialog
title/copy/close-button assertions were already current).
- Explicit discovery scan test: enable discovery for that test by passing the
real /api/system/settings response through with discoveryEnabled flipped on,
so the 'Run discovery' scan button appears and the candidate->review flow runs.
Verified: all 5 desktop tests pass against the managed local backend.
Contract-Neutral: E2E quarantine recovery
Re-pin the assistant-drawer flow to the current surfaces: open via the
page-contextual 'Ask Pulse Assistant about TrueNAS' launcher on a live
/truenas page (not the retired /settings/infrastructure/platforms/truenas), and
resume the stubbed session through the 'Pulse Assistant sessions' menu -> the
'Resume Tool output preview proof' option (the session-picker list, not a bare
resume button).
Broaden the /api/ai/sessions stub glob to '**/api/ai/sessions*' so it matches
the picker's ?limit= query (the trailing * excludes the /messages subpath). The
tool row now renders the human summary with the target ('Run read-only command
on current resource') and the raw command preview ('$ printf preview-proof-0606')
as two separate lines, and the details control is the tool-row toggle
(title 'Show tool details'), not a standalone 'Details' button.
Verified: passes against the managed local backend.
Contract-Neutral: E2E quarantine recovery
Retrofit a worker-scoped cookie-session fixture (journeyAuth.ts) onto journey
smoke specs 01/06/07 so each worker logs in once and every test inherits the
cookie via storageState, instead of ensureAuthenticated's token path (which
leaves page.request cookie-less -> 401 on apiRequest/agent-report/audit
endpoints).
ensureJourneyReady verifies the cookie against the API directly and lands the
page on a non-bootstrap route (never '/' or '/login', which force the login
form even with a valid session -- #1574), so per-test auth issues zero login
POSTs and the suite stays under the backend's 10-logins/min/IP limit. The two
login-flow tests (health check, bootstrap-and-login) plus the logout/re-login
cycle keep their own explicit login on an isolated session -- logout() destroys
the current session, so it must never run against the shared worker cookie.
Re-pin 01 to the current platform-first IA: primary-nav tabs
['Proxmox','Alerts','Settings'] (no single 'Infrastructure' tab; Proxmox is
resource-admitted, so beforeAll enables mock mode first), settings sidebar
categories ['Infrastructure','System','Security']. Re-pin 06's reporting route
to /settings/support/reporting (heading 'Data & Reports') and guarantee mock
mode on before the two report-generation polls. Accept 503 in 07's
investigation-endpoint status set.
Verified: each spec passes on its own and a combined 01+06+07 run stays green
with zero login-rate-limit failures against the managed local backend.
Contract-Neutral: E2E quarantine recovery
Wiping the agent state dir regenerates the agent ID and a fresh install
command mints a fresh token, so Docker host identity resolution correctly
refuses to adopt the old record (a foreign token must never take over a
live host) and creates a new one. Nothing ever reaped the old record, so
it sat in state forever with a stale agent version, stale containers and
stale image digests. That is the source of the outdated-agent banner that
contradicts the update dialog in #1586, the doubled container rows in
#1564, and plausibly the digest-mismatch preflight refusal there too.
Reap a matching record only when it stopped reporting before the
superseding token was minted, the same explicit-reenroll-intent rule the
removal block uses (#1581). A record that is still reporting keeps
advancing LastSeen and is never touched, and supersession sets no
resurrection block.
Refs #1586#1564
PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT: docker host state bug fix; live Codex agent WIP holds the contract docs
The pre-auth login bootstrap short-circuit only honoured per-tab hints
(sessionStorage auth user, in-memory token) before deciding to pin the
user to the login page, and the session cookie is HttpOnly so the
document.cookie fallback can never see it. Closing the tab therefore
forced a fresh login even though a valid 30-day remember-me session
cookie was still present, on every browser.
Treat a remembered login username in localStorage as a bootstrap hint so
the /api/state probe runs and the HttpOnly cookie can authenticate.
Verified live on an isolated backend: with the hint the reload lands
authenticated; without it the same cookie state pins to login.
Fixes#1531
PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT: frontend auth bootstrap bug fix; live Codex agent WIP holds the contract docs
These never-quarantined specs had rotted red on CI against v6 IA changes.
- 67: the add-infrastructure dialog replaced per-platform buttons with a
detect-or-agent chooser; the TLS-warning tests deep-link straight to
the platform credential slot via ?add=vmware / ?add=truenas
(ADD_STEP_TO_TYPE), and the notifications page heading is now
'Notifications'.
- 68-platform-pages-shell: platform sub-tabs are inventory-gated
(64cbdd6e4), so a platform with no connected instance shows the
empty-state card instead of sub-tab chrome; the no-data test accepts
either. The Docker containers tab was collapsed into Overview
(1f66a2f93), so it drops from the expected tab list.
- 69: the Commercial Funnel and Infrastructure Onboarding analytics were
removed from the diagnostics page and both exports (c6bcad255); the
spec now pins that boundary (analytics never render and are stripped
from full and sanitized exports) instead of asserting they appear.
Contract-Neutral: E2E straggler re-pins verified green on a fresh managed backend
Delist nineteen quarantined specs (69 green, 13 env-gated skips in one
full-batch run); 05/19/20/30/52/60/63/77/79 passed unchanged. 03 stays
quarantined with its reason recorded inline: multi-tenant scenarios 6/7
carry an unresolved org-resolution bug that CI's multi-tenant
environment would exercise, and 47/48 still await their concept rework.
- 15: organization panels only exist in multi-tenant installs, so those
routes now skip when the feature is off, and the Remote Access shell
description follows the refreshed Relay copy.
- 17: the backups page defaults to the guest-centric Coverage view when
anything needs attention; the day-activity assertions enter the By
date view first.
- 18/51: the Patrol page crashed to its error boundary under stale
autonomy stubs; the stubs now carry the requested/effective levels and
autopilot acknowledgement the page reads, and the paused copy is
sentence-case.
- 43: rewritten as a platform-pages tour (TrueNAS, vSphere, Proxmox,
Docker, PBS, PMG) since the settings platform views it pinned were
retired to a 404 with the legacy aliases.
- 49: the demo-estate proof follows the per-platform IA and drops a
self-contradictory storage exclusion that could never pass alongside
the west-a-service-pool assertion.
- 57: the RC Preview badge only renders in the authenticated shell, so
the spec signs in instead of stubbing the session.
- 62: the retired platform-management aliases deliberately 404 now
(cfb16e6da); the spec pins the graceful not-found page instead of the
removed redirect.
- 75: switches to cookie-session auth (the token path leaves
page.request unauthenticated), anchors on the Connection posture
region, and drops the retired operations sub-route.
- 84: a missing lab artifact is now a skip rather than a hard failure.
Contract-Neutral: E2E quarantine recovery, spec re-pins verified green on a fresh managed backend
Re-pin five quarantined commercial-funnel specs and delist all eight
from the quarantine (54/55/56 already passed unchanged; the two managed
demo runtime tests stay env-gated).
- The old stubbed auth shell renders the login page now that the app
shell makes auth-gated calls beyond the stub set; 59/61/70 sign in for
real and stub only the license or diagnostics surface under test.
- 53: hidden demo billing routes redirect to the settings infrastructure
root (the /install sub-route was retired), and the browser probe list
gains the trial/activate endpoint its hidden-keys contract already
named but never probed.
- 58: Remote Access stays in the free-tier nav on purpose (the panel
renders its own upgrade gate; hiding it made Relay undiscoverable), so
the spec pins its presence instead of its absence.
- 59/70: billing moved to the Pulse Intelligence settings group with a
Plans & Billing heading; plan-card assertions re-anchor at page scope
on the current copy (capability sentences, Apply key, Manual key
recovery disclosure, Patrol-flavoured activation summary).
- 61: the Commercial Funnel diagnostics card was deliberately removed
(c6bcad255); the spec now pins the inverse boundary, asserting the
internal funnel analytics never render even when the API payload
carries them.
Contract-Neutral: E2E quarantine recovery, spec re-pins verified green on a fresh managed backend
Re-pin seven quarantined specs to the surfaces that replaced the retired
/workloads and /storage pages and delist them from the quarantine.
- 44/45: the summary chart strip and its range buttons are gone; the
sparkline path maps x by timestamp, so the payload proofs assert
window coverage and tail stability directly against
/api/charts/workloads.
- 46: multi-range storage history proofs move onto /proxmox/storage
with point-count floors a fresh seeded backend guarantees.
- 50: physical disks live under the Physical Disks tab on
/proxmox/storage; rows are pinned by identity because live refreshes
re-sort the table, and late in-flight poll responses no longer fail
teardown.
- 59: the wide-set assertions run at 1600px (wide engages at 1536 after
the layout fix) plus a no-horizontal-scroll proof for the 1440px
compact default.
- 62: the Growth column is fixed to a 24h window; the rendered label is
proven against the same used-capacity history the endpoint serves.
- 64: the workloads surface lives on /proxmox, drawer tabs expose tab
roles, and row selection no longer writes a resource deep link.
Contract-Neutral: E2E quarantine recovery, spec re-pins verified green on a fresh managed backend
The workloads table picks its layout stage from the viewport width but
renders inside a shell roughly 120px narrower. The wide column set sums
to about 1370px, so engaging it at a 1440px viewport left the DEFAULT
desktop layout scrolling horizontally by about 50px between 1440 and
1535px. Horizontal scroll is reserved for columns a user explicitly
pins; the breakpoint default must fit, so the wide stage now waits for
a 1536px viewport.
Found by re-pinning the workloads column-layout E2E spec, whose no-
horizontal-scroll contract this drift had broken.
Contract-Neutral: frontend layout fix with unit coverage; contract docs carry another agent's WIP on the shared tree
Field report (Johannes Strasser, 6.1.0-rc.3 mobile chat). The assistant
could find an LXC container but told him it had no tools to restart it.
Relay-mobile tokens carry only the relay access scope, which does not
satisfy the execute-authority check, so mobile chat sessions were offered
read tools only. The gap was one-sided: mobile could already approve and
execute planned actions, it just could not propose them.
Relay-mobile callers now hold execute authority, and the chat entrypoint
pins their sessions to approval-required mode, so every mobile-originated
action lands on the approval queue regardless of the configured control
level. Web sessions are unchanged.
Proxmox guests advertise reboot while container platforms advertise
restart, and the Assistant tool schema plus the resolved-resource action
lists teach models both words. A pulse_control call with the wrong
spelling failed with CapabilityNotFound even though the equivalent
capability was advertised. Plan requests now follow the resource's own
vocabulary when the requested verb is missing but its lifecycle synonym
is advertised; the rewritten name flows into the plan, the audit record
and the executor. Non-synonym verbs still fail closed.
Field report (Johannes Strasser, 6.1.0-rc.3). The chat control-mode pill
wrapped to three clipped lines at the panel's right edge. The composer
chip row forced a single line with hidden overflow from the sm viewport
up, but the panel is 560px wide regardless of viewport, so a long model
label squeezed the trailing pill into the clip. The row now wraps at
every width, matching its below-sm behavior, and the pill label stays on
one line.
The fixed 560px width behind that failure mode is now adjustable. A drag
handle on the panel's left edge resizes it between 420px and 960px
(clamped so the rest of the app stays usable), double-click resets,
arrow keys nudge for keyboard users, and the width persists per browser.
Tool-heavy chat histories exceed the relay proxy's 47KB response cap, so
the mobile app could never load big conversations over Remote Access.
GET /api/ai/sessions/{id}/messages now accepts compact=1 (strips tool
outputs, results and thought signatures while keeping tool_use_id
linkage) and max_bytes=N (returns the newest messages that fit the
budget; a single oversized message is truncated with a notice). The
mobile client sends compact=1&max_bytes=40960 and falls back to a
halving limit ladder when older servers answer 413.
Percentage metric specs derive their critical escalation as trigger+10
capped at 99. A user-set trigger of 99 or 100 produced critical <=
trigger, which failed spec validation and silently dropped the whole
metric from evaluation while logging a warn every poll cycle. Omit the
escalation threshold instead so the warning-level threshold keeps
working.
Seen in the wild as the 'Skipping invalid canonical unified metric
spec' warn spam in #1593's diagnostics.
Contract-Neutral: behavior fix with regression test; alerts contract docs carry another agent's WIP on the shared tree, delta deferred
A PBS datastore reached the unified registry twice, once from the PBS
snapshot adapter under the canonical <instance-id>/<name> source ID and
once from the PBS poller's models.Storage conversion under the legacy
<instance-id>-<name> storage ID. The thresholds page rendered two cards
per datastore and their alert overrides split across both key formats,
with only the legacy key honoured at evaluation time.
Skip the redundant storage-conversion ingest when the canonical
datastore resource is already registered, carry the canonical ID as a
storage alias so evaluation accepts overrides in either format, and add
the legacy dash key to the frontend candidate list so pre-existing
overrides stay bound to the single surviving card and re-home onto the
canonical key on the next save.
Fixes#1591
Contract-Neutral: behavior fix with regression tests across registry/alerts/frontend; contract docs carry another agent's WIP on the shared tree, delta deferred
The PVE backup poll classified storage as PBS-backed by the pbs- name
prefix as well as the storage type. A dir storage merely named pbs-*
(content backup, is_mountpoint) lost all of its vzdump backups from the
Backups view whenever a direct PBS connection was also configured,
because the poll skipped its content as 'PBS is authoritative'. Classify
by storage type only.
Fixes#1592
Contract-Neutral: behavior fix with regression test; monitoring contract docs carry another agent's WIP on the shared tree, delta deferred
The canonical alert evaluation reactivation branch read and mutated
recentlyResolved and resolvedAlias while holding only m.mu, while the
broadcaster and recovery paths guarded them with resolvedMutex, so the
two lock domains did not exclude each other and a flapping resource
re-firing within the five-minute cooldown concurrently with the state
broadcast crashed the daemon with a concurrent map access fault
(issue #1590, reported with exact sites and a race repro).
Both eval sites now take resolvedMutex for the lookup and removal only,
keeping history and dispatch outside the lock. GetResolvedAlert takes
the write lock because getResolvedAlertNoLock backfills resolvedAlias
on a canonical-identity miss, which made even read-locked lookups
racy against each other. The lock ordering documentation now states
the real rule the cleanup path already relied on: resolvedMutex is
subordinate to m.mu and may be acquired while holding it, never the
reverse.
Contract-Neutral: crash bugfix restoring the documented resolved-map lock guarantee (#1590); no contract surface changes
The Patrol finding-notification work (23695681a, e0e454abf) added
patrol_finding_notifications_enabled and
patrol_finding_notify_min_severity to the AI settings payload but did
not update the six JSON snapshot pins, leaving Build and Test red on
every push since. The fields are intended API surface; the snapshots
now include them.
Contract-Neutral: snapshot refresh for the intended Patrol finding-notification fields added in 23695681a/e0e454abf; no behavior change
With no systems connected, the Connected systems page rendered two
buttons both named Add infrastructure, the page-header CTA and the
posture band's setup action. Identical accessible names on one page are
ambiguous for assistive tech, and the strict-mode accessible-name
locators in several recovered Core E2E specs resolved both and failed
on every fresh-backend CI run. The zero-state posture action is now
Add your first system, which also reads more task-first next to its
own detail line.
Contract-Neutral: zero-state CTA label disambiguation; no contract surface changes
Both specs came back from quarantine on 2026-07-17 proven against
chromium, but Core E2E also runs them on mobile-safari, where they have
failed on every run since. The mobile layout collapses the thresholds
platform-scope filter into the FilterBar sheet, so the TrueNAS scope
button the first spec clicks does not exist and the click falls through
to the platform nav tab, and platform tables render as cards, so the
tr-row locators the second spec asserts on match nothing. Skip both on
mobile projects the same way 06-theme-visual does; mobile presentation
coverage lives in 04-mobile.spec.ts.
The desired side of the /api/connections command-policy comparison is
contractually the effective config served to the agent after token scope
and binding checks, but a host whose recorded TokenID no longer resolved
to a live API token skipped sanitization entirely and kept the raw
profile desire. A stale binding, typical after a token was revoked and
the agent reinstalled, then presented as Command policy mismatch even
though the served runtime config could never enable commands and the
operator had nothing to fix (seen on issue #1564). When API tokens
exist and the host's token cannot be resolved, desired command policy
now fails closed to disabled.
The contract delta also records the install-token mint semantics that
landed with the Add Pulse Agent scope fix, closing the deferred
agent-lifecycle delta noted on that commit's contract-neutral trailer.
Contract-Neutral: fail-closed bugfix bringing the connections command-policy view into conformance with the existing agent-lifecycle contract clause; the staged agent-lifecycle delta records the semantics, api-contracts/storage-recovery have no real delta
Completes #1369. The delivery path landed in 23695681a with config-only
gating; this surfaces the two fields so operators can turn finding
notifications off or restrict them to critical findings without editing
config by hand.
The AI settings GET response and update request carry
patrol_finding_notifications_enabled and
patrol_finding_notify_min_severity, with the same warning-or-critical
validation the alert-trigger severity field uses. The Patrol settings
page gains a Notifications card between Triggers and Model readiness,
mirroring the alert-trigger toggle-plus-severity pattern, with the
severity select shown only while notifications are enabled.
Exercised end to end against a scratch backend in mock mode. A severity
change and an explicit opt-out both survive save, reload, and the GET
round trip, and the opt-out hides the severity select immediately.
Patrol findings previously reached operators only through the Relay
mobile push path. Anyone relying on the email, webhook, or Apprise
destinations they already configured for alerts got no proactive signal
when Patrol detected a problem and had to open /patrol to learn about
it. This is the delivery half of #1369.
Each newly stored warning or critical finding now also flows through a
FindingNotifyCallback wired in the router, which projects the finding
into the alert shape the notification manager delivers. The callback
fires only on the genuinely-new path in recordFindingWithInvestigation,
so a finding notifies at most once per lifetime regardless of how many
later runs re-detect it, and SendAlert's own per-ID cooldown backstops
that. Demo mode never notifies.
Gating lives in AIConfig via patrol_finding_notifications_enabled
(default on, matching the long-standing default for mobile push) and
patrol_finding_notify_min_severity (warning or critical, default
warning). The enabled flag persists without omitempty so an explicit
opt-out survives reload while pre-existing configs inherit the default.
The settings surface for these fields follows in a separate commit once
the AI settings handler is free.
Keys linked to Readonly Admin users return 403 on endpoints Pulse
polls through the deprecated REST bridge on TrueNAS SCALE 25.10
(issue #1589); note the Full Admin workaround until the WebSocket API
migration (#1550) lands.
Contract-Neutral: user docs note for the TrueNAS 25.10 role limitation (#1589)
The manual-run safety timer declared 'connection timed out' whenever
the Patrol stream stayed quiet for 15 seconds, but the trigger request
had already succeeded and a self-hosted model cold-loading can take
longer than that to emit its first event, so it reported failure over
runs that were still warming up (issue #1588). The timer now refreshes
Patrol data first and only surfaces a failure when the status shows no
run in progress, with wording that names the provider rather than the
connection.
Contract-Neutral: frontend toast timing bugfix (#1588); no contract surface changes
The Add Pulse Agent flow composed a fixed scope list in the frontend,
so ticking Enable Pulse command execution added --enable-commands to
the install command while the token it shipped with never carried
agent:exec, and the command channel rejected every registration
(issues #1586, #1564, confirmed by a reporter's server log). Even with
the scope, the generic token had no binding metadata, so the first-use
binding gate would have refused it next.
The install token is now minted through POST /api/agent-install-command
with type 'host'. The server decides the scopes from enableCommands at
mint time, stamps the install_type/issued_via metadata that makes the
token eligible for first-use command-channel binding
(canBindProxmoxAgentInstallExecToken renamed canBindAgentInstallExecToken
and extended to the host install type), and returns the sanitized token
record. The frontend regenerates the token when the checkbox toggles,
since scopes cannot be upgraded on an existing token, and revokes the
superseded token so toggling does not accumulate orphans.
Contract-Neutral: install-token mint bugfix (#1586, #1564): checkbox-promised exec scope now real; agent-lifecycle contract delta deferred because the contract docs carry another agent's uncommitted WIP on the shared tree
The rejection told operators to re-run the agent installer, but the
token minted by the original install command cannot gain the exec scope
after the fact and install tokens are single use, so re-running the
same command loops forever (issues #1586, #1564). Name the actual
recovery step instead.
Contract-Neutral: reworded operator-facing rejection copy (#1586, #1564); no behavior or contract change
A pool built from /dev/disk/by-id/nvme-MODEL_SERIAL_1-part1 references
(systemd appends the _1 namespace token) made serialFromByID take the
last underscore token, so it extracted the namespace digit instead of
the serial and the disk fell back to the generic 'ZFS' usage string
while an identically-built node without the suffix resolved its pool
name. Reported with exact zpool output in issue #1540.
Contract-Neutral: bugfix restoring intended disk-to-pool annotation (#1540); no contract surface changes
The server-side disk-to-pool matcher learned nvme-eui.<hex> zpool member
references in 4dcc18fbd, but the host agent keeps its own copy of the
matching for SMART pool annotation and never derived a key from the
nvme-eui form, nor stripped the eui. prefix smartctl reports on NVMe
WWNs. systemd's nvme by-id links can also carry a trailing _<n>
namespace suffix (nvme-MODEL_SERIAL_1), which made the serial
derivation return the namespace digit instead of the serial. Both gaps
left a pool member unmatched so the disk fell back to the generic
usage string (issue #1540).
Contract-Neutral: bugfix restoring intended disk-to-pool annotation (#1540); no contract surface changes
wilddev65's rc.2 retest confirmed the connection-keying fix split the two
same-serial CORE boxes but left four symptoms. All four trace to Pulse
reading fields the TrueNAS API has never served, which the pre-fix
identity flapping had masked behind bigger breakage:
- Datasets all showed Offline: pool.dataset.query returns no "mounted"
field on any TrueNAS version (CORE 13 and SCALE both strip it from the
property allowlist), and the parser read absence as unmounted. A listed
dataset now defaults to mounted unless "locked" (encrypted, key
unloaded) or an explicit mounted=false says otherwise.
- Disks all showed Attention/Unknown: disk.query carries no status or
smart_status field either, so every disk landed on UNKNOWN health,
which the storage table bucketed as Attention. Per-disk ZFS state
(ONLINE/DEGRADED/FAULTED/...) and pool membership are now derived from
the pool.query topology that both platforms attach unconditionally --
disk.query's extra.pools cannot cross the REST bridge, whose query
params turn into filters that silently match nothing. Disks therefore
nest under their pools again, and a genuinely unreadable health renders
as neutral Unknown instead of amber Attention.
- Disk temperatures were blank: disk.temperatures takes parameters, so
REST v2.0 has only ever served it as POST with a body keyed by param
name; the client's GET failed on every version, and CORE (no JSON-RPC
endpoint) has no reporting fallback to rescue it. The ladder now tries
native JSON-RPC reporting first and falls back to the POST form, and
the provider surfaces API-reported disk temps as SMART sensor entries
so the host Thermals card lists disks for API-backed systems.
- The storage table ordered identically-named pools (a DR pair both
named "tank") by map iteration order, flipping between refreshes;
ordering now tie-breaks on system name then id.
Verified end-to-end against two stub TrueNAS CORE 13 REST servers
reporting the same placeholder DMI serial: storage tab nests both
systems' pools/datasets/disks correctly with temperatures and Healthy
states, and the system drawer Thermals card lists per-disk readings.
Completes the first-session elicitation fix that a3f8b18bf started at
the tool layer. The second observed failure mode ("Are there any alerts
I should look at?" on Ollama qwen3:8b) had the model calling
pulse_question BEFORE any tool call, so no tool error copy and no tool
description can reach it — the resolve-before-asking prompt policy
(945ed2198) needs runtime teeth for small local models.
The agentic loop now refuses an interactive pulse_question issued
before the run has attempted any real tool call: the model gets an
error tool result steering it to read-only enumeration
(pulse_summarize {"action":"fleet"} and the alert tools need no
parameters), no question card or clarify event reaches the stream,
sibling tool calls from the same provider turn keep processing instead
of tripping the interactive-set skip path, any queued tool attempt
satisfies the gate for the rest of the run, and it fails open after 2
refusals so an unanswerable prompt cannot livelock. Non-interactive
profiles keep their existing separate block. The system prompt
discloses the gate; question-flow tests now open with a look turn the
way real runs must; a corpus scenario pins the stream promise (a
natural first question produces an answer, never a clarification card).
Live-verified on a real Ollama qwen3:8b scratch instance (real local
agent, not mock): both natural openers stream tool-backed answers with
zero question events. Full ./internal/ai/... suite green. ai-runtime
contract updated in-commit.
A fresh install's natural first question ("how is my machine doing?",
observed live with Ollama qwen3:8b) made the model call pulse_summarize
action=fleet without resource_ids; the tool errored and the agentic loop
surfaced a structured question asking a first-run user for "the
comma-separated list of resource IDs" — pure jargon, funnel-killing
(GitHub discussion #1042). Two root fixes:
pulse_summarize self-targets. action=fleet with resource_ids omitted now
enumerates the known fleet from the executor's unified resource provider
(infrastructure parents first, then guests, then storage; deduped;
bounded at the existing 50-resource cap with a truncation note). Both
modes resolve what models actually pass — canonical unified IDs and
unambiguous names — onto the reporting request shape the way the API
path's resolveReportSubject does: the canonical ID stays ResourceID
(findings/recovery keying) and the resolved metrics target rides
MetricsResourceID, so store queries find data instead of silently
returning zero points. Reporting types are classified from the unified
resource (agent-backed hosts "agent", pure Proxmox nodes "node" — the
documented target-type exception — Docker hosts "docker-host");
resource_type becomes an optional filter/default. Remaining error paths
instruct the model to enumerate or retry and forbid asking the operator
for resource IDs.
Ask-user policy covers tool-argument recovery. The system prompt's
resolve-before-asking section (945e2198's target policy) now extends to
failed tool calls: missing/invalid arguments are self-recovered
(enumerate, retry), and internal identifiers are never valid questions.
The pulse_question description carries the same prohibition so the
structured clarification surface cannot become an identifier elicitation
channel on small local models.
ai-runtime and api-contracts contracts updated in-commit (the
subsystem_lookup line pin follows the api-contracts insertion); full
./internal/ai/... and ./internal/agentcapabilities/... suites green.