diff --git a/docs/AlarmProbeFindings.md b/docs/AlarmProbeFindings.md
new file mode 100644
index 0000000..f67e360
--- /dev/null
+++ b/docs/AlarmProbeFindings.md
@@ -0,0 +1,123 @@
+# Alarm Probe Findings
+
+`WnWrapAlarmConsumer` rests on two assumptions that no unit test can settle, because both
+are properties of AVEVA's alarm provider rather than of our code:
+
+1. **GUID identity.** The snapshot diff in `ComputeTransitions` keys on the alarm record's
+ `GUID`. If wnwrap mints a fresh GUID when an alarm changes state, a single
+ `UNACK_ALM → ACK_ALM` transition reads as one alarm disappearing and a different one
+ appearing — a spurious clear plus a spurious raise on every acknowledge.
+2. **`ALARM_RECORDS/@COUNT` semantics.** `IsTruncatedFetch` treats a reply holding exactly
+ `maxAlmCnt` records as truncated, because `GetXmlCurrentAlarms2` exposes no explicit
+ "more available" flag. If the reply's `COUNT` attribute carries the *total* active count
+ rather than the records-in-reply count, truncation detection can become exact instead of
+ conservative, and the bounded staleness `ApplySnapshotUpdate` accepts goes away.
+
+This document records what a live probe run against the dev rig (`DESKTOP-6JL3KKO`,
+2026-08-17) 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, by the 2026-05-01 capture in `AlarmClientDiscovery.md` |
+| GUID stable across `UNACK → ACK`, and across clear-then-re-raise | **Open** |
+| `COUNT` = total active vs records-in-reply under a capped fetch | **Open** |
+
+Both open questions are blocked by the same thing: the rig has no active alarm and cannot
+be driven into one over MXAccess, so there is no alarm instance whose GUID can be followed
+through an acknowledge and no population large enough to overflow a capped fetch.
+
+## Why The Rig Cannot Raise An Alarm
+
+The rig is otherwise healthy, which is what makes the blocker specific rather than a
+general "nothing works":
+
+- `aaEngine`, `alarmmgr`, `NmxSvc`, and `wnwrapServerEx` are all running.
+- `TestArea` (area of `TestMachine_001`…`_003`) and the objects themselves are deployed
+ (`deployed_version` non-null in the `ZB` Galaxy Repository) and on scan — the probe's
+ advised `ScanState` subtags 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)`, and `SetXmlAlarmQuery` all return 0, and
+ `GetXmlCurrentAlarms2` returns 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:
+
+- Re-enable the System Platform script that flips `TestMachine_001.TestAlarm001`
+ (referenced throughout `AlarmClientDiscovery.md`). It writes from inside the engine, so
+ the attribute's security classification does not apply.
+- Drive the write through `AuthenticateUser` + `WriteSecured` with a Galaxy account
+ permitted on that classification. The worker already implements both verbs; the probe
+ used plain `Write`, 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
+
+```
+
+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`.
+
+## Implications
+
+### Transition identity
+
+`ComputeTransitions` keying on GUID is safe for the raise and clear legs, which is the
+evidence `AlarmClientDiscovery.md` already carries. The acknowledge leg — the one where a
+re-minted GUID would corrupt the feed, because an ack is the state change most likely to
+create a new record in a provider that models acknowledgement as a separate event — is
+still assumed rather than observed. Nothing here justifies changing the diff, but the
+assumption should not be described in code as established.
+
+### Truncation detection
+
+`IsTruncatedFetch` stays as written. Tightening it to an exact test requires knowing that
+`COUNT` reports the total, and this run cannot show that. The conservative rule keeps its
+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.
+
+The one substantive correction is to the phrasing rather than the logic. The reply is not
+featureless — it carries a `COUNT` attribute the parser currently ignores. Whether that
+attribute is a usable "more available" signal is unverified, not absent, and the comments
+in `WnWrapAlarmConsumer` now say so.
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
index b9f44f4..041e901 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
@@ -402,11 +402,15 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
Dictionary next = ParseSnapshotXml(xml, out int fetchedRecordCount);
// TRUNCATION CLIFF. GetXmlCurrentAlarms2 caps its reply at maxAlmCnt
- // and gives no "there is more" flag, so a reply holding exactly the
- // cap is indistinguishable from a galaxy that happens to have exactly
- // that many active alarms. Treat the ambiguous case as truncated: the
- // false-positive cost is a snapshot that stays stale for one poll, the
- // false-negative cost is every alarm past the cap reading as cleared.
+ // and gives no *verified* "there is more" flag, so a reply holding
+ // exactly the cap is indistinguishable from a galaxy that happens to
+ // have exactly that many active alarms. Treat the ambiguous case as
+ // truncated: the false-positive cost is a snapshot that stays stale for
+ // one poll, the false-negative cost is every alarm past the cap reading
+ // as cleared. (The reply's ALARM_RECORDS/@COUNT attribute is a
+ // candidate exact signal, but only if it reports the total rather than
+ // the records in the reply — untested on a live rig, see
+ // docs/AlarmProbeFindings.md.)
bool truncated = IsTruncatedFetch(fetchedRecordCount, maxAlarmsPerFetch);
IReadOnlyList transitions;
@@ -436,11 +440,15 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// Decides whether a fetch that came back holding
/// records hit the cap.
/// GetXmlCurrentAlarms2 caps its reply at maxAlmCnt and
- /// offers no "more available" flag, so a reply at exactly the cap is
- /// indistinguishable from a galaxy that happens to hold exactly that
- /// many active alarms; both are treated as truncated. Exposed as
- /// internal static so the rule is unit-testable without the
- /// wnwrapConsumer COM object.
+ /// offers no confirmed "more available" flag, so a reply at exactly the
+ /// cap is indistinguishable from a galaxy that happens to hold exactly
+ /// that many active alarms; both are treated as truncated. The reply
+ /// root carries an ALARM_RECORDS/@COUNT attribute that would make
+ /// the test exact if it reported the total active count rather than the
+ /// records in this reply; a live probe could not discriminate the two
+ /// (see docs/AlarmProbeFindings.md), so the count is deliberately
+ /// not trusted here. Exposed as internal static so the rule is
+ /// unit-testable without the wnwrapConsumer COM object.
///
/// ALARM records the reply carried.
/// The cap that was passed to the fetch.
@@ -599,6 +607,17 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// stays correct for the alarms it does carry — first sightings
/// and state changes are computed from presence alone.
///
+ ///
+ /// Every rule above assumes the GUID identifies the alarm
+ /// instance rather than the state it is in: a re-minted
+ /// GUID would read as the old alarm vanishing and a new one
+ /// appearing, i.e. a spurious Clear plus a spurious Raise. Live
+ /// capture confirms stability across the active→returned leg only
+ /// (docs/AlarmClientDiscovery.md); the acknowledge leg and
+ /// re-raise-after-clear are assumed, not observed, because the dev
+ /// rig's alarm attributes reject unauthenticated writes — see
+ /// docs/AlarmProbeFindings.md.
+ ///
///
/// The snapshot from the previous poll (or empty on first call).
/// The snapshot just parsed from GetXmlCurrentAlarms2.