Follow-up to 90331b6. Three comment/prose corrections, no behaviour change.
AcknowledgeByName's comment still said the 6-arg overload "works and reaches the
alarm-history path correctly", which the same commit's own findings contradict in
three other places. It now says what was observed: rc=0 means accepted, not
applied — the 2026-08-18 probe acked a live alarm six ways and the snapshot, the
OPERATOR_NAME field, and the extension's .Acked attribute all stayed put. The -55
tracks the consumer, not the overload. Subscribe's comment gets the same
treatment: "lets AlarmAckByName succeed" becomes "return rc=0".
AlarmProbeFindings.md said the re-raise arrives as "a separate record" alongside
the returned one, which reads as coexistence and is wrong. The snapshot carries
one record per tag: the cap=1024 replies bracketing the re-raise are both
elementCount=3 (one per TestMachine_00{1,2,3}) at an identical 1613 bytes, and the
old GUID is absent from the later one. The re-raise replaces the record, so a
single poll spanning it sees the Clear and the Raise together.
Worker diff verified strictly comment-only; builds x86 on windev, 0W/0E.
15 KiB
Alarm Probe Findings
WnWrapAlarmConsumer rested on two assumptions that no unit test can settle, because both
are properties of AVEVA's alarm provider rather than of our code. The second is now settled
and one half of the first is; the questions are stated here as they were asked:
- GUID identity. The snapshot diff in
ComputeTransitionskeys on the alarm record'sGUID. If wnwrap mints a fresh GUID when an alarm changes state, a singleUNACK_ALM → ACK_ALMtransition reads as one alarm disappearing and a different one appearing — a spurious clear plus a spurious raise on every acknowledge. ALARM_RECORDS/@COUNTsemantics.IsTruncatedFetchtreats a reply holding exactlymaxAlmCntrecords as truncated, becauseGetXmlCurrentAlarms2exposes no explicit "more available" flag. If the reply'sCOUNTattribute carries the total active count rather than the records-in-reply count, truncation detection can become exact instead of conservative, and the bounded stalenessApplySnapshotUpdateaccepts goes away.
This document records what live probe runs against the dev rig (DESKTOP-6JL3KKO,
2026-08-17 and 2026-08-18) could and could not establish, so the next attempt starts from
the blocker rather than rediscovering it.
Outcome
| Question | Status |
|---|---|
GUID stable across polls and ALM → RTN |
Answered — yes (2026-05-01 capture in AlarmClientDiscovery.md, reconfirmed 2026-08-18) |
| GUID stable across clear-then-re-raise | Answered 2026-08-18 — no, a re-raise mints a new GUID |
GUID stable across UNACK → ACK |
Open — the rig cannot be driven into an acknowledged state at all |
COUNT = total active vs records-in-reply under a capped fetch |
Answered 2026-08-18 — records in the reply |
The 2026-08-17 run below is kept because it is the record of the wrong-verb blocker. The
2026-08-18 run cleared that blocker with AuthenticateUser + WriteSecured and answered
two of the three questions; the acknowledge leg is now blocked on something narrower and
different, described in "Second attempt".
First attempt (2026-08-17): plain Write
Why The Rig Could Not Raise An Alarm
The rig is otherwise healthy, which is what makes the blocker specific rather than a general "nothing works":
aaEngine,alarmmgr,NmxSvc, andwnwrapServerExare all running.TestArea(area ofTestMachine_001…_003) and the objects themselves are deployed (deployed_versionnon-null in theZBGalaxy Repository) and on scan — the probe's advisedScanStatesubtags report true, and every advised alarm attribute delivers an initial value, so the MXAccess read path is live.- The wnwrap consumer subscribes cleanly:
InitializeConsumer,RegisterConsumer,Subscribe(\\DESKTOP-6JL3KKO\Galaxy!TestArea), andSetXmlAlarmQueryall return 0, andGetXmlCurrentAlarms2returns well-formed XML on every poll.
What fails is the write that would set the alarm condition. Every Write to the alarm
UDAs completes with a security failure:
WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=0 category=SecurityError detectedBy=RespondingAutomationObject detail=1008 text=]
The status comes back from the responding automation object, not from the proxy, so the
request reaches the engine and the engine refuses it. The advised value confirms the
refusal is total rather than transient: neither the alarm UDA nor its .InAlarm /.Acked
subtags report any change after a write attempt, across six write attempts in one session
(raise, clear, re-raise, cleanup). The attributes carry a security classification that a
plain Write cannot satisfy.
The 2026-05-01 capture that answered the ALM → RTN leg did not hit this, because the
alarm condition was driven from inside the engine by a System Platform script rather than
from an external MXAccess client. That script is not running now, and the values sat idle
for the whole probe session.
Unblocking
Any one of these makes both questions answerable, in rough order of cost. The second one is what the 2026-08-18 run did, and it worked:
- Re-enable the System Platform script that flips
TestMachine_001.TestAlarm001(referenced throughoutAlarmClientDiscovery.md). It writes from inside the engine, so the attribute's security classification does not apply. - Drive the write through
AuthenticateUser+WriteSecuredwith a Galaxy account permitted on that classification. The worker already implements both verbs; the probe used plainWrite, which is the wrong verb for a secured attribute. - Reclassify the test UDAs to free access in the IDE and redeploy
TestMachine_001…_003.
Three separate objects are wired to the same alarm UDA name, so once writes land, a
maxAlmCnt of 1 or 2 forces truncation against three active alarms and answers the COUNT
question in the same run.
Evidence
Snapshot payload, identical at every cap (1, 2, and 1024) and at every poll across the ~100-second session:
<?xml version="1.0"?><ALARM_RECORDS COUNT="0"></ALARM_RECORDS>
Two things follow from the empty case alone. COUNT is present on the root element in
every reply, so the attribute exists as a candidate signal rather than something wnwrap
omits. And COUNT agrees with the element count here — but trivially, since both are zero,
which is exactly the case that cannot discriminate the two hypotheses.
The probe used for the run was a throwaway file in the windev CI clone
(C:\build\mxaccessgw-ci), deleted afterwards; the clone is back to a clean tree at
origin/main. Nothing in this repository changed to run it. The reusable, Skip-gated
harness it was modelled on is
src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs.
Second attempt (2026-08-18): secured-write verb
The findings above named the fix in their own "Unblocking" list, and it holds:
AuthenticateUser + WriteSecured raises the alarms that plain Write could not touch.
The rig's alarm attributes are not unreachable — they are secured, and the 2026-08-17 probe
used the wrong verb.
Method
Three throwaway probes in the windev CI clone (C:\build\mxaccessgw-ci, detached at
ab3ff16), each an x86 net48 xUnit fact running on a dedicated STA with a
PeekMessage/DispatchMessage pump so MXAccess events deliver:
- secured write + both GUID legs + the capped-fetch question,
- the acknowledge surface, exhaustively,
- the acknowledge question re-asked through an independent observer.
All three used LMXProxyServerClass directly for the write path and a
wwAlarmConsumerClass pair — a reader with SetXmlAlarmQuery applied and an ack-only
consumer without it — mirroring WnWrapAlarmConsumer's own two-consumer arrangement. The
subscription was \\DESKTOP-6JL3KKO\Galaxy!TestArea, matching the 2026-08-17 run.
MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER / _PASSWORD are set at neither machine nor
user scope on the box, so the probes used the default rig identity, Administrator with an
empty password. All three files were deleted afterwards and the clone is a clean tree.
The secured write lands
Same session, same item, one after the other — the control reproduces 2026-08-17 exactly and the secured verb succeeds:
Write 'TestMachine_001.TestAlarm001' <- True
WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=0 category=SecurityError detectedBy=RespondingAutomationObject detail=1008 text=]
observed 'TestMachine_001.TestAlarm001' = False
AuthenticateUser -> userId=1
WriteSecured 'TestMachine_001.TestAlarm001' <- True currentUserId=1 verifierUserId=0
DATA-CHANGE hItem=1 value=True quality=192
WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=-1 category=Ok detectedBy=RespondingAutomationObject detail=0 text=]
observed 'TestMachine_001.TestAlarm001' = True
AuthenticateUser("Administrator", "") resolves to user id 1, matching the value
WorkerLiveMxAccessSmokeTests records. No verifier is needed: verifierUserId=0 is
accepted. All six subsequent raise/clear writes across the three objects behaved the same
way, so the path is reliable rather than a one-off.
Q2 — ALARM_RECORDS/@COUNT reports the reply, not the total
With all three TestMachine_00{1,2,3}.TestAlarm001 alarms active, the same subscription
fetched at three caps back to back:
FETCH three-active cap=1024 ALARM_RECORDS/@COUNT=3 elementCount=3
FETCH three-active cap=1 ALARM_RECORDS/@COUNT=1 elementCount=1
FETCH three-active cap=2 ALARM_RECORDS/@COUNT=2 elementCount=2
@COUNT tracked the cap, not the population. It equals the element count in every reply,
including the two that are provably truncated — the galaxy held three active alarms while
@COUNT read 1 and 2. The attribute therefore carries no "more available" information, and
the hypothesis that it might report the total is refuted rather than merely untested.
IsTruncatedFetch stays exactly as written, and this is now settled rather than deferred:
there is no exact truncation signal to switch to. Tightening it is not a future task.
The conservative rule keeps its original justification — at the cap, treating a complete
fetch as truncated costs one poll of staleness, while treating a truncated fetch as
complete broadcasts clears for every alarm past the cap.
Q1 — clear-then-re-raise mints a new GUID
TestMachine_001.TestAlarm001 was raised, cleared, and re-raised through WriteSecured,
with a full snapshot fetch after each leg:
after-raise TAG=TestMachine_001.TestAlarm001 GUID=72B84A6E7BA74D42B611EB5393626F79 STATE=UNACK_ALM VALUE=true
after-clear-001 TAG=TestMachine_001.TestAlarm001 GUID=72B84A6E7BA74D42B611EB5393626F79 STATE=UNACK_RTN VALUE=false
after-reraise TAG=TestMachine_001.TestAlarm001 GUID=2394FEAA94774154AAA97C3AC29CA51E STATE=UNACK_ALM VALUE=true
Two facts, one confirming and one new. The ALM → RTN leg holds its GUID, reconfirming the
2026-05-01 capture against a write-driven rather than script-driven transition. The re-raise
does not: the record comes back under a GUID that has never been seen before.
The two records do not coexist — the snapshot carries one record per tag, and the
re-raise replaces the returned record rather than joining it. Both fetches above are the
full cap=1024 reply, byte-identical in length (1613) at @COUNT=3/elementCount=3, the
three records being one per TestMachine_00{1,2,3}; 72B84A6E… is simply absent from the
later one. So a single poll spanning the re-raise sees the old GUID disappear and the new
one appear together.
That is the correct reading for ComputeTransitions rather than a problem for it. The old
GUID leaving the active set is a real Clear and the new GUID is a real Raise, because they
are two alarm instances. The diff needed no change.
Q1 — the acknowledge leg is still unobserved, for a new reason
The blocker moved. The rig can now be driven into an alarm; it cannot be driven out of
UNACK_ALM. Every acknowledge surface wwAlarmConsumerClass exposes was tried against a
freshly raised alarm, each followed by 16 seconds of polling:
| Attempt | Result |
|---|---|
AlarmAckByName 6-arg on the ack-only consumer, node = machine |
rc=0, state unchanged |
| same, node empty | rc=0, state unchanged |
same, name as Galaxy!TestArea.TestMachine_001.TestAlarm001 |
rc=0, state unchanged |
same, operator name Administrator |
rc=0, state unchanged |
AlarmAckByName 6-arg on the reader consumer (SetXmlAlarmQuery applied) |
rc=-55, state unchanged |
AlarmAckByName 8-arg (v2) on the ack-only consumer |
rc=0, state unchanged |
The rc=-55 on the SetXmlAlarmQuery-applied consumer reproduces the 2026-05-01 finding
that motivated the two-consumer split, so the ack-only consumer was correctly provisioned;
the rc=0 returns are the ones that go nowhere.
A third probe removed the last doubt by watching the alarm extension's own .Acked
attribute over MXAccess — an observer entirely independent of the wnwrap snapshot:
VALUES after-raise: 'TestMachine_001.TestAlarm001.Acked' = False
AlarmAckByName -> rc=0
VALUES after-wnwrap-ack: 'TestMachine_001.TestAlarm001.Acked' = False
XML after-wnwrap-ack: STATE=UNACK_ALM OPRNAME= OPRNODE=
Nothing moves: not the extension attribute, not the snapshot STATE, not OPERATOR_NAME.
rc=0 from wnwrap means the call was accepted, not that an acknowledgement was applied.
Acknowledging by writing the extension attribute directly is not an alternative — the attribute is not writable, and says so with an operational rather than a security failure, for both verbs:
WriteSecured 'TestMachine_001.TestAlarm001.Acked' <- true
WRITE-COMPLETE statuses=[success=0 category=MxCategoryOperationalError detectedBy=MxSourceRespondingAutomationObject detail=1007]
Write 'TestMachine_001.TestAlarm001.Acked' <- true
WRITE-COMPLETE statuses=[success=0 category=MxCategoryOperationalError detectedBy=MxSourceRespondingAutomationObject detail=1007]
detail=1007 from the responding automation object, unchanged by authentication, is a
read-only attribute rather than a permission refusal — the 1008 SecurityError the alarm
UDA itself returns is what a permission refusal looks like on this rig. So the ack has no
MXAccess-side entry point and the wnwrap-side entry point is inert.
Remaining unblock paths for the acknowledge leg
- Acknowledge from the System Platform side — the IDE's alarm client, InTouch, or an
ArchestrA graphic bound to the alarm — and watch the snapshot from a running probe. This
proves whether wnwrap's
STATEever reportsACK_ALMat all, which is the actual question; the ack API being inert may be a wnwrap defect layered on top of a snapshot that would report the state correctly. - Check whether
alarmmgron this rig is configured with an alarm-acknowledgement security requirement that the wnwrap consumer, which passes an operator name string and no authenticated identity, cannot meet. If so, ack over wnwrap is not merely untested here but unavailable by configuration, and the gateway'sAcknowledgeByNamepath needs the same treatment on any customer galaxy configured that way. - If neither lands, the acknowledge leg stays assumed. It is worth restating that this is a
documentation gap, not a correctness one: a re-minted GUID on acknowledge would produce a
spurious Clear plus a spurious Raise, which is the same shape the now-observed re-raise
behaviour produces and which
ComputeTransitionsalready handles as two instances.
Rig state left behind
The three TestMachine_00{1,2,3}.TestAlarm001 UDAs are back to false and their
.InAlarm subtags read false, but each leaves a UNACK_RTN record in the wnwrap
snapshot, since nothing can acknowledge them away. SnapshotActiveAlarms counts only
UNACK_ALM and ACK_ALM as active, so these are inert for the gateway; they will clear on
the next alarmmgr restart.