DriverHostActor no longer acks Applied the moment the revision advances. It
asks OpcUaPublishActor for the outcome (RebuildAddressSpace.ReportOutcome) and
acks Applied+Degraded(reason) when the apply was degraded; a missing outcome
times out (60 s, inside the coordinator's 2 min deadline) to a degraded ack,
never a clean one. Re-dispatching a revision whose apply was degraded or is
still unconfirmed re-applies instead of taking the clean idempotent
short-circuit - that re-apply is also what repairs the degraded nodes.
Tests with a bare publish probe use a clean-outcome auto-pilot probe.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
ApplyAck gains trailing Degraded/DegradedReason (defaulted, so an old node's
DPS JSON or gRPC ack decodes exactly as before). mesh_transport.proto adds
ApplyAckMessage.degraded = 6 / optional degraded_reason = 7. OpcUaPublishActor
replies RebuildCompleted(Degraded, Reason) on every handler path when the
RebuildAddressSpace carries ReportOutcome.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
The AwaitAssert lambdas returned bool into an Action, so they never waited;
the test raced the rediscovery publish (5-22% failure on base too). Make the
asserts real and drive the republish with a short health poll interval.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
S7.Net's own STRING/DT encoders throw ArgumentException; that fell into the
generic write catch (BadCommunicationError + Degraded health). Now surfaced as
S7ValueRangeException like the WSTRING/DATE codecs.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
OtOpcUaNodeManager.MapSeverity re-bucketed every projected severity onto the
SDK's five EventSeverity values (<800 -> 700, else 900), defeating #626's exact
OpcUaSeverity: an MTConnect Fault (800) read and evented as 900; a scripted
alarm graded 1000 published 900 while /alerts said 1000; a native Low (200)
published 300. EventSeverity merely names points on the 1..1000 scale, so the
node now carries the value clamped to 1..1000 (ToPart9Severity), at
materialise, projection and in the delta gate.
MTConnect wire test now asserts Fault ActiveState=true/Severity=800 and the
Normal clear ActiveState=false/Retain=false/Severity=800.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
EventFilter.AddSelectClause(type, "ActiveState/Id") does not split the string;
it selects a single QualifiedName named "ActiveState/Id", which no event carries.
AckedState/Id, ActiveState/Id and EnabledState/Id therefore always arrived null
and Client.CLI 'alarms' (and Client.UI) reported every alarm inactive and
unacknowledged while a direct Read said Active. Pre-existing since a2883b82;
server-side event delivery was correct (proven with two-element select clauses,
incl. after #629). Select them with explicit two-element browse paths.
Over-the-wire test drives OpcUaClientService.SubscribeAlarmsAsync against the
in-process server (native + scripted); ServerEventRig now also selects
ActiveState/Id, Severity and Retain.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
- Unavailable keeps an ACTIVE condition active: quality-only BadNoCommunication; on data
return same statement => quality-only Good, Normal => Clear, different => Retrigger.
- A changed alarm reference set (alarm-bearing delta = full rebuild) clears the whole
last-told record so every active condition is re-asserted; the same set keeps it.
- Quality-only events go through the host's AlarmQualityUpdate path, never a projected
snapshot (which could ack a retained unacked condition or rewrite message/severity).
- Deterministic tie-break between equal-level conditions (ordinal nativeCode, then type).
- Dedup key is level+nativeCode+nativeSeverity+qualifier; a text-only change updates nothing.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
MTConnectConditionAlarmWireTests drives a scripted Agent's Fault/Normal through the real
driver, NativeAlarmProjector and sink to a client event subscription on the raw device
folder. docs/drivers/MTConnect.md replaces 'CONDITION v1: plain String' with the alarm
mapping, opt-in authoring, multi-condition, ack, replay and comms-loss behaviour, and a
live-check recipe; EVENT vocabularies as enumerations marked won't-fix. AlarmTracking.md
lists the MTConnect source and the two new AlarmEventArgs fields.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
New() ran FindNodeInAddressSpace under Lock, taking other node managers' locks (lock-order
risk); the three AlarmConditionType_ShelvingState_* ids are now a static set. Adds wire tests:
TimedShelve/Unshelve by the condition's own instance method ids (object = condition or its
ShelvingState) succeed and report, and a ShelvedStateMachineType_* id call is pinned as
BadMethodInvalid with a positive control.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
The parser keeps a condition's text, nativeCode, nativeSeverity, qualifier and type. A new
MTConnectConditionTracker applies MTConnect's multi-condition semantics (per-nativeCode
activation/clearing across /sample deltas, /current baseline reset, Unavailable), and
MTConnectConditionAlarmMapper turns tracker state into Part 9 transitions: Warning=500,
Fault=800, Normal=clear, Unavailable=inactive + BadNoCommunication; escalation or a new
code retriggers. The driver implements IAlarmSource keyed by the alarm tag's RawPath,
diffs against what the server was actually told (only when a handler is attached), so a
reconnect/re-baseline never re-raises an unchanged condition and a missed transition is
delivered on the next subscribe. Ack is a no-op success. Existing CONDITION String tags
are unchanged; a tag becomes an alarm only via the standard TagConfig 'alarm' object.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
AlarmEventArgs gains optional OpcUaSeverity (supersedes the four-bucket mapping, clamped
1..1000) and QualityStatusCode (projected onto the condition's Quality). An Unspecified-kind
event carrying a quality is a quality-only annotation: the host updates the condition but
publishes no alerts row. Both default null, so every existing native source is unchanged.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Create(assignNodeIds: true) asked CustomNodeManager2.New, whose default returns the
node's current id, so every condition child kept the type's ns=0 instance-declaration id
(or an SDK-generated ns=0 number): translate of a child path returned BadNoMatch and a
Read of a browsed child id was BadNodeIdUnknown or served from the TYPE (UnshelveTime 0
on a condition timed-shelved for a minute), bypassing the data-plane ACL.
OtOpcUaNodeManager.New now derives {conditionId}/{browsePath} in the condition's
namespace, so the ACL scope of a child is its condition's scope plus the path. The
ShelvingState methods adopt their original AlarmConditionType_ShelvingState_* id as
MethodDeclarationId so clients calling them by that id still resolve. Condition removal
and rebuild forget the whole PredefinedNodes subtree so a removed condition's children
stop answering.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe