Normalize diagnostics collection fields to empty arrays before encoding and harden the sanitized GitHub export path against null arrays so empty v5 installs can still produce issue attachments.
Refs #1454
shouldNotifyAfterCooldown previously returned true on every call when
Schedule.Cooldown was 0 or negative, which the alert evaluation loop
runs on every metric tick. With cooldown disabled, an active alert was
re-notified on each tick.
The UI labels cooldown=0 as "Disabled," so the intuitive contract is
"do not re-notify," not "re-notify continuously." Treat <=0 as
"first-time only": fire the initial notification, then suppress
subsequent re-notifications until the alert clears or the cooldown is
configured to a positive value. Level escalation re-notifications
remain handled at the call site and are unaffected.
Tests cover all three branches: first-time fire with cooldown=0,
re-notification suppression with cooldown=0 (named regression guard
for #1444), and the same behavior for negative values.
When the snapshot-polling budget runs out mid-loop, or a single guest's
GetVMSnapshots/GetContainerSnapshots call returns an error, the polling
function used to early-return without writing any state. That meant:
1. snapshots successfully fetched for earlier guests in the same
cycle were thrown away, and
2. on the next successful cycle, the freshly-polled snapshots
replaced the entire instance's snapshot list — wiping out any
snapshots whose owning VM had failed to respond this round.
For users with a busy production cluster (many guests, intermittent
per-VM API failures), this manifests as "new snapshots never appear
in the Backups tab" because the failing VM keeps blanking the list
the moment a successful poll lands (#1437).
Now we read the previous snapshots for the instance up front, track
which guests we successfully polled this cycle, and at the end merge
the fresh data with previously-known snapshots for any guest we
couldn't reach. Successfully-polled guests get their fresh data so
new snapshots appear; failed guests keep their last-known list so
transient errors do not blank state. The early-return on deadline is
removed so the merge runs even on partial-failure cycles.
Tests cover the carry-forward path: a fresh successful poll for one
VM lands a new snapshot, and a concurrent failed poll for a second
VM preserves its previously-known snapshot rather than dropping it.
The qemu-guest-agent's get-fsinfo cannot reliably report ZFS mounts on
some guest configurations (notably Proxmox Backup Server), so VMs that
have ZFS-formatted partitions show only their EXT4 root and datastore
in the VM Overview FILESYSTEMS card while the much larger ZFS dataset
holding the actual backups is missing entirely (Fixes#1438).
The unified pulse-agent running inside the same guest already has
direct OS-level visibility into every mounted filesystem, including
ZFS, and Pulse already knows the link between the host agent and its
guest via Host.LinkedVMID / Host.LinkedContainerID (set in
findLinkedProxmoxEntity by hostname match).
GetState now calls StateSnapshot.MergeLinkedHostDisksIntoGuests after
producing the snapshot. For each Host with a linked VM or container,
that helper:
1. appends host-agent disks to the guest's Disks slice, deduped by
mountpoint (qemu-guest-agent entries take precedence so we don't
overwrite per-VM-perspective values), and
2. updates the guest's aggregate Disk.{Total,Used,Free,Usage} to
include the newly-added partitions so the row total stays
consistent with the partitions visible in the FILESYSTEMS card.
The merge runs on a defensive copy of the disks slice to avoid
mutating the underlying State slice that GetSnapshot shallow-copies.
Tests cover the happy path (PBS-shaped fixture mirroring the issue
screenshots), the no-link no-op, container linking, empty-mountpoint
filtering, and the slice-isolation invariant.
Ollama keeps the loaded model in RAM for 5 minutes by default after
each request, and every new request refreshes that 5-minute window.
Pulse never passed keep_alive, so any Ollama traffic (Patrol, alert
analysis, AI chat) within 5 minutes of the previous request kept the
model warm — and on a server with continuous Pulse activity that
meant the model never unloaded, even with Patrol set to a 24-hour
interval (Fixes#1425).
Pass keep_alive=30s on every Chat and ChatStream request. Short
enough that the model unloads shortly after a Patrol burst or
one-shot analysis ends, long enough to span the small gaps between
sequential calls within a single analysis session (so the model is
not reloaded mid-burst).
Tests assert that both the streaming and non-streaming Chat paths
include the keep_alive field in the Ollama request body.
Distinguish a real rebuild ("recovery" after disk replacement) from
routine maintenance ("check" data scrubs, "resync" after unclean
shutdown) using the in-progress sync action from /proc/mdstat. The
mdadm --detail State field does not reliably surface scrub state on
all kernel/distribution combinations (notably Synology DSM), which is
why scheduled scrubs were firing "RAID array is rebuilding" warnings
every 30 seconds (Fixes#1446).
The mdadm parser now extracts the operation keyword from the
/proc/mdstat progress line and surfaces it as RAIDArray.Operation
alongside the existing speed parse. The alert layer treats "recovery"
and "reshape" as rebuild signals; "check" and "resync" are treated as
maintenance and do not fire an alert. Stringy State matching is kept
as a backstop for arrays without a /proc/mdstat progress line, but
"resync" alone in State no longer counts as a rebuild signal.
Threaded the new field through the host-agent report, the resources
converter, and the monitor's models conversion. Added /proc/mdstat
parser tests covering recovery/check/resync/reshape/idle, and
end-to-end alert tests for recovery (alerts), check (silent scrub),
and resync (silent maintenance).
When the email config passed to sendHTMLEmailWithError differs from the
manager's persisted config (a test send with edited but unsaved
settings), build a fresh manager so stale Username, Password,
AuthRequired, SMTPHost, SMTPPort, TLS, StartTLS, or Provider fields
cannot leak into the SMTP exchange. The shared production manager is
left untouched.
Without this, a relay-mode test (port 25, no credentials) on a
deployment that previously had authenticated SMTP saved would still
attempt AUTH and fail with "AUTH not available" because the manager's
old AuthRequired and credentials persisted (Fixes#1440).
When the configs match, the existing reuse path is preserved so the
production manager's rate limiter keeps working across grouped sends.
Address review feedback:
- Extract shared tool-call finalization into emitFinalToolCalls closure
to eliminate duplication between [DONE] and EOF-fallback paths
- Build tool calls in deterministic index order (sorted)
- Normalize stopReason consistently in both paths
- Add unit tests:
- TestOpenAIClient_ChatStream_ToolCallWithSimultaneousEOF: verifies
tool calls are parsed when Read returns n>0 and io.EOF together
- TestOpenAIClient_ChatStream_ToolCallWithoutDONE: verifies fallback
emission when stream ends without [DONE]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The read loop in ChatStream breaks immediately on io.EOF without
processing remaining buffered data. Per Go's io.Reader contract,
Read may return both n > 0 and io.EOF simultaneously, so the final
bytes (which may contain tool call deltas and [DONE]) are silently
discarded.
This causes the agentic loop to see tool_calls=0 even though the
model correctly produced tool calls in the stream.
Changes:
- Process pendingData when EOF is received before breaking
- Add fallback: emit accumulated tool calls if [DONE] was never
reached (server closed connection early)
Fixes#1411
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On QNAP, /usr/local/bin is a tiny RAM disk that gets wiped on every
reboot. The install wrapper stores the real binary under
${QNAP_VOL}/.pulse-agent/<name> and a boot script copies it back into
/usr/local/bin. Without refreshing the stored copy, auto-updates applied
to the RAM disk were silently reverted on the next reboot.
Mirror the Unraid persistence pattern: after the atomic in-place swap,
when running on QNAP, rewrite the stored binary via a temp-file rename.
Skip when the running binary already is the persistent copy (fallback
mode, where the rename step already updated it).
The infra discovery service auto-started with a hardcoded 5-minute
ticker the moment the AI service initialized, regardless of the user's
Patrol schedule. Each tick called AnalyzeForDiscovery, which hit the
Ollama chat endpoint and reset Ollama's keep_alive (5 min default), so
the model never had a chance to unload between requests.
Default the discovery interval to 24h and align it with the user's
Patrol preset (GetPatrolInterval) when the AI service constructs the
discovery service. With Patrol at its 6h default, the LLM now sits idle
long enough for Ollama to release it.
Fixes#1425
Apply quiet-hours suppression to escalation notifications so offline and other suppressed categories do not bypass the normal notification rules during escalation.
Fixes#1398.
Prefer Podman's reported CPU percentage from the compat stats payload and fall back to Podman's wall-clock calculation instead of Docker's multi-core normalization.
Fixes#1391.