Package the atomic API-token deletion fix, alert delivery evidence, reducer-backed lifecycle cutover, separated agent install tokens, and filesystem history feedback for the next release candidate.
Change-source: pulse-maintainer
Document reducer ownership for stateful alert transitions, timestamping, manual clears, acknowledgement retention, and refire history. Register the purpose-built stateful regression suite as canonical runtime and runtime-support proof.
Change-source: pulse-maintainer
Record the reducer as transition truth for canonical lifecycle and shared metric thresholds, including intent grace and activation-event ownership. Add a focused proof that metric intent remains pending until activation and emits one fired event.
Change-source: pulse-maintainer
The last uncharacterized behavior: while Pulse has fresh evidence a
Proxmox backup caused an offline state, activation defers — bounded by
the max-deferral cap on total condition-active time — and after the
backup ends the grace extends to the backup's end plus the post-grace,
still capped. The discrete activation path is unified so the gate always
operates on a tracked pending incident, and the shadow feed now models
the deferral independently from the manager's decision instead of
echoing its hold as operator suppression.
Unit tests cover deferral, post-grace release, the never-ending-backup
cap, and post-grace capping; parity runs the real
LoadIntentPolicies/IntentBackup composition with simulated clocks.
Fourth consecutive parity slice with no manager defect. Every
discrete-family and metric-family behavior is now pinned.
Phase 1 capstone of docs/ALERT_ENGINE_EVOLUTION.md. The deterministic
reducer now runs continuously inside the live manager against the same
production observations: the canonical lifecycle path (connectivity,
powered-state, discrete-state kinds) via a deferred hook that replays
each evaluation — including the resolved intent context — through the
reducer; the poll-driven recovery paths (PBS/PMG/storage, node,
connection-degraded); and manual acknowledge/unacknowledge/clear. The
feed seeds from active canonical alerts at enable so restarts do not
read as mass divergence.
Every state disagreement is counted (Manager.ShadowDivergences) and
recorded in the alert event log as a shadow_divergence event with both
engines' states, rate-limited to one report per key per ten minutes.
After each divergence the reducer resyncs to the manager, so one
divergence yields one event — including divergences caused by manager
mutations the feed does not observe. Appends never block evaluation and
a disabled feed is a nil-check no-op.
This converts the parity harnesses' test-time guarantee into an
always-on invariant; the production divergence rate becomes the
go/no-go evidence for each Phase 2 family cutover. The full
activation/ack/recovery/re-fire cycle runs divergence-free in tests.
The input-layer direction stands; the transition-core and suppression
freezes do not (docs/ALERT_ENGINE_EVOLUTION.md). The stable-behaviors
list is a characterization inventory pinned by the reducer parity
harnesses, not a freeze — so a future session cannot mistake the frozen
scoping for current direction.
Document activation-only intent gating and register the reducer and manager-parity proofs required by canonical governance.
Change-source: pulse-maintainer
Phase 1 slice 6 of docs/ALERT_ENGINE_EVOLUTION.md.
DiscreteRule.Intent characterizes the manager's intent gate as composed
by evaluateCanonicalLifecycleAlert: operator suppression (expected
offline, muted, retired, active maintenance windows) and explicit grace
policies hold activation only — confirmations keep counting, the
incident stays pending, and release activates with the run's first
active observation as the start; an already-firing incident is never
suppressed. Grace accrues concurrently with operator suppression. The
manager measures grace on monotonic process runtime; the reducer uses
the signal clock, coincident under continuous operation.
reducer_parity_intent_test.go drives the real composition with the
manager's m.now and m.intentClock seams on the simulated clock, a
scenario-controlled operator resolver, and policies loaded through
LoadIntentPolicies. Third consecutive parity slice with no manager
defect found; the operator scenarios' StartTime parity exercises the
lifecycleFirstMatched preservation in composition. Deferred: the
backup-offline deferral sub-policy.
Record acknowledgement retention and restoration semantics in the alerts contract, and register the reducer unit and manager-parity proofs for the slice.
Change-source: pulse-maintainer
Phase 1 slice 5 of docs/ALERT_ENGINE_EVOLUTION.md. State.Acknowledge /
Unacknowledge characterize the manager's ack semantics: an ack marks the
firing incident and a canonical record, survives per-tick rebuilds and
short resolve/re-fire cycles (restored on re-activation), is cleared by
unacknowledge, and expires after an hour of inactivity. The manager's
restore path has no age check — expiry comes from cleanup pruning the
inactive record — so the reducer enforces AckRetention deterministically
at restore time, on the signal clock. Restoration wires into both
families: checkMetric shares preserveAlertState.
reducer_parity_ack_test.go drives AcknowledgeAlert/UnacknowledgeAlert
and observations through both engines; the expiry scenario backdates the
manager's records and runs the real Cleanup pass. Second consecutive
parity slice with no manager defect found.
Record recovery confirmation and retained re-fire semantics in the alerts contract, and register the reducer unit and manager-parity proofs for both slices.
Change-source: pulse-maintainer
Phase 1 slice 4 of docs/ALERT_ENGINE_EVOLUTION.md. The reducer now keeps
a resolved-occurrence ledger: an activation inside RefireRetention (5
minutes, on the signal clock) consumes the record, restores the original
occurrence's StartedAt, and emits EventRefired — mirroring
consumeRecentlyResolvedForRefireWithPrimaryLock, where a re-fire within
the recently-resolved window reactivates the same occurrence without a
new history entry. Outside the window the re-fire is a fresh occurrence
with a fresh start.
reducer_parity_refire_test.go anchors the simulated epoch at wall time
and backdates recentlyResolved timestamps per step so the manager's
wall-clock retention check follows simulated time; StartTime is asserted
exactly on both engines. First parity slice with no manager defect
found. The wall-vs-evidence clock mix in the manager's retention check
is recorded as a deliberately deferred nuance.
Phase 1 slice 3 of docs/ALERT_ENGINE_EVOLUTION.md.
DiscreteRule.RecoveryConfirmations characterizes the poll-driven offline
composition: a firing incident resolves only after N consecutive
non-matching observations (default 3, storage 2), any matching
observation resets the run, pending still clears on a single
non-matching observation, and disable bypasses the gate — mirroring
clearResourceOfflineAlert + confirmOfflineRecoveryNoLock.
reducer_parity_recovery_test.go drives the manager through the exact
production composition (offline poll: reset recovery counter + evaluate
connectivity spec; healthy poll: clearResourceOfflineAlert) and diffs
the reducer after every step, including first-activation StartTime. The
harness caught the stale first-matched backdating fixed in the previous
commit.
Record discrete confirmation and first-match timing ownership in the alerts contract, move the manager regression into the recognized incident proof, and restore status revision alignment with the stable source of truth.
Change-source: pulse-maintainer
Phase 1 slice 2 of docs/ALERT_ENGINE_EVOLUTION.md. ApplyDiscrete
characterizes the canonical lifecycle path's match-spec semantics for
connectivity / powered-state / discrete-state kinds: N consecutive
matching observations activate with StartTime at the first match, one
non-matching observation clears at this layer, severity follows the
spec while firing, and a disabled spec resolves. The incident sub-key
generalizes from metric name to state key.
reducer_parity_discrete_test.go diffs the reducer against
evaluateCanonicalLifecycleAlert after every step with fully simulated
time on both sides, including alert StartTime on first activations. The
harness again caught a real defect on first run — the confirmation
start-time understatement fixed in the previous commit. Recovery
confirmations and re-fire start restoration are documented as later
slices.
Record canonical metric resolution and reducer parity ownership in the alerts contract, move the stale-resolution regression into the recognized shared proof, and restore deterministic status evidence ordering.
Change-source: pulse-maintainer
Phase 1 of docs/ALERT_ENGINE_EVOLUTION.md. internal/alerts/reducer is a
pure transition core for the metric-threshold family — hysteresis
trigger/clear, sustained-for delay with dip reset, warning/critical
severity derivation with the percentage 99-cap — characterized from
Manager.checkMetric, with time entering only through the signal's
ObservedAt so every sequence is deterministic and replayable.
reducer_parity_test.go drives the live manager and the reducer through
identical observation sequences (simulated time via the established
pending-backdate trick) and fails on any divergence after every step,
with the manager as the reference. The harness proved itself on its
first run by catching the stale-resolve defect fixed in the previous
commit.
Registers coverage gap alert-lifecycle-contract-coverage. Remaining
slices: offline/confirmation families, ack lifecycle, intent
interaction, shadow-mode runtime feed.
Bind the combined delivery-attempt and held-event activity surface to the alerts, API, and frontend contracts. Add recognized state and presentation proofs and record desktop, refresh, and narrow-browser verification for the current feature commit.
Change-source: pulse-maintainer
Record durable persistence as the token-revocation commit boundary across API, security, agent-lifecycle, and storage contracts. Route the exact-removal and rollback proofs through the canonical token lifecycle suite.
Change-source: pulse-maintainer
Keep status.json.updated_at aligned with the stable SOURCE_OF_TRUTH revision while retaining the newly recorded live alert coverage evidence.
Change-source: pulse-maintainer
Bind monitor-side event-log bootstrap to its canonical boundary and exercise the new event infrastructure through the registry-recognized alerts, API, monitoring, and agent-lifecycle proof files.
Change-source: pulse-maintainer
Bind the delivery diagnosis, active-card presentation, and additive event log to their canonical contracts and recognized API/frontend tests. Sync the shipped API reference, record current browser evidence, and restore sorted truthful control-plane status evidence.
Change-source: pulse-maintainer
Adds internal/alerts/eventlog: a SQLite-backed, additive event log that
records lifecycle transitions (resolved, acknowledged, unacknowledged,
escalated, flapping detected) and notification decisions (dispatched,
deferred by quiet hours, suppressed — with the mechanism that held
them). Appends never block alert evaluation: a full buffer drops the
event and counts the drop; a store that fails to open degrades to
recording nothing. 90-day retention, hourly prune.
The manager emits at the existing funnels only — dispatchAlert and the
safe-call resolve/ack/escalate seams — so no lifecycle behavior
changes. Lifecycle "fired" is deliberately not recorded yet: the
active-alert store funnel also runs on persisted restore, so firing
waits for the explicit activation seam in a later phase. The monitoring
bootstrap enables the log per manager; ephemeral managers and tests
record nothing unless they opt in.
GET /api/alerts/events (monitoring:read) reads the log with
alertIdentifier/type/since/limit filters, newest first.
Phase 0 of docs/ALERT_ENGINE_EVOLUTION.md (coverage gap
alert-engine-suppression-observability).
GET /api/alerts/delivery-diagnosis without alertIdentifier now returns
the diagnosis array for every active alert in one manager pass, so list
surfaces do not need a request per alert. Extracts the per-alert
diagnosis into a locked helper shared by both paths; single-alert
behavior is unchanged.
First slice of coverage gap alert-engine-suppression-observability
(docs/ALERT_ENGINE_EVOLUTION.md Phase 0).
The March canonical migration froze the transition core and suppression
path; the post-March regression record (#1682, #1683, #1553, #1693) shows
those layers are where the recurring lifecycle bug classes live.
ALERT_ENGINE_EVOLUTION.md extends the migration end-state one layer
deeper — additive event log (Phase 0), shadow reducer (Phase 1),
family-by-family cutover (Phase 2), declarative rules (Phase 3) — using
the same strangler mechanism. Registers the Phase 0 work as coverage gap
alert-engine-suppression-observability with its record.
Complete the canonical contract fanout for socket-owned delta baselines and independently reconciled logical table rows across alerts, performance, and frontend primitives.
Change-source: pulse-maintainer
Invalidate every keyed projection when an oversized socket snapshot is dropped. REST recovery may hydrate the display, but infrastructure and alert deltas now wait for a socket-owned baseline and request throttled recovery if one is missing.
Change-source: pulse-maintainer
Third increment of the resource-payload-static-metadata gap, the
snapshot-side statics. Measurement first: the mock estate ships only 5
distinct non-default policy postures across 723 resources and 56
distinct templated AI-safe summary strings across the same rows
(~150KB of inline duplication per snapshot), and every canonical alias
list carries the row's own wire id verbatim.
Non-default policies and AI-safe summaries now dedupe through the same
content-addressed catalog contract as capabilities: state-level
policyCatalog and aiSafeSummaryCatalog hold the distinct blobs,
resources carry policyRef / aiSafeSummaryRef, and ingestion expands
refs back to the inline shape (default posture synthesis unchanged for
ref-less rows). The broadcast conversion returns one catalogs struct so
the next deduped field is a loop entry, and catalog changes ride the
same delta frame as the refs that need them. Alias lists stop shipping
the row's own wire id — every identity consumer unions resource.id
beside the alias spread — while the primary id stays so aliases remain
the complete live vocabulary a host is reachable by, per the
established slimming invariant.
Verified against a 1857-resource estate: zero inline policies or
summaries on the wire (743 refs resolving into 1KB + 7KB catalogs),
zero alias lists carrying the wire id, catalog-expanded postures and
summaries reaching the store byte-equivalently, and the estate
rendering clean on desktop and the 36-row phone window.
Remaining gap scope: canonicalIdentity alias/superseded history is
genuine per-resource identity data (~600KB per snapshot) with no safe
derivation shortcut, and the proxmox-facet/platformData mirror
duplication (~200KB) is deliberate canonical-merge fabric; both stay
recorded under resource-payload-static-metadata for a future owner.
Second increment of the resource-payload-static-metadata gap: the full
activeAlerts array (~37-75KB at mock-estate churn) re-shipped whenever
any single alert changed. Alerts now ride the generalized keyed delta
transport: the per-field keying and transition handling in the state
delta engine is a declarative keyedDeltaFields list shared with the
connected-infrastructure projection, and activeAlertsDelta carries
id-keyed merge patches, removed ids, and order. Measured on the mock
estate the per-tick alert payload drops to 184-1981 bytes.
One boundary held deliberately fixed: alert application timing does not
change. The client applies alert deltas to an isolated baseline and
feeds the exact same applyActiveAlerts path immediately on arrival,
input-active or not — alert lifecycle truth never waits for the
operator-input idle gate (alerts subsystem boundary, documented in
6ecdc81d1), and the pending-acknowledgment safeguards see byte-
equivalent records. Alerts handed to the store are cloned so it never
adopts baseline-owned objects. Deltas without a baseline are ignored
and heal on the next full payload; unkeyable entries fall back to
whole-field diffing.
Remaining gap scope unchanged: snapshot-side static metadata
(canonicalIdentity alias history ~609KB per snapshot, and the partially
regressed aiSafeSummary/policy omissions) stays open under
resource-payload-static-metadata.
First increment of the resource-payload-static-metadata gap: the
connected-infrastructure reporting projection was re-shipped whole
(~55-60KB at 50-node scale) on every realtime broadcast even though
measurement showed the only field that ever changes tick-to-tick is
lastSeen, and the client deep-reconciled all 69 rich items per tick —
the dominant reactive cost inside every websocket message after the
resource merge itself.
The projection now rides the same per-client keyed delta engine as
resources. buildClientStateSnapshot keys it by id beside the resource
map, and buildClientStateDelta emits connectedInfrastructureDelta with
id-keyed JSON merge patches, removed ids, and order through one shared
keyed-array builder. Measured on the mock estate the per-tick payload
drops from ~55-60KB to 0.7-4KB of timestamp patches. Entries that
cannot key by id fall the whole field back to plain whole-payload
diffing, and keyed/unkeyed baseline transitions re-ship the full array
once, so the keyed path can never corrupt the projection.
The client mirrors the resource baseline pattern: full payloads
establish an isolated raw baseline, deltas apply as merge patches
through the now-generic keyed apply helper, and the store sync hands
reconcile reference-stable objects for untouched items so a tick
reconciles only what changed instead of deep-walking the projection.
Deltas without a baseline are ignored and heal on the next full
payload; the operator-input deferral gate carries the pending changed
ids across a gesture and flushes them in the same coalesced batch.
The remaining gap scope (snapshot-side static metadata such as
canonicalIdentity alias history, ~609KB per snapshot) stays open under
resource-payload-static-metadata.
Document the alert-side ownership boundary for the shared websocket input gate: resource projections may wait for idle, while active and resolved alert lifecycle updates remain immediate.
Change-source: pulse-maintainer
A tick landing while the operator types in a filter, presses a pointer,
or spins the wheel delayed that interaction's response by the same
200-560ms the scroll path used to pay. The deferral gate now keys on a
shared input-activity window fed by scroll, wheel, pointerdown, and
keydown through one window-level capture registration. Bare pointer
movement deliberately does not gate: a resting hand on the mouse must
not starve data freshness, and the flush recheck still lands deferred
ticks within ~550ms of the last deliberate input.
Verified on the 929-guest mock estate: zero long frames during a 12s
simulated typing burst spanning multiple realtime ticks, one coalesced
flush ~520ms after the last keystroke, and live data continuing to land
on desktop and narrow viewports.
The remaining scroll hitch on large estates was the realtime websocket
tick: every few seconds a delta landed mid-gesture and blocked the main
thread for 200-560ms. The scroll path itself was already clean after the
windowing fixes, so the tick was the one thing still hitting the wheel.
Mid-gesture ticks now cost ~1ms: the resource delta queues unapplied in
arrival order (even the baseline patch walks the whole estate), and the
reporting projection plus the lastUpdate tick token defer latest-wins
alongside it. Holding the token matters as much as holding the data —
it is what unified resource projections and workload remaps key on, so
deferring it keeps every downstream consumer quiescent through the
gesture. Everything drains as one coalesced batch at scroll idle,
through the same deferral set and flush the hidden-tab path uses.
Ordering and lifecycle guards: a non-empty queue keeps queueing so
deltas never apply out of order, hidden-tab ticks drain the queue before
applying in place, a full snapshot supersedes and clears the queue, and
connection resets drop all pending deferral state. Alert application
stays live mid-gesture.
Verified on the 50-node/929-guest mock estate: zero websocket long
frames across 16-20s continuous scrolls (previously one 200-560ms block
per tick), one coalesced flush ~300-450ms after gesture end, and the
Last refresh footer still advancing on desktop and narrow viewports.
This is a scheduling boundary, not a cost reduction: the per-tick
reconciliation cost itself remains owned by the
resource-payload-static-metadata coverage gap.
Wheel scrolling the Proxmox overview on a 929-guest estate hitched for
200-260ms per runway rebalance. Four compounding causes, all fixed:
- The windowing runway policy rebalanced in ~53-row jumps inside a
non-passive wheel listener, so the compositor waited on a multi-frame
remount. Both windowing owners now keep a symmetric runway topped up
in dead-band batches (one bounded mutation frame per ~8 rows of
travel), every windowed-page listener is passive, and only a viewport
that leaves the mounted window entirely re-centers it in one jump.
- The Workloads viewport-sync effect tracked the windowing signals its
own measurement pass moves, so every shift re-ran row measurement
(two forced reflows) and listener binding. The measurement pass is
now untracked, matching the unified-resource sync owner; the platform
and storage windowing owners had the same latent tracking and are
untracked too.
- WorkloadPanel built its guest-id lookup from the windowed slice, so a
shift re-validated every mounted row's memo chain. The lookup now
derives from the full group and only changes when data changes.
- The CPU/memory/disk bar hooks read offsetWidth in onMount, forcing a
full-table reflow per mounted bar during every top-up. Width now
comes solely from the ResizeObserver's initial pre-paint delivery.
Prewarmed-but-hidden platform tabs also ran viewport sync per scroll
event and paid the visible surface's layout flush; hidden surfaces now
skip sync via a shared checkVisibility guard.
Measured on the 50-node/929-guest mock estate at 2560x1240: a 4-row
top-up fell from ~230ms to ~7ms, scroll-jitter oscillation produces
zero frames over 25ms, and sustained fast scrolling holds native frame
cadence with one bounded mutation frame per dead-band. The remaining
scroll-time long tasks are realtime websocket merge ticks, which the
realtime-merge lane already owns.
Contract-Neutral: Storage history behavior is documented in the staged API and storage contracts and covered by staged focused regressions; shared router references do not change agent-lifecycle, performance, or security contracts, and the proof allowlists do not yet name the focused test files.