Residuals of #760: 1. An ack routed to a disabled / not-yet-spawned driver, or to a stubbed child (driver does not run on this node, or its factory rejected the config), was dropped and left the condition Acknowledged. DriverHostActor now reverts it through the same NativeAlarmProjector.TryRevertAcknowledge path as a refused ack. 2. A native ack arriving while the host is Stale was dropped before the projector recorded it, so it could never be reverted. Stale now marks the whole NativeAlarmAckPreflightView unavailable (SetHostUnavailable), so the node manager refuses every native ack synchronously; BecomeSteady clears it. 3. (keyed sessions without a Galaxy identity) is not addressed. Tests: DriverHostActorNativeAlarmTests' client-ack test and the FOCAS respawn theory now start the host's own driver (AckingAlarmDriver; a factory building a real FocasDriver per spawn over per-instance fake CNCs, with a real config-change respawn) - both fail against the fix in their old shape. New: ack to a disabled driver reverts, ack to a stubbed driver reverts, a Stale host refuses through the pre-check view - all three fail with the fix mutated out. Runtime.Tests 943/0/3. Docs: AlarmTracking.md, .claude/rules/alarms.md. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
6.2 KiB
paths
| paths | |||||
|---|---|---|---|---|---|
|
Scripted alarm ack/shelve and Part 9 condition method behaviour
Moved verbatim from the root
CLAUDE.md(2026-10-13) so it loads only when working in this area. RootCLAUDE.md§ "Subsystem guides" indexes it. Edit it here, not in the root file.
Scripted Alarm Ack/Shelve
Inbound operator acknowledge/shelve for scripted alarms is fully implemented. Two surfaces converge on the alarm-commands DPS topic: (1) OPC UA Part 9 condition methods (Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve) wired in OtOpcUaNodeManager, gated on the AlarmAck role — conferred by the session's API-key principal's NodeAcl grants (or the anonymous principal's), carried by RoleCarryingUserIdentity; LDAP is AdminUI-only; (2) AdminUI /alerts per-row buttons routed through the AdminOperationsActor singleton (gated by DriverOperator policy). ScriptedAlarmHostActor dispatches from the topic to the engine. Client.CLI supports ack, confirm, shelve commands. See docs/ScriptedAlarms.md §"Inbound operator ack/shelve" and docs/AlarmTracking.md.
⚠️ One condition event per accepted method call is OURS, not the SDK's (Gitea #587). The SDK's own
report is gated on AutoReportStateChanges (left false), so before #587 a client Acknowledge emitted no
condition event — the old "SDK auto-fires its own event (E2)" comments were wrong. WireMethodTransitionReporting
wraps every method's SDK handler (Ack/Confirm/AddComment/Enable/Disable/OneShotShelve/TimedShelve/Unshelve) and,
on Good, reports once via ReportConditionEvent — under Lock, so the engine's/driver's re-projection
(routed by a fire-and-forget Tell before the SDK applies state) waits and then hits the delta-gate with no
delta. Things to keep: the SDK's shelve handlers never apply shelving, so the wrapper does; only a real
before/after change is reported (a re-ack is silent and restores the Confirmed/Retain the SDK flips);
MaxTimeShelved is set to int.MaxValue ms at materialise because Create() left it 0 and every TimedShelve
failed. ⚠️ Native alarms support Acknowledge and AddComment only — Confirm, the shelve methods and
Enable/Disable return BadNotSupported (no upstream path, no pair replication, and NativeAlarmProjector's
Confirmed/Unshelved constants own those fields); a native ack re-asserts ConfirmedState = true because the
SDK's ack unconfirms any confirm-capable condition and ScadaBridge maps that field. Pinned over the wire by
ConditionMethodEventTests (exactly-once after a sentinel, folder + i=2253).
A native ack the driver refuses or fails is not left Acknowledged (#760). Two layers: (1) a refusal the
driver can decide without I/O (IAlarmAcknowledgePreflight — Galaxy: anonymous session and no fallback ack
account) fails the Part 9 call BadUserAccessDenied via NativeAlarmAckPreflight, read lock-free from the
NativeAlarmAckPreflightView snapshot DriverHostActor publishes; never do I/O (e.g. a Secrets lookup) in a
pre-check — it runs under the SDK Lock. (2) anything else (keyed session without an identity, MXAccess
non-zero hresult, driver still connecting/reconnecting) comes back as DriverInstanceActor.AlarmAckFailed — or, for a
disabled/unspawned/stubbed driver, is decided in the host itself (#765) — and DriverHostActor re-projects the condition unacknowledged — only while the ack is still pending in
NativeAlarmProjector (any driver transition since then wins). A host that came up Stale cannot route any
native ack, so it marks the whole NativeAlarmAckPreflightView unavailable and every native ack is refused
synchronously until BecomeSteady (#765). A test that routes an ack and expects it to STAND must start the owning
driver (AckingAlarmDriver in Runtime.Tests).
Behaviours that were wrong or missing and are now fixed — keep them: the SDK's OnTimedUnshelve is a
no-op (the engine's AutoUnshelve owns timed-shelve expiry, and UnshelveTime reports the real
remaining time from AlarmConditionSnapshot.UnshelveAtUtc); a commented re-ack is stored as an
AddComment (durable, replicated) and reports one event, an uncommented re-ack stays silent; a native
ack is refused BadInvalidState off-Primary via NativeAlarmAckPrimaryGate (fed by
IDriverPrimaryRoleSource; DriverHostActor publishes its Primary verdict at host start and on member
changes, so the boot window before the first redundancy snapshot is closed — the same fix covers the MQTT
Sparkplug primary-host claim). A native re-ack is still re-sent to the driver (the retry path, by design).
Condition Severity is carried verbatim, clamped to 1..1000 — ToPart9Severity; the node manager used to re-bucket onto
the five EventSeverity points (so an MTConnect Fault read 900, scripted Critical 1000 read 900, native Low 200 read 300).
ScadaBridge only clamps, so exact values pass through. ⚠️ Never select a two-state id with AddSelectClause(type, "X/Id") —
the string becomes ONE QualifiedName named X/Id, which no event carries; the Client.CLI/Client.UI filter did this since
a2883b82 and every alarm read inactive+unacked. Use new SimpleAttributeOperand(type, [X, Id]) (ScadaBridge already does).
Condition children are instance-scoped (#629). Every child of a materialised condition carries
{ConditionId}/{browse path} in the condition's namespace (e.g. …/temp_hi/ShelvingState/UnshelveTime), assigned by
the OtOpcUaNodeManager.New override — the SDK default kept each child on the TYPE's ns=0 id, so a direct Read hit
the type (or BadNodeIdUnknown), translate failed, and children sat outside the ACL (the slash join now puts them
under their condition's scope). ⚠️ The shelve methods resolve by the AlarmConditionType_ShelvingState_* ids only
because New adopts those (a fixed set) as their MethodDeclarationId; calls by the ShelvedStateMachineType_* id
are no longer supported (pinned by a test). Condition teardown must forget the whole PredefinedNodes subtree
(ForgetPredefinedSubtree) or a removed condition's children keep answering.