diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md
new file mode 100644
index 0000000..ef0c95a
--- /dev/null
+++ b/docs/AlarmClientDiscovery.md
@@ -0,0 +1,121 @@
+# aaAlarmManagedClient discovery — public surface, 2026-05-01
+
+Result of running
+`MxGateway.Worker.Tests.AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface`
+against the deployed AVEVA assembly:
+
+- File:
+ `C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll`
+- Assembly identity: `aaAlarmManagedClient, Version=1.0.7368.41290,
+ Culture=neutral, PublicKeyToken=7ebd82b507d9e10c`
+
+## Public types
+
+- `aaAlarmManagedClient.AlarmClient` (class)
+- `aaAlarmManagedClient.PriorityData` (class)
+
+That's the entire exported surface — two types, no interfaces, no
+delegates.
+
+## `AlarmClient` events
+
+**None.** The class has no public events at all. The reflection probe's
+`GetEvents(BindingFlags.Public | Instance | Static)` returned an empty
+list.
+
+## `AlarmClient` methods (relevant subset)
+
+- **Lifecycle:**
+ `RegisterConsumer(int hWnd, string szProductName, string
+ szApplicationName, string szVersion, bool bRetainHiddenAlarms) → int`,
+ `DeregisterConsumer() → int`,
+ `InitializeConsumer(string szApplicationName) → int`,
+ `UninitializeConsumer() → int`,
+ `Dispose()`.
+- **Subscription:**
+ `Subscribe(string szSubscription, short wFromPri, short wToPri,
+ eQueryType QueryType, eSortFlags SortFlags, eAlarmFilterState
+ FilterMask, eAlarmFilterState FilterSpecification) → int`.
+- **Change enumeration (pull on poke):**
+ `GetStatistics(out int lPercentQuery, out int lTotalAlarms, out int
+ lActiveAlarms, out int lSuppressedAlarms, out int lSuppressedFilters,
+ out int lNewAlarms, out int lChangesCount, out int[] ChangeCodes,
+ out int[] ChangePos, out int[] hAlarm) → int`.
+- **Record fetch:**
+ `GetAlarmExtendedRec(int lIndex, out AlarmRecord almRec) → int`,
+ `GetAlarmExtendedRec2(...)`,
+ `GetHighPriAlarm(out AlarmRecord almRec) → int`.
+- **Selection model** (used by ack-selected-* family):
+ `DeselectAll`, `SelectAlaramEntry(short select, int from, int to)`,
+ `SelectByGUID(Guid)`, `SelectAlarmCount(int from, int to)`.
+- **Acknowledge:**
+ `AlarmAckByGUID(Guid alarmGuid, string ackComment, string ackOprName,
+ string ackOprNode, string ackOprDomain, string ackOprFullName) → int`
+ is the per-alarm full-fidelity native ack.
+ `AlarmAckSelected(string ackComment, string ackOprName, string
+ ackOprNode, string ackOprDomain, string ackOprFullName) → int`
+ acks whatever the selection model currently has selected.
+ Several `AckSelected*Group/Tag/Priority/All/Visible*Alarms_Ex(...)`
+ variants exist for bulk ack scoped to a group / tag / priority range.
+- **Suppress / shelve:** `SupressSelected*` and `ShelveSelected*`
+ families plus `DoAlarmShelveAction(...)`. Out of scope for the v1
+ alarm path.
+- **Snapshot/filter** (`SF*` prefix): `SFSetSortA / SFSetFilterA /
+ SFCreateSnapshot / SFGetListCount / SFDeleteSnapshot / SFRefreshAlarm /
+ SFGetStatistics`. Snapshot-style query API, distinct from the
+ consumer-subscription path. Not currently used.
+
+## What this means
+
+The architecture comment on
+`src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs` (PR A.5) is
+**wrong against this deployed assembly**:
+
+> "The AVEVA alarm-manager surface (`IAlarmMgrDataProvider`) exposes
+> the events we need as plain .NET events — no Windows message pump
+> required."
+
+There is no managed event surface. `AlarmClient.RegisterConsumer`
+takes an `hWnd` because **WM_APP messaging is the actual notification
+mechanism**: AVEVA's alarm provider WM_APP-pokes the registered window,
+and the consumer is expected to call `GetStatistics` on each poke to
+pull `ChangeCodes` / `ChangePos` / `hAlarm` arrays, then
+`GetAlarmExtendedRec(pos, …)` per index to fetch each changed record.
+
+`AlarmClientConsumer.AlarmRecordReceived` has no production callers as
+a result — `RaiseAlarmRecordReceived` is `internal` for tests and
+never gets invoked at runtime. Until A.2 lands a WM_APP pump,
+`MX_EVENT_FAMILY_ON_ALARM_TRANSITION` cannot carry events.
+
+## Implications for A.2
+
+The original plan banner was right: A.2 needs a hidden message-only
+window inside the worker's STA whose hWnd is passed to
+`RegisterConsumer`, with a `WindowProc` that intercepts the AVEVA
+WM_APP message and routes change-enumeration into
+`MxAccessAlarmEventSink.EnqueueTransition`. Open questions before
+implementation:
+
+1. **WM_APP message ID.** Not in the public surface — needs either
+ AVEVA's C++ Toolkit reference (canonical doc per `gateway.md`) or
+ a runtime probe (subclass a window, log every WM arriving while a
+ live alarm is fired, identify the AVEVA one). Worth doing once on
+ the dev rig and checking the result in.
+2. **`wParam` / `lParam` semantics.** Probably none — the pattern is
+ "got poked; pull state via `GetStatistics`." Confirm during the
+ probe.
+3. **Threading.** AVEVA almost certainly delivers the WM on the
+ thread that owns the window. The worker's STA is the natural
+ home; the existing `StaRuntime` already runs a pump there. If
+ AVEVA assumes a UI thread (MTA-incompatible call paths inside
+ `GetStatistics`), the alarm path may need its own STA.
+4. **Subscription scope.** `AlarmClient.Subscribe(szSubscription,
+ …)` takes an AVEVA-syntax string for the alarm provider (e.g.
+ `"\Galaxy!"` for a whole Galaxy or `"\Galaxy!Group.Tag"` for a
+ subset). The configured Galaxy name is already known to the
+ worker via the existing data session — reuse it.
+
+PR A.5's `Subscribe` / `AcknowledgeByGuid` / `SnapshotActiveAlarms`
+are correct — they're pull-style and don't depend on the missing
+event surface. The event-subscription wiring is what has to be
+replaced.
diff --git a/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs b/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs
index a78849e..ae878fa 100644
--- a/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs
+++ b/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs
@@ -13,22 +13,39 @@ namespace MxGateway.Worker.MxAccess;
///
///
///
-/// The AVEVA alarm-manager surface (IAlarmMgrDataProvider)
-/// exposes the events we need as plain .NET events — no Windows
-/// message pump required. The worker keeps its STA thread for
-/// MxAccess COM but the alarm-client callbacks arrive on the
-/// AVEVA managed-client's internal callback thread.
+/// ⚠ Architecture finding (2026-05-01 reflection probe —
+/// see docs/AlarmClientDiscovery.md): contrary to the
+/// original PR A.5 design, aaAlarmManagedClient.AlarmClient
+/// exposes zero public events on the deployed assembly
+/// (aaAlarmManagedClient.dll v1.0.7368.41290). There is no
+/// managed event surface. RegisterConsumer(hWnd, …) takes a
+/// window handle because the actual notification mechanism is
+/// WM_APP-pump messaging — AVEVA's alarm provider WM_APP-pokes the
+/// registered window, and the consumer pulls the change set via
+/// GetStatistics + GetAlarmExtendedRec on each poke.
///
///
-/// The constructor parameters that
-/// takes (hWnd, product / application / version names,
-/// retain-hidden flag) are pinned to safe defaults; the live
-/// hWnd is intentionally IntPtr.Zero because we use
-/// the managed-event surface, not the WM_APP pump. Verify
-/// on dev rig that RegisterConsumer with
-/// hWnd=0 still wires the managed event handlers; if it
-/// requires a real hWnd, the worker creates a hidden message-only
-/// window and passes that handle here.
+/// As a result, has no production
+/// caller — is invoked only
+/// from tests. currently calls
+/// RegisterConsumer(hWnd: 0, …) + Subscribe(…) and
+/// returns OK, but no notifications will arrive at runtime because
+/// no window is attached. Until A.2's WM_APP pump lands, the
+/// gateway's MX_EVENT_FAMILY_ON_ALARM_TRANSITION family
+/// cannot carry any events.
+///
+///
+/// as written is still a load-bearing call
+/// the WM_APP path needs (it tells the alarm provider which
+/// subscription expression to scope notifications to). The wiring
+/// that has to change is the notification-receive side: replace the
+/// hWnd: 0 default with a real hidden message-only window
+/// hWnd owned by the worker's STA, and add a WindowProc that
+/// routes the AVEVA WM_APP message into a change-pull path that
+/// ultimately invokes .
+/// and
+/// are pull-style and don't depend on the event surface — they're
+/// correct as is.
///
///
public sealed class AlarmClientConsumer : IMxAccessAlarmConsumer
@@ -63,10 +80,12 @@ public sealed class AlarmClientConsumer : IMxAccessAlarmConsumer
lock (subscribeLock)
{
- // hWnd=0: AVEVA's managed event surface routes through the
- // GetAlarmChangesCompleted .NET event, not a window-message pump.
- // Verify on dev rig that 0 is accepted; if not, supply a hidden
- // message-only window's handle here.
+ // hWnd=0: placeholder — the AVEVA alarm provider notifies via
+ // WM_APP messages, not via a managed-client event surface (see
+ // docs/AlarmClientDiscovery.md, 2026-05-01 reflection probe).
+ // RegisterConsumer accepts hWnd=0 but no notifications will reach
+ // this consumer until the WM_APP pump lands as part of A.2 and
+ // a real message-only window's handle is supplied here.
int registerResult = client.RegisterConsumer(
hWnd: 0,
szProductName: DefaultProductName,
diff --git a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs
index 5b1f430..450067a 100644
--- a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs
+++ b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs
@@ -11,19 +11,35 @@ namespace MxGateway.Worker.MxAccess;
///
///
///
-/// Architecture (pinned 2026-04-30): the worker hosts
+/// Architecture (revised 2026-05-01 — see
+/// docs/AlarmClientDiscovery.md): the worker hosts
/// aaAlarmManagedClient.AlarmClient alongside the existing
-/// ArchestrA.MxAccess COM consumer. Both are x86 .NET Framework
-/// 4.8 — the worker's existing runtime — and both use the same Windows
-/// STA + WM_APP message pump. The MxAccess COM Toolkit at
+/// ArchestrA.MxAccess COM consumer. Both are x86 .NET
+/// Framework 4.8. The MxAccess COM Toolkit at
/// C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll
/// exposes no alarm events; the alarm provider lives in a separate
/// AVEVA service that aaAlarmManagedClient subscribes to.
///
///
+/// Notification mechanism: WM_APP pump. A reflection
+/// probe of aaAlarmManagedClient.dll (v1.0.7368.41290) on
+/// 2026-05-01 confirmed the public AlarmClient class has zero
+/// public events. The original PR A.5 design (managed-event surface,
+/// no message pump) is incorrect against this assembly. AVEVA's
+/// alarm provider WM_APP-pokes a window registered through
+/// RegisterConsumer(hWnd, …); the consumer pulls the change
+/// set via GetStatistics + GetAlarmExtendedRec on each
+/// poke. PR A.5's still owns the
+/// handle and the
+/// /
+/// pull-style calls; only
+/// the receive path is wrong.
+///
+///
/// Discovered API surface (see
/// AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface in
-/// MxGateway.Worker.Tests — Skip-gated reflection probe):
+/// MxGateway.Worker.Tests — Skip-gated reflection probe; full
+/// output captured in docs/AlarmClientDiscovery.md):
///
///
/// - RegisterConsumer(hWnd, productName, applicationName, version, retainHidden) — registers a Windows-message-pump consumer; the AVEVA alarm service WM_APP-pokes the hWnd when alarms change.
@@ -33,20 +49,25 @@ namespace MxGateway.Worker.MxAccess;
/// - AlarmAckByGUID(alarmGuid, ackComment, oprName, oprNode, oprDomain, oprFullName) — full-fidelity native Acknowledge: comment + four operator-identity fields are atomic with the ack transition.
///
///
-/// Wiring plan (subsequent PRs):
+/// Open questions before A.2 implementation
+/// (see docs/AlarmClientDiscovery.md "Implications for A.2"):
///
///
-/// - Worker session-startup wires AlarmClient.RegisterConsumer against the worker's existing STA hWnd; Subscribe with the Galaxy provider name + a permissive priority/filter range.
-/// - The STA's WM_APP handler routes alarm-changed messages into ; the message ID is established at runtime via the consumer's reported handler (verify on dev rig).
-/// - Gateway-side AcknowledgeAlarm RPC translates to a worker command that calls AlarmClient.AlarmAckByGUID with the OPC UA operator's resolved identity — replaces the worker-pending diagnostic from PR A.3.
+/// - WM_APP message ID — not in the public surface, needs AVEVA C++ Toolkit reference or a runtime probe.
+/// - wParam / lParam semantics — likely none (the pattern is "got poked → pull state via GetStatistics"), but confirm during the probe.
+/// - STA / threading affinity for the message-only window — likely the worker's existing STA, but if AVEVA assumes UI-thread inside GetStatistics the alarm path may need its own STA.
+/// - Subscription scope — reuse the configured Galaxy name from the data session.
///
///
-/// Until those PRs land, is a no-op. The worker
-/// continues to function for data subscriptions, and the gateway's
-/// family is reserved on
-/// the wire but never emitted. lmxopcua-side AlarmConditionService
-/// keeps the sub-attribute synthesis active and continues to surface
-/// alarms to OPC UA Part 9 clients in the meantime.
+/// Until A.2 lands a hidden message-only window + WindowProc that
+/// routes WM_APP into ,
+/// is a no-op. The worker continues to function
+/// for data subscriptions, and the gateway's
+/// family is reserved
+/// on the wire but never emitted. lmxopcua-side
+/// AlarmConditionService keeps the sub-attribute synthesis
+/// active and continues to surface alarms to OPC UA Part 9 clients
+/// in the meantime.
///
///
public sealed class MxAccessAlarmEventSink : IMxAccessEventSink