bug(alarms): an active scripted alarm under-reports as normal after a full-rebuild deploy #487
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Confirmed pre-existing bug, deferred out of v3 Batch 4 because the fix carries real risk (see below). Recorded in
docs/plans/2026-07-16-v3-batch4-PR.mdunder "Documented follow-ups"; filed now so it is tracked rather than living in a plan doc.What happens
The scripted-alarm Part 9 condition node has the same redeploy-reset race the VirtualTag
ReassertValuefix closed:RebuildAddressSpaceclears_alarmConditions, andMaterialiseScriptedAlarmsrecreates each condition fresh/normal.ScriptedAlarmEngine.LoadAsyncreloads from persisted state and yieldsEmissionKind.Nonefor a still-active condition, whichOnEngineEmissiondrops.So an active scripted alarm with static dependencies under-reports as normal after a full-rebuild deploy, until its next real transition. This is the known "snapshot-is-one-shot / restart-to-re-deliver" behaviour, not a Batch 4 regression.
Why it was deferred
The fix is a Core-engine emission-contract change carrying double-alarm-history risk — a naive re-emit would append a duplicate historian/AVEVA row per deploy. That is the alarm analogue of the VirtualTag M1 historian issue.
Proposed shape
A host-driven, node-only
AlarmStateUpdatere-assert inOnAlarmsLoaded, using the same safe post-materialise ordering as the VirtualTag fix, reusing_engine.GetState(alarmId)+LoadedAlarmIds, and never touching thealertstopic (that is what would duplicate history).Verified still unfixed as of
master7808faa7:ReassertValueexists only inVirtualTagActor/VirtualTagHostActor; there is noOnAlarmsLoadedequivalent.Live-verify this one — the VirtualTag analogue was only caught on the rig, and the failure mode (an alarm silently reading normal) is invisible to unit tests that drive a transition.
Fixed on branch
fix/487-scripted-alarm-reassert(commit549c6564). Not merged yet.What landed
ScriptedAlarmHostActor.ReassertConditionNodes, called fromOnAlarmsLoaded, node-only, exactly as this issue proposed. Two deliberate deviations from the sketch:ScriptedAlarmEngine.GetProjections()rather thanGetState(alarmId)+LoadedAlarmIds.GetStatereturns only the Part 9 condition — no severity, no message, no input quality — so re-asserting from it would have clobbered the node's severity and Message.ScriptedAlarmProjectionbundles the condition with the definition's severity, the message template resolved against the live value cache, and the last-observed worst-input quality. It carries noEmissionKind, so it is structurally impossible for the emission machinery (or a future refactor) to mistake it for a transition — that is the double-history guard, made a type property rather than a convention.Also: the timestamp is the condition's own
LastTransitionUtc, not the deploy instant. And no duplicate Part 9 events —WriteAlarmCondition's delta-gate compares against the node's current state, so a re-assert onto a freshly materialised node is a genuine delta (fires once, correctly, since the rebuild reset what clients see) while one onto a surgically-preserved node is suppressed.Correction to this issue's premise
The issue describes the symptom as "an active scripted alarm ... under-reports as normal". On a real node that specific framing does not hold, because
Activeis deliberately not persisted — neitherLocalDbAlarmConditionStateStore'salarm_condition_statetable nor the documented contract indocs/ScriptedAlarms.md§"State persistence" carries it; the engine re-derives Active from the predicate at everyLoadAsync. So a still-active predicate-driven alarm does re-emitActivatedon reload and its node recovers on its own.The bug is real, but it bites the fields that are persisted:
Acked,Confirmed,Shelving,Enabled. The sharpest case, and the one used for the live gate:That is a worse symptom than the one originally described, and it is what the fix is gated against.
Live gate — PASSED
docker-dev rig,
central-2, pre-fix image vs. the rebuilt one. Scripted alarm ongateequip(MAIN), authored through the real/unsequipment Alarms modal; driven to active-then-cleared-unacked; rebuild forced by a node restart (RestoreAppliedre-materialises everything).ConditionRefreshafter rebuildSA-alwaysactiveabsent — Retain=falseUnacknowledged,Retain: True12:55:38= the condition's ownLastTransitionUtc(matchesupdated_at_utcin LocalDb exactly), not the restart instant/alertsduring re-assertThe
/alertsleg was explicitly falsified rather than accepted as a bare absence: immediately afterwards the predicate was flipped back to true and a realACTIVATEDrow appeared on the same open panel, proving the feed was live the whole time.Tests
6 new host tests (
ScriptedAlarmHostActorTests) + 6 new engine tests (ScriptedAlarmProjectionTests). Falsifiability checked by disabling the call: 4 of the 6 host tests go red. The other 2 (A_normal_alarm_is_not_reasserted,A_disabled_plan_is_not_reasserted) are absence guards against an over-broad fix and pass either way by design.Full solution builds clean;
Runtime.Tests504/504,Core.ScriptedAlarms.Tests82/82.Docs:
docs/ScriptedAlarms.md§"Post-(re)load condition-node re-assert (#487)"; the deferral note indocs/plans/2026-07-16-v3-batch4-PR.mdupdated to point at it.Side-discovery filed separately as #504 (
/scriptsis a hard 500 when anyScript.SourceHashis shorter than 12 chars — the docker-dev seed trips it).