diff --git a/archreview/plans/R2-07-surgical-pure-adds-plan.md.tasks.json b/archreview/plans/R2-07-surgical-pure-adds-plan.md.tasks.json
index e77928e6..df9b97d7 100644
--- a/archreview/plans/R2-07-surgical-pure-adds-plan.md.tasks.json
+++ b/archreview/plans/R2-07-surgical-pure-adds-plan.md.tasks.json
@@ -79,7 +79,7 @@
{
"id": "T9",
"subject": "Phase 2: OtOpcUaNodeManager.RemoveAlarmConditionNode (+ native-flag cleanup) (high-risk)",
- "status": "pending",
+ "status": "completed",
"blockedBy": [
"T7"
]
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
index a09a7e58..cbcb0de2 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
@@ -1823,7 +1823,28 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
}
///
- public bool RemoveAlarmConditionNode(string alarmNodeId) => false;
+ public bool RemoveAlarmConditionNode(string alarmNodeId)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
+ EnsureAddressSpaceCreated();
+
+ GeneralModelChangeEventState e;
+ lock (Lock)
+ {
+ // Unknown id ⇒ map drift ⇒ false (caller rebuilds). Mirrors the condition teardown inside
+ // RebuildAddressSpace, scoped to this one condition. The equipment folder's event-notifier
+ // promotion is intentionally NOT demoted here — other conditions may still hang under it, and a
+ // lingering notifier folder is harmless (RemoveEquipmentSubtree demotes it when the whole
+ // equipment goes).
+ if (!_alarmConditions.TryRemove(alarmNodeId, out var condition)) return false;
+ condition.Parent?.RemoveChild(condition);
+ PredefinedNodes?.Remove(condition.NodeId);
+ _nativeAlarmNodeIds.Remove(alarmNodeId);
+ e = BuildNodesRemovedModelChange(alarmNodeId);
+ }
+ ReportModelChangeOutsideLock(e, alarmNodeId);
+ return true;
+ }
///
public bool RemoveEquipmentSubtree(string equipmentNodeId) => false;