fix(runtime): prune the native-alarm projector on deploy instead of clearing it (#641)

DriverHostActor cleared NativeAlarmProjector's per-condition prior state on
EVERY apply, so after any deploy a driver Acknowledge of a still-active alarm
projected Active=false (writing it inactive) and a Clear of an unacknowledged
alarm projected Acked=true (silently acknowledging it). The projector is now
pruned to the conditions the new composition still carries (RetainOnly).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 17:05:42 -04:00
parent ddd7678269
commit 11f13f6326
4 changed files with 113 additions and 6 deletions
@@ -40,9 +40,25 @@ public sealed class NativeAlarmProjector
StatusCode: e.QualityStatusCode ?? OpcUaStatus.Good);
}
/// <summary>Clears all tracked per-node state (call on address-space rebuild).</summary>
/// <summary>Clears all tracked per-node state.</summary>
public void Clear() => _prior.Clear();
/// <summary>
/// Gitea #641 — drop the tracked state of every condition NOT in <paramref name="deployedNodeIds"/>, keeping
/// the rest. Called on every apply instead of <see cref="Clear"/>: a condition that is still deployed keeps
/// its state across the deploy (the node manager carries the node's state across a rebuild too), so the
/// next Acknowledge does not project <c>Active = false</c> onto an active alarm and the next Clear does not
/// project <c>Acked = true</c> onto an unacknowledged one. A removed condition is forgotten, so a later
/// re-add starts from the default.
/// </summary>
/// <param name="deployedNodeIds">The condition node ids the new composition still materialises.</param>
public void RetainOnly(IReadOnlySet<string> deployedNodeIds)
{
ArgumentNullException.ThrowIfNull(deployedNodeIds);
foreach (var nodeId in _prior.Keys.Where(k => !deployedNodeIds.Contains(k)).ToList())
_prior.Remove(nodeId);
}
private static ushort MapSeverity(AlarmSeverity s) => s switch
{
AlarmSeverity.Low => 200,