diff --git a/docs/DesignDecisions.md b/docs/DesignDecisions.md
index f3fd10b..32055df 100644
--- a/docs/DesignDecisions.md
+++ b/docs/DesignDecisions.md
@@ -203,7 +203,7 @@ Consequences, and how this sits with the existing failover/reconcile design:
separately — see the next decision. A galaxy that truncates persistently
is a configuration problem: raise `MxGateway:Alarms:MaxAlarmsPerFetch`.
-### Alarms — truncation is reported per record on the public snapshot stream
+### Alarms — truncation is reported per record on the snapshot stream, as a status frame on the live feed
Decision (2026-08-17): the truncated-fetch verdict above is carried to clients as
`QueryActiveAlarmsReplyPayload.snapshot_truncated` on the worker IPC reply and as
@@ -239,6 +239,23 @@ sub-cap fetch clears it, and `GatewayAlarmMonitor.ClearCache` drops it with the
cache generation it describes. A caveat that never turns off is a caveat
operators learn to ignore.
+The **live feed carries the verdict as set-level status**, not per record.
+`StreamAlarms` has an envelope — `AlarmFeedMessage` — so the shape forced on
+`QueryActiveAlarms` above is not forced here: the feed gets a fifth oneof case,
+`snapshot_status` (`AlarmSnapshotStatus.truncated`), alongside `provider_status`.
+The two carriers are therefore deliberately different shapes for the same verdict,
+and each is the only additive option on its own surface. Emission is
+**edge-triggered**, for the same reason the flag is not latched: a status frame
+repeated on every reconcile is noise a consumer filters out, and a filtered-out
+signal is no signal. The exception is the open-time frame, which is
+unconditional — a late joiner cannot distinguish "not truncated" from "this
+gateway does not send the frame" by silence, so it is told explicitly. It is
+ordered after `provider_status` and before the cached `active_alarm` frames so a
+consumer applying the snapshot as it streams holds the caveat while it applies
+the records it qualifies. A monitor restart's `ClearCache` emits the clearing
+frame as well: feed subscribers outlive the monitor's worker session, so a silent
+re-seed would leave them caveating a set that is no longer truncated.
+
This is gateway metadata about **our** fetch mechanics, not a claim about MXAccess
behaviour, so it is not a parity deviation: no event is synthesized and no
MXAccess-observable semantics change.
diff --git a/docs/Grpc.md b/docs/Grpc.md
index 271434c..15bd1c8 100644
--- a/docs/Grpc.md
+++ b/docs/Grpc.md
@@ -94,7 +94,7 @@ An accepted gRPC command payload can still be too large for the worker pipe: the
### `StreamAlarms`
-`StreamAlarms` is a server-streaming, **session-less** RPC that attaches to the gateway's central alarm feed. The handler delegates to `IGatewayAlarmService.StreamAsync`. The stream opens with one `AlarmFeedMessage` carrying an `active_alarm` per currently-active alarm (the ConditionRefresh snapshot), then a single `snapshot_complete`, then a `transition` for every subsequent raise / acknowledge / clear. It is served by the always-on `GatewayAlarmMonitor`, which owns a single gateway-managed worker session and fans out to every attached client — clients no longer open a session of their own. `alarm_filter_prefix`, when set, scopes the stream to a sub-tree.
+`StreamAlarms` is a server-streaming, **session-less** RPC that attaches to the gateway's central alarm feed. The handler delegates to `IGatewayAlarmService.StreamAsync`. The stream opens with a `provider_status` and a `snapshot_status` `AlarmFeedMessage` (the current provider mode and snapshot-completeness verdict), then one `AlarmFeedMessage` carrying an `active_alarm` per currently-active alarm (the ConditionRefresh snapshot), then a single `snapshot_complete`, then a `transition` for every subsequent raise / acknowledge / clear — interleaved with a further `provider_status` on each failover/failback and a further `snapshot_status` on each change of the truncation verdict. It is served by the always-on `GatewayAlarmMonitor`, which owns a single gateway-managed worker session and fans out to every attached client — clients no longer open a session of their own. `alarm_filter_prefix`, when set, scopes the stream to a sub-tree.
### `QueryActiveAlarms`
@@ -119,6 +119,36 @@ The gateway emits `provider_status` once when a client first subscribes
and again on every failover or failback. A late-joining client therefore
always learns the current provider mode without waiting for the next switch.
+#### Snapshot completeness on the alarm feed
+
+`AlarmFeedMessage` has a fifth `payload` case, `snapshot_status`, carrying
+an `AlarmSnapshotStatus` message:
+
+```protobuf
+message AlarmSnapshotStatus {
+ bool truncated = 1; // the cached active set may be missing alarms
+}
+```
+
+It is the feed-level twin of the per-record
+`ActiveAlarmSnapshot.from_truncated_snapshot` flag: `truncated` is true while the
+monitor's cached active-alarm set derives from a capped worker fetch. Read it as
+"this set may be incomplete", never as a statement about record fidelity — that
+is what `degraded` / `source_provider` mean, and the two are independent.
+
+Emission mirrors `provider_status` but with one ordering rule of its own. The
+gateway emits `snapshot_status` once when a client subscribes, **after** the
+open-time `provider_status` and **before** the cached `active_alarm` frames, so a
+consumer applying the snapshot as it streams has the completeness caveat in hand
+while it applies it. The open-time frame is unconditional — an explicit
+`truncated = false` is what distinguishes a complete set from a gateway that
+never sends the frame. Afterwards it is emitted only on a *change* of verdict:
+when a reconcile flips the verdict either way, and when a monitor restart drops a
+truncated verdict with the cache generation it describes (feed subscribers
+outlive the monitor's worker session, so they see that clear). Clients that do
+not know the case see an unset `payload` oneof and ignore the message, exactly as
+before.
+
`AlarmProviderMode` is an enum with three values:
| Value | Meaning |
diff --git a/gateway.md b/gateway.md
index 1013b24..610904a 100644
--- a/gateway.md
+++ b/gateway.md
@@ -249,7 +249,18 @@ from broadcasting Clears for alarms it simply had no room to mention. That
suppression is reported structurally rather than only in a rate-limited worker
warning: the `QueryActiveAlarms` reply payload carries `snapshot_truncated`,
every `ActiveAlarmSnapshot` in it carries `from_truncated_snapshot`, and the
-dashboard Alarms tab shows a warning banner while the flag is set. The flag
+dashboard Alarms tab shows a warning banner while the flag is set. The live
+`StreamAlarms` feed carries the same verdict at feed level as an
+`AlarmSnapshotStatus` message (the `snapshot_status` oneof case), so a streaming
+consumer learns it without polling `QueryActiveAlarms`. It is emitted on stream
+open — after the `provider_status` frame and before the cached `active_alarm`
+frames, so the caveat precedes the records it qualifies — and thereafter only on
+a *change* of verdict (a status frame repeated every reconcile is one consumers
+learn to ignore). The open-time frame is unconditional: an explicit
+`truncated = false` is what separates a complete set from a gateway that never
+sends the frame. A monitor restart drops the verdict with the cache generation it
+describes, and because feed subscribers outlive the monitor's worker session that
+drop is emitted as a clearing frame too. The flag
means "this active set may be incomplete", not "this record is unreliable" —
it is independent of the subtag-fallback `degraded` field above. It is not
latched: the first fetch that comes back under the cap is complete, restores
diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs
index feadce8..2979802 100644
--- a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs
+++ b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs
@@ -361,173 +361,176 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
"cm94eRIaChJkaWFnbm9zdGljX21lc3NhZ2UYBiABKAlCCgoIX2hyZXN1bHRK",
"BAgBEAJSCnNlc3Npb25faWQiUQoTU3RyZWFtQWxhcm1zUmVxdWVzdBIdChVj",
"bGllbnRfY29ycmVsYXRpb25faWQYASABKAkSGwoTYWxhcm1fZmlsdGVyX3By",
- "ZWZpeBgCIAEoCSKEAgoQQWxhcm1GZWVkTWVzc2FnZRJACgxhY3RpdmVfYWxh",
+ "ZWZpeBgCIAEoCSLJAgoQQWxhcm1GZWVkTWVzc2FnZRJACgxhY3RpdmVfYWxh",
"cm0YASABKAsyKC5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25h",
"cHNob3RIABIbChFzbmFwc2hvdF9jb21wbGV0ZRgCIAEoCEgAEkEKCnRyYW5z",
"aXRpb24YAyABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLk9uQWxhcm1UcmFu",
"c2l0aW9uRXZlbnRIABJDCg9wcm92aWRlcl9zdGF0dXMYBCABKAsyKC5teGFj",
- "Y2Vzc19nYXRld2F5LnYxLkFsYXJtUHJvdmlkZXJTdGF0dXNIAEIJCgdwYXls",
- "b2FkIpgBChNBbGFybVByb3ZpZGVyU3RhdHVzEjQKBG1vZGUYASABKA4yJi5t",
- "eGFjY2Vzc19nYXRld2F5LnYxLkFsYXJtUHJvdmlkZXJNb2RlEhAKCGRlZ3Jh",
- "ZGVkGAIgASgIEg4KBnJlYXNvbhgDIAEoCRIpCgVzaW5jZRgEIAEoCzIaLmdv",
- "b2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi6wEKDU14U3RhdHVzUHJveHkSDwoH",
- "c3VjY2VzcxgBIAEoBRI3CghjYXRlZ29yeRgCIAEoDjIlLm14YWNjZXNzX2dh",
- "dGV3YXkudjEuTXhTdGF0dXNDYXRlZ29yeRI4CgtkZXRlY3RlZF9ieRgDIAEo",
- "DjIjLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNTb3VyY2USDgoGZGV0",
- "YWlsGAQgASgFEhQKDHJhd19jYXRlZ29yeRgFIAEoBRIXCg9yYXdfZGV0ZWN0",
- "ZWRfYnkYBiABKAUSFwoPZGlhZ25vc3RpY190ZXh0GAcgASgJIukDCgdNeFZh",
- "bHVlEjIKCWRhdGFfdHlwZRgBIAEoDjIfLm14YWNjZXNzX2dhdGV3YXkudjEu",
- "TXhEYXRhVHlwZRIUCgx2YXJpYW50X3R5cGUYAiABKAkSDwoHaXNfbnVsbBgD",
- "IAEoCBIWCg5yYXdfZGlhZ25vc3RpYxgEIAEoCRIVCg1yYXdfZGF0YV90eXBl",
- "GAUgASgFEhQKCmJvb2xfdmFsdWUYCiABKAhIABIVCgtpbnQzMl92YWx1ZRgL",
- "IAEoBUgAEhUKC2ludDY0X3ZhbHVlGAwgASgDSAASFQoLZmxvYXRfdmFsdWUY",
- "DSABKAJIABIWCgxkb3VibGVfdmFsdWUYDiABKAFIABIWCgxzdHJpbmdfdmFs",
- "dWUYDyABKAlIABI1Cg90aW1lc3RhbXBfdmFsdWUYECABKAsyGi5nb29nbGUu",
- "cHJvdG9idWYuVGltZXN0YW1wSAASMwoLYXJyYXlfdmFsdWUYESABKAsyHC5t",
- "eGFjY2Vzc19nYXRld2F5LnYxLk14QXJyYXlIABITCglyYXdfdmFsdWUYEiAB",
- "KAxIABJAChJzcGFyc2VfYXJyYXlfdmFsdWUYEyABKAsyIi5teGFjY2Vzc19n",
- "YXRld2F5LnYxLk14U3BhcnNlQXJyYXlIAEIGCgRraW5kIv4ECgdNeEFycmF5",
- "EjoKEWVsZW1lbnRfZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3NfZ2F0ZXdh",
- "eS52MS5NeERhdGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRISCgpkaW1l",
- "bnNpb25zGAMgAygNEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEh0KFXJhd19l",
- "bGVtZW50X2RhdGFfdHlwZRgFIAEoBRI1Cgtib29sX3ZhbHVlcxgKIAEoCzIe",
- "Lm14YWNjZXNzX2dhdGV3YXkudjEuQm9vbEFycmF5SAASNwoMaW50MzJfdmFs",
- "dWVzGAsgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQzMkFycmF5SAAS",
- "NwoMaW50NjRfdmFsdWVzGAwgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5J",
- "bnQ2NEFycmF5SAASNwoMZmxvYXRfdmFsdWVzGA0gASgLMh8ubXhhY2Nlc3Nf",
- "Z2F0ZXdheS52MS5GbG9hdEFycmF5SAASOQoNZG91YmxlX3ZhbHVlcxgOIAEo",
- "CzIgLm14YWNjZXNzX2dhdGV3YXkudjEuRG91YmxlQXJyYXlIABI5Cg1zdHJp",
- "bmdfdmFsdWVzGA8gASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52MS5TdHJpbmdB",
- "cnJheUgAEj8KEHRpbWVzdGFtcF92YWx1ZXMYECABKAsyIy5teGFjY2Vzc19n",
- "YXRld2F5LnYxLlRpbWVzdGFtcEFycmF5SAASMwoKcmF3X3ZhbHVlcxgRIAEo",
- "CzIdLm14YWNjZXNzX2dhdGV3YXkudjEuUmF3QXJyYXlIAEIICgZ2YWx1ZXMi",
- "mQEKDU14U3BhcnNlQXJyYXkSOgoRZWxlbWVudF9kYXRhX3R5cGUYASABKA4y",
- "Hy5teGFjY2Vzc19nYXRld2F5LnYxLk14RGF0YVR5cGUSFAoMdG90YWxfbGVu",
- "Z3RoGAIgASgNEjYKCGVsZW1lbnRzGAMgAygLMiQubXhhY2Nlc3NfZ2F0ZXdh",
- "eS52MS5NeFNwYXJzZUVsZW1lbnQiTQoPTXhTcGFyc2VFbGVtZW50Eg0KBWlu",
- "ZGV4GAEgASgNEisKBXZhbHVlGAIgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52",
- "MS5NeFZhbHVlIhsKCUJvb2xBcnJheRIOCgZ2YWx1ZXMYASADKAgiHAoKSW50",
- "MzJBcnJheRIOCgZ2YWx1ZXMYASADKAUiHAoKSW50NjRBcnJheRIOCgZ2YWx1",
- "ZXMYASADKAMiHAoKRmxvYXRBcnJheRIOCgZ2YWx1ZXMYASADKAIiHQoLRG91",
- "YmxlQXJyYXkSDgoGdmFsdWVzGAEgAygBIh0KC1N0cmluZ0FycmF5Eg4KBnZh",
- "bHVlcxgBIAMoCSI8Cg5UaW1lc3RhbXBBcnJheRIqCgZ2YWx1ZXMYASADKAsy",
- "Gi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKCFJhd0FycmF5Eg4KBnZh",
- "bHVlcxgBIAMoDCJYCg5Qcm90b2NvbFN0YXR1cxI1CgRjb2RlGAEgASgOMicu",
- "bXhhY2Nlc3NfZ2F0ZXdheS52MS5Qcm90b2NvbFN0YXR1c0NvZGUSDwoHbWVz",
- "c2FnZRgCIAEoCSqfCwoNTXhDb21tYW5kS2luZBIfChtNWF9DT01NQU5EX0tJ",
- "TkRfVU5TUEVDSUZJRUQQABIcChhNWF9DT01NQU5EX0tJTkRfUkVHSVNURVIQ",
- "ARIeChpNWF9DT01NQU5EX0tJTkRfVU5SRUdJU1RFUhACEhwKGE1YX0NPTU1B",
- "TkRfS0lORF9BRERfSVRFTRADEh0KGU1YX0NPTU1BTkRfS0lORF9BRERfSVRF",
- "TTIQBBIfChtNWF9DT01NQU5EX0tJTkRfUkVNT1ZFX0lURU0QBRIaChZNWF9D",
- "T01NQU5EX0tJTkRfQURWSVNFEAYSHQoZTVhfQ09NTUFORF9LSU5EX1VOX0FE",
- "VklTRRAHEiYKIk1YX0NPTU1BTkRfS0lORF9BRFZJU0VfU1VQRVJWSVNPUlkQ",
- "CBIlCiFNWF9DT01NQU5EX0tJTkRfQUREX0JVRkZFUkVEX0lURU0QCRIwCixN",
- "WF9DT01NQU5EX0tJTkRfU0VUX0JVRkZFUkVEX1VQREFURV9JTlRFUlZBTBAK",
- "EhsKF01YX0NPTU1BTkRfS0lORF9TVVNQRU5EEAsSHAoYTVhfQ09NTUFORF9L",
- "SU5EX0FDVElWQVRFEAwSGQoVTVhfQ09NTUFORF9LSU5EX1dSSVRFEA0SGgoW",
- "TVhfQ09NTUFORF9LSU5EX1dSSVRFMhAOEiEKHU1YX0NPTU1BTkRfS0lORF9X",
- "UklURV9TRUNVUkVEEA8SIgoeTVhfQ09NTUFORF9LSU5EX1dSSVRFX1NFQ1VS",
- "RUQyEBASJQohTVhfQ09NTUFORF9LSU5EX0FVVEhFTlRJQ0FURV9VU0VSEBES",
- "KAokTVhfQ09NTUFORF9LSU5EX0FSQ0hFU1RSQV9VU0VSX1RPX0lEEBISIQod",
- "TVhfQ09NTUFORF9LSU5EX0FERF9JVEVNX0JVTEsQExIkCiBNWF9DT01NQU5E",
- "X0tJTkRfQURWSVNFX0lURU1fQlVMSxAUEiQKIE1YX0NPTU1BTkRfS0lORF9S",
- "RU1PVkVfSVRFTV9CVUxLEBUSJwojTVhfQ09NTUFORF9LSU5EX1VOX0FEVklT",
- "RV9JVEVNX0JVTEsQFhIiCh5NWF9DT01NQU5EX0tJTkRfU1VCU0NSSUJFX0JV",
- "TEsQFxIkCiBNWF9DT01NQU5EX0tJTkRfVU5TVUJTQ1JJQkVfQlVMSxAYEiQK",
- "IE1YX0NPTU1BTkRfS0lORF9TVUJTQ1JJQkVfQUxBUk1TEBkSJgoiTVhfQ09N",
- "TUFORF9LSU5EX1VOU1VCU0NSSUJFX0FMQVJNUxAaEiUKIU1YX0NPTU1BTkRf",
- "S0lORF9BQ0tOT1dMRURHRV9BTEFSTRAbEicKI01YX0NPTU1BTkRfS0lORF9R",
- "VUVSWV9BQ1RJVkVfQUxBUk1TEBwSLQopTVhfQ09NTUFORF9LSU5EX0FDS05P",
- "V0xFREdFX0FMQVJNX0JZX05BTUUQHRIeChpNWF9DT01NQU5EX0tJTkRfV1JJ",
- "VEVfQlVMSxAeEh8KG01YX0NPTU1BTkRfS0lORF9XUklURTJfQlVMSxAfEiYK",
- "Ik1YX0NPTU1BTkRfS0lORF9XUklURV9TRUNVUkVEX0JVTEsQIBInCiNNWF9D",
- "T01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRDJfQlVMSxAhEh0KGU1YX0NPTU1B",
- "TkRfS0lORF9SRUFEX0JVTEsQIhIYChRNWF9DT01NQU5EX0tJTkRfUElORxBk",
- "EiUKIU1YX0NPTU1BTkRfS0lORF9HRVRfU0VTU0lPTl9TVEFURRBlEiMKH01Y",
- "X0NPTU1BTkRfS0lORF9HRVRfV09SS0VSX0lORk8QZhIgChxNWF9DT01NQU5E",
- "X0tJTkRfRFJBSU5fRVZFTlRTEGcSIwofTVhfQ09NTUFORF9LSU5EX1NIVVRE",
- "T1dOX1dPUktFUhBoKnoKEUFsYXJtUHJvdmlkZXJNb2RlEiMKH0FMQVJNX1BS",
- "T1ZJREVSX01PREVfVU5TUEVDSUZJRUQQABIgChxBTEFSTV9QUk9WSURFUl9N",
- "T0RFX0FMQVJNTUdSEAESHgoaQUxBUk1fUFJPVklERVJfTU9ERV9TVUJUQUcQ",
- "AiqtAgoNTXhFdmVudEZhbWlseRIfChtNWF9FVkVOVF9GQU1JTFlfVU5TUEVD",
- "SUZJRUQQABIiCh5NWF9FVkVOVF9GQU1JTFlfT05fREFUQV9DSEFOR0UQARIl",
- "CiFNWF9FVkVOVF9GQU1JTFlfT05fV1JJVEVfQ09NUExFVEUQAhImCiJNWF9F",
- "VkVOVF9GQU1JTFlfT1BFUkFUSU9OX0NPTVBMRVRFEAMSKwonTVhfRVZFTlRf",
- "RkFNSUxZX09OX0JVRkZFUkVEX0RBVEFfQ0hBTkdFEAQSJwojTVhfRVZFTlRf",
- "RkFNSUxZX09OX0FMQVJNX1RSQU5TSVRJT04QBRIyCi5NWF9FVkVOVF9GQU1J",
- "TFlfT05fQUxBUk1fUFJPVklERVJfTU9ERV9DSEFOR0VEEAYqygEKE0FsYXJt",
- "VHJhbnNpdGlvbktpbmQSJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX1VOU1BF",
- "Q0lGSUVEEAASHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX1JBSVNFEAESJQoh",
- "QUxBUk1fVFJBTlNJVElPTl9LSU5EX0FDS05PV0xFREdFEAISHwobQUxBUk1f",
- "VFJBTlNJVElPTl9LSU5EX0NMRUFSEAMSIwofQUxBUk1fVFJBTlNJVElPTl9L",
- "SU5EX1JFVFJJR0dFUhAEKqoBChNBbGFybUNvbmRpdGlvblN0YXRlEiUKIUFM",
- "QVJNX0NPTkRJVElPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFMQVJNX0NP",
- "TkRJVElPTl9TVEFURV9BQ1RJVkUQARImCiJBTEFSTV9DT05ESVRJT05fU1RB",
- "VEVfQUNUSVZFX0FDS0VEEAISIgoeQUxBUk1fQ09ORElUSU9OX1NUQVRFX0lO",
- "QUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0ZWdvcnkSIgoeTVhfU1RBVFVTX0NB",
- "VEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoaTVhfU1RBVFVTX0NBVEVHT1JZX1VO",
- "S05PV04QARIZChVNWF9TVEFUVVNfQ0FURUdPUllfT0sQAhIeChpNWF9TVEFU",
- "VVNfQ0FURUdPUllfUEVORElORxADEh4KGk1YX1NUQVRVU19DQVRFR09SWV9X",
- "QVJOSU5HEAQSKgomTVhfU1RBVFVTX0NBVEVHT1JZX0NPTU1VTklDQVRJT05f",
- "RVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FURUdPUllfQ09ORklHVVJBVElPTl9F",
- "UlJPUhAGEigKJE1YX1NUQVRVU19DQVRFR09SWV9PUEVSQVRJT05BTF9FUlJP",
- "UhAHEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TRUNVUklUWV9FUlJPUhAIEiUK",
- "IU1YX1NUQVRVU19DQVRFR09SWV9TT0ZUV0FSRV9FUlJPUhAJEiIKHk1YX1NU",
- "QVRVU19DQVRFR09SWV9PVEhFUl9FUlJPUhAKKsoCCg5NeFN0YXR1c1NvdXJj",
- "ZRIgChxNWF9TVEFUVVNfU09VUkNFX1VOU1BFQ0lGSUVEEAASHAoYTVhfU1RB",
- "VFVTX1NPVVJDRV9VTktOT1dOEAESIwofTVhfU1RBVFVTX1NPVVJDRV9SRVFV",
- "RVNUSU5HX0xNWBACEiMKH01YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19M",
- "TVgQAxIjCh9NWF9TVEFUVVNfU09VUkNFX1JFUVVFU1RJTkdfTk1YEAQSIwof",
- "TVhfU1RBVFVTX1NPVVJDRV9SRVNQT05ESU5HX05NWBAFEjEKLU1YX1NUQVRV",
- "U19TT1VSQ0VfUkVRVUVTVElOR19BVVRPTUFUSU9OX09CSkVDVBAGEjEKLU1Y",
- "X1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19BVVRPTUFUSU9OX09CSkVDVBAH",
- "Kt0ECgpNeERhdGFUeXBlEhwKGE1YX0RBVEFfVFlQRV9VTlNQRUNJRklFRBAA",
- "EhgKFE1YX0RBVEFfVFlQRV9VTktOT1dOEAESGAoUTVhfREFUQV9UWVBFX05P",
- "X0RBVEEQAhIYChRNWF9EQVRBX1RZUEVfQk9PTEVBThADEhgKFE1YX0RBVEFf",
- "VFlQRV9JTlRFR0VSEAQSFgoSTVhfREFUQV9UWVBFX0ZMT0FUEAUSFwoTTVhf",
- "REFUQV9UWVBFX0RPVUJMRRAGEhcKE01YX0RBVEFfVFlQRV9TVFJJTkcQBxIV",
- "ChFNWF9EQVRBX1RZUEVfVElNRRAIEh0KGU1YX0RBVEFfVFlQRV9FTEFQU0VE",
- "X1RJTUUQCRIfChtNWF9EQVRBX1RZUEVfUkVGRVJFTkNFX1RZUEUQChIcChhN",
- "WF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQCxIVChFNWF9EQVRBX1RZUEVfRU5V",
- "TRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNVUklUWV9DTEFTU0lGSUNBVElPTl9F",
- "TlVNEA0SIgoeTVhfREFUQV9UWVBFX0RBVEFfUVVBTElUWV9UWVBFEA4SHwob",
- "TVhfREFUQV9UWVBFX1FVQUxJRklFRF9FTlVNEA8SIQodTVhfREFUQV9UWVBF",
- "X1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVNWF9EQVRBX1RZUEVfSU5URVJOQVRJ",
- "T05BTElaRURfU1RSSU5HEBESGwoXTVhfREFUQV9UWVBFX0JJR19TVFJJTkcQ",
- "EhIUChBNWF9EQVRBX1RZUEVfRU5EEBMqowMKElByb3RvY29sU3RhdHVzQ29k",
- "ZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09ERV9VTlNQRUNJRklFRBAAEhsKF1BS",
- "T1RPQ09MX1NUQVRVU19DT0RFX09LEAESKAokUFJPVE9DT0xfU1RBVFVTX0NP",
- "REVfSU5WQUxJRF9SRVFVRVNUEAISKgomUFJPVE9DT0xfU1RBVFVTX0NPREVf",
- "U0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQUk9UT0NPTF9TVEFUVVNfQ09ERV9T",
- "RVNTSU9OX05PVF9SRUFEWRAEEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1dP",
- "UktFUl9VTkFWQUlMQUJMRRAFEiAKHFBST1RPQ09MX1NUQVRVU19DT0RFX1RJ",
- "TUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFUVVNfQ09ERV9DQU5DRUxFRBAHEisK",
- "J1BST1RPQ09MX1NUQVRVU19DT0RFX1BST1RPQ09MX1ZJT0xBVElPThAIEikK",
- "JVBST1RPQ09MX1NUQVRVU19DT0RFX01YQUNDRVNTX0ZBSUxVUkUQCSq/AgoM",
- "U2Vzc2lvblN0YXRlEh0KGVNFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIa",
- "ChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5HEAESIQodU0VTU0lPTl9TVEFURV9T",
- "VEFSVElOR19XT1JLRVIQAhIiCh5TRVNTSU9OX1NUQVRFX1dBSVRJTkdfRk9S",
- "X1BJUEUQAxIdChlTRVNTSU9OX1NUQVRFX0hBTkRTSEFLSU5HEAQSJQohU0VT",
- "U0lPTl9TVEFURV9JTklUSUFMSVpJTkdfV09SS0VSEAUSFwoTU0VTU0lPTl9T",
- "VEFURV9SRUFEWRAGEhkKFVNFU1NJT05fU1RBVEVfQ0xPU0lORxAHEhgKFFNF",
- "U1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoVU0VTU0lPTl9TVEFURV9GQVVMVEVE",
- "EAkywwUKD014QWNjZXNzR2F0ZXdheRJdCgtPcGVuU2Vzc2lvbhInLm14YWNj",
- "ZXNzX2dhdGV3YXkudjEuT3BlblNlc3Npb25SZXF1ZXN0GiUubXhhY2Nlc3Nf",
- "Z2F0ZXdheS52MS5PcGVuU2Vzc2lvblJlcGx5EmAKDENsb3NlU2Vzc2lvbhIo",
- "Lm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9uUmVxdWVzdBomLm14",
- "YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9uUmVwbHkSVAoGSW52b2tl",
- "EiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXF1ZXN0GiMubXhh",
- "Y2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXBseRJYCgxTdHJlYW1FdmVu",
- "dHMSKC5teGFjY2Vzc19nYXRld2F5LnYxLlN0cmVhbUV2ZW50c1JlcXVlc3Qa",
- "HC5teGFjY2Vzc19nYXRld2F5LnYxLk14RXZlbnQwARJsChBBY2tub3dsZWRn",
- "ZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFsYXJt",
- "UmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3YXkudjEuQWNrbm93bGVkZ2VBbGFy",
- "bVJlcGx5EmEKDFN0cmVhbUFsYXJtcxIoLm14YWNjZXNzX2dhdGV3YXkudjEu",
- "U3RyZWFtQWxhcm1zUmVxdWVzdBolLm14YWNjZXNzX2dhdGV3YXkudjEuQWxh",
- "cm1GZWVkTWVzc2FnZTABEm4KEVF1ZXJ5QWN0aXZlQWxhcm1zEi0ubXhhY2Nl",
- "c3NfZ2F0ZXdheS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcXVlc3QaKC5teGFj",
- "Y2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3QwAUImqgIjWkIu",
- "TU9NLldXLk14R2F0ZXdheS5Db250cmFjdHMuUHJvdG9iBnByb3RvMw=="));
+ "Y2Vzc19nYXRld2F5LnYxLkFsYXJtUHJvdmlkZXJTdGF0dXNIABJDCg9zbmFw",
+ "c2hvdF9zdGF0dXMYBSABKAsyKC5teGFjY2Vzc19nYXRld2F5LnYxLkFsYXJt",
+ "U25hcHNob3RTdGF0dXNIAEIJCgdwYXlsb2FkIpgBChNBbGFybVByb3ZpZGVy",
+ "U3RhdHVzEjQKBG1vZGUYASABKA4yJi5teGFjY2Vzc19nYXRld2F5LnYxLkFs",
+ "YXJtUHJvdmlkZXJNb2RlEhAKCGRlZ3JhZGVkGAIgASgIEg4KBnJlYXNvbhgD",
+ "IAEoCRIpCgVzaW5jZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3Rh",
+ "bXAiKAoTQWxhcm1TbmFwc2hvdFN0YXR1cxIRCgl0cnVuY2F0ZWQYASABKAgi",
+ "6wEKDU14U3RhdHVzUHJveHkSDwoHc3VjY2VzcxgBIAEoBRI3CghjYXRlZ29y",
+ "eRgCIAEoDjIlLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNDYXRlZ29y",
+ "eRI4CgtkZXRlY3RlZF9ieRgDIAEoDjIjLm14YWNjZXNzX2dhdGV3YXkudjEu",
+ "TXhTdGF0dXNTb3VyY2USDgoGZGV0YWlsGAQgASgFEhQKDHJhd19jYXRlZ29y",
+ "eRgFIAEoBRIXCg9yYXdfZGV0ZWN0ZWRfYnkYBiABKAUSFwoPZGlhZ25vc3Rp",
+ "Y190ZXh0GAcgASgJIukDCgdNeFZhbHVlEjIKCWRhdGFfdHlwZRgBIAEoDjIf",
+ "Lm14YWNjZXNzX2dhdGV3YXkudjEuTXhEYXRhVHlwZRIUCgx2YXJpYW50X3R5",
+ "cGUYAiABKAkSDwoHaXNfbnVsbBgDIAEoCBIWCg5yYXdfZGlhZ25vc3RpYxgE",
+ "IAEoCRIVCg1yYXdfZGF0YV90eXBlGAUgASgFEhQKCmJvb2xfdmFsdWUYCiAB",
+ "KAhIABIVCgtpbnQzMl92YWx1ZRgLIAEoBUgAEhUKC2ludDY0X3ZhbHVlGAwg",
+ "ASgDSAASFQoLZmxvYXRfdmFsdWUYDSABKAJIABIWCgxkb3VibGVfdmFsdWUY",
+ "DiABKAFIABIWCgxzdHJpbmdfdmFsdWUYDyABKAlIABI1Cg90aW1lc3RhbXBf",
+ "dmFsdWUYECABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAASMwoL",
+ "YXJyYXlfdmFsdWUYESABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14QXJy",
+ "YXlIABITCglyYXdfdmFsdWUYEiABKAxIABJAChJzcGFyc2VfYXJyYXlfdmFs",
+ "dWUYEyABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLk14U3BhcnNlQXJyYXlI",
+ "AEIGCgRraW5kIv4ECgdNeEFycmF5EjoKEWVsZW1lbnRfZGF0YV90eXBlGAEg",
+ "ASgOMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeERhdGFUeXBlEhQKDHZhcmlh",
+ "bnRfdHlwZRgCIAEoCRISCgpkaW1lbnNpb25zGAMgAygNEhYKDnJhd19kaWFn",
+ "bm9zdGljGAQgASgJEh0KFXJhd19lbGVtZW50X2RhdGFfdHlwZRgFIAEoBRI1",
+ "Cgtib29sX3ZhbHVlcxgKIAEoCzIeLm14YWNjZXNzX2dhdGV3YXkudjEuQm9v",
+ "bEFycmF5SAASNwoMaW50MzJfdmFsdWVzGAsgASgLMh8ubXhhY2Nlc3NfZ2F0",
+ "ZXdheS52MS5JbnQzMkFycmF5SAASNwoMaW50NjRfdmFsdWVzGAwgASgLMh8u",
+ "bXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQ2NEFycmF5SAASNwoMZmxvYXRfdmFs",
+ "dWVzGA0gASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5GbG9hdEFycmF5SAAS",
+ "OQoNZG91YmxlX3ZhbHVlcxgOIAEoCzIgLm14YWNjZXNzX2dhdGV3YXkudjEu",
+ "RG91YmxlQXJyYXlIABI5Cg1zdHJpbmdfdmFsdWVzGA8gASgLMiAubXhhY2Nl",
+ "c3NfZ2F0ZXdheS52MS5TdHJpbmdBcnJheUgAEj8KEHRpbWVzdGFtcF92YWx1",
+ "ZXMYECABKAsyIy5teGFjY2Vzc19nYXRld2F5LnYxLlRpbWVzdGFtcEFycmF5",
+ "SAASMwoKcmF3X3ZhbHVlcxgRIAEoCzIdLm14YWNjZXNzX2dhdGV3YXkudjEu",
+ "UmF3QXJyYXlIAEIICgZ2YWx1ZXMimQEKDU14U3BhcnNlQXJyYXkSOgoRZWxl",
+ "bWVudF9kYXRhX3R5cGUYASABKA4yHy5teGFjY2Vzc19nYXRld2F5LnYxLk14",
+ "RGF0YVR5cGUSFAoMdG90YWxfbGVuZ3RoGAIgASgNEjYKCGVsZW1lbnRzGAMg",
+ "AygLMiQubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFNwYXJzZUVsZW1lbnQiTQoP",
+ "TXhTcGFyc2VFbGVtZW50Eg0KBWluZGV4GAEgASgNEisKBXZhbHVlGAIgASgL",
+ "MhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlIhsKCUJvb2xBcnJheRIO",
+ "CgZ2YWx1ZXMYASADKAgiHAoKSW50MzJBcnJheRIOCgZ2YWx1ZXMYASADKAUi",
+ "HAoKSW50NjRBcnJheRIOCgZ2YWx1ZXMYASADKAMiHAoKRmxvYXRBcnJheRIO",
+ "CgZ2YWx1ZXMYASADKAIiHQoLRG91YmxlQXJyYXkSDgoGdmFsdWVzGAEgAygB",
+ "Ih0KC1N0cmluZ0FycmF5Eg4KBnZhbHVlcxgBIAMoCSI8Cg5UaW1lc3RhbXBB",
+ "cnJheRIqCgZ2YWx1ZXMYASADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0",
+ "YW1wIhoKCFJhd0FycmF5Eg4KBnZhbHVlcxgBIAMoDCJYCg5Qcm90b2NvbFN0",
+ "YXR1cxI1CgRjb2RlGAEgASgOMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5Qcm90",
+ "b2NvbFN0YXR1c0NvZGUSDwoHbWVzc2FnZRgCIAEoCSqfCwoNTXhDb21tYW5k",
+ "S2luZBIfChtNWF9DT01NQU5EX0tJTkRfVU5TUEVDSUZJRUQQABIcChhNWF9D",
+ "T01NQU5EX0tJTkRfUkVHSVNURVIQARIeChpNWF9DT01NQU5EX0tJTkRfVU5S",
+ "RUdJU1RFUhACEhwKGE1YX0NPTU1BTkRfS0lORF9BRERfSVRFTRADEh0KGU1Y",
+ "X0NPTU1BTkRfS0lORF9BRERfSVRFTTIQBBIfChtNWF9DT01NQU5EX0tJTkRf",
+ "UkVNT1ZFX0lURU0QBRIaChZNWF9DT01NQU5EX0tJTkRfQURWSVNFEAYSHQoZ",
+ "TVhfQ09NTUFORF9LSU5EX1VOX0FEVklTRRAHEiYKIk1YX0NPTU1BTkRfS0lO",
+ "RF9BRFZJU0VfU1VQRVJWSVNPUlkQCBIlCiFNWF9DT01NQU5EX0tJTkRfQURE",
+ "X0JVRkZFUkVEX0lURU0QCRIwCixNWF9DT01NQU5EX0tJTkRfU0VUX0JVRkZF",
+ "UkVEX1VQREFURV9JTlRFUlZBTBAKEhsKF01YX0NPTU1BTkRfS0lORF9TVVNQ",
+ "RU5EEAsSHAoYTVhfQ09NTUFORF9LSU5EX0FDVElWQVRFEAwSGQoVTVhfQ09N",
+ "TUFORF9LSU5EX1dSSVRFEA0SGgoWTVhfQ09NTUFORF9LSU5EX1dSSVRFMhAO",
+ "EiEKHU1YX0NPTU1BTkRfS0lORF9XUklURV9TRUNVUkVEEA8SIgoeTVhfQ09N",
+ "TUFORF9LSU5EX1dSSVRFX1NFQ1VSRUQyEBASJQohTVhfQ09NTUFORF9LSU5E",
+ "X0FVVEhFTlRJQ0FURV9VU0VSEBESKAokTVhfQ09NTUFORF9LSU5EX0FSQ0hF",
+ "U1RSQV9VU0VSX1RPX0lEEBISIQodTVhfQ09NTUFORF9LSU5EX0FERF9JVEVN",
+ "X0JVTEsQExIkCiBNWF9DT01NQU5EX0tJTkRfQURWSVNFX0lURU1fQlVMSxAU",
+ "EiQKIE1YX0NPTU1BTkRfS0lORF9SRU1PVkVfSVRFTV9CVUxLEBUSJwojTVhf",
+ "Q09NTUFORF9LSU5EX1VOX0FEVklTRV9JVEVNX0JVTEsQFhIiCh5NWF9DT01N",
+ "QU5EX0tJTkRfU1VCU0NSSUJFX0JVTEsQFxIkCiBNWF9DT01NQU5EX0tJTkRf",
+ "VU5TVUJTQ1JJQkVfQlVMSxAYEiQKIE1YX0NPTU1BTkRfS0lORF9TVUJTQ1JJ",
+ "QkVfQUxBUk1TEBkSJgoiTVhfQ09NTUFORF9LSU5EX1VOU1VCU0NSSUJFX0FM",
+ "QVJNUxAaEiUKIU1YX0NPTU1BTkRfS0lORF9BQ0tOT1dMRURHRV9BTEFSTRAb",
+ "EicKI01YX0NPTU1BTkRfS0lORF9RVUVSWV9BQ1RJVkVfQUxBUk1TEBwSLQop",
+ "TVhfQ09NTUFORF9LSU5EX0FDS05PV0xFREdFX0FMQVJNX0JZX05BTUUQHRIe",
+ "ChpNWF9DT01NQU5EX0tJTkRfV1JJVEVfQlVMSxAeEh8KG01YX0NPTU1BTkRf",
+ "S0lORF9XUklURTJfQlVMSxAfEiYKIk1YX0NPTU1BTkRfS0lORF9XUklURV9T",
+ "RUNVUkVEX0JVTEsQIBInCiNNWF9DT01NQU5EX0tJTkRfV1JJVEVfU0VDVVJF",
+ "RDJfQlVMSxAhEh0KGU1YX0NPTU1BTkRfS0lORF9SRUFEX0JVTEsQIhIYChRN",
+ "WF9DT01NQU5EX0tJTkRfUElORxBkEiUKIU1YX0NPTU1BTkRfS0lORF9HRVRf",
+ "U0VTU0lPTl9TVEFURRBlEiMKH01YX0NPTU1BTkRfS0lORF9HRVRfV09SS0VS",
+ "X0lORk8QZhIgChxNWF9DT01NQU5EX0tJTkRfRFJBSU5fRVZFTlRTEGcSIwof",
+ "TVhfQ09NTUFORF9LSU5EX1NIVVRET1dOX1dPUktFUhBoKnoKEUFsYXJtUHJv",
+ "dmlkZXJNb2RlEiMKH0FMQVJNX1BST1ZJREVSX01PREVfVU5TUEVDSUZJRUQQ",
+ "ABIgChxBTEFSTV9QUk9WSURFUl9NT0RFX0FMQVJNTUdSEAESHgoaQUxBUk1f",
+ "UFJPVklERVJfTU9ERV9TVUJUQUcQAiqtAgoNTXhFdmVudEZhbWlseRIfChtN",
+ "WF9FVkVOVF9GQU1JTFlfVU5TUEVDSUZJRUQQABIiCh5NWF9FVkVOVF9GQU1J",
+ "TFlfT05fREFUQV9DSEFOR0UQARIlCiFNWF9FVkVOVF9GQU1JTFlfT05fV1JJ",
+ "VEVfQ09NUExFVEUQAhImCiJNWF9FVkVOVF9GQU1JTFlfT1BFUkFUSU9OX0NP",
+ "TVBMRVRFEAMSKwonTVhfRVZFTlRfRkFNSUxZX09OX0JVRkZFUkVEX0RBVEFf",
+ "Q0hBTkdFEAQSJwojTVhfRVZFTlRfRkFNSUxZX09OX0FMQVJNX1RSQU5TSVRJ",
+ "T04QBRIyCi5NWF9FVkVOVF9GQU1JTFlfT05fQUxBUk1fUFJPVklERVJfTU9E",
+ "RV9DSEFOR0VEEAYqygEKE0FsYXJtVHJhbnNpdGlvbktpbmQSJQohQUxBUk1f",
+ "VFJBTlNJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQUxBUk1fVFJBTlNJ",
+ "VElPTl9LSU5EX1JBSVNFEAESJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX0FD",
+ "S05PV0xFREdFEAISHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX0NMRUFSEAMS",
+ "IwofQUxBUk1fVFJBTlNJVElPTl9LSU5EX1JFVFJJR0dFUhAEKqoBChNBbGFy",
+ "bUNvbmRpdGlvblN0YXRlEiUKIUFMQVJNX0NPTkRJVElPTl9TVEFURV9VTlNQ",
+ "RUNJRklFRBAAEiAKHEFMQVJNX0NPTkRJVElPTl9TVEFURV9BQ1RJVkUQARIm",
+ "CiJBTEFSTV9DT05ESVRJT05fU1RBVEVfQUNUSVZFX0FDS0VEEAISIgoeQUxB",
+ "Uk1fQ09ORElUSU9OX1NUQVRFX0lOQUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0",
+ "ZWdvcnkSIgoeTVhfU1RBVFVTX0NBVEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoa",
+ "TVhfU1RBVFVTX0NBVEVHT1JZX1VOS05PV04QARIZChVNWF9TVEFUVVNfQ0FU",
+ "RUdPUllfT0sQAhIeChpNWF9TVEFUVVNfQ0FURUdPUllfUEVORElORxADEh4K",
+ "Gk1YX1NUQVRVU19DQVRFR09SWV9XQVJOSU5HEAQSKgomTVhfU1RBVFVTX0NB",
+ "VEVHT1JZX0NPTU1VTklDQVRJT05fRVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FU",
+ "RUdPUllfQ09ORklHVVJBVElPTl9FUlJPUhAGEigKJE1YX1NUQVRVU19DQVRF",
+ "R09SWV9PUEVSQVRJT05BTF9FUlJPUhAHEiUKIU1YX1NUQVRVU19DQVRFR09S",
+ "WV9TRUNVUklUWV9FUlJPUhAIEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TT0ZU",
+ "V0FSRV9FUlJPUhAJEiIKHk1YX1NUQVRVU19DQVRFR09SWV9PVEhFUl9FUlJP",
+ "UhAKKsoCCg5NeFN0YXR1c1NvdXJjZRIgChxNWF9TVEFUVVNfU09VUkNFX1VO",
+ "U1BFQ0lGSUVEEAASHAoYTVhfU1RBVFVTX1NPVVJDRV9VTktOT1dOEAESIwof",
+ "TVhfU1RBVFVTX1NPVVJDRV9SRVFVRVNUSU5HX0xNWBACEiMKH01YX1NUQVRV",
+ "U19TT1VSQ0VfUkVTUE9ORElOR19MTVgQAxIjCh9NWF9TVEFUVVNfU09VUkNF",
+ "X1JFUVVFU1RJTkdfTk1YEAQSIwofTVhfU1RBVFVTX1NPVVJDRV9SRVNQT05E",
+ "SU5HX05NWBAFEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVRVUVTVElOR19BVVRP",
+ "TUFUSU9OX09CSkVDVBAGEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElO",
+ "R19BVVRPTUFUSU9OX09CSkVDVBAHKt0ECgpNeERhdGFUeXBlEhwKGE1YX0RB",
+ "VEFfVFlQRV9VTlNQRUNJRklFRBAAEhgKFE1YX0RBVEFfVFlQRV9VTktOT1dO",
+ "EAESGAoUTVhfREFUQV9UWVBFX05PX0RBVEEQAhIYChRNWF9EQVRBX1RZUEVf",
+ "Qk9PTEVBThADEhgKFE1YX0RBVEFfVFlQRV9JTlRFR0VSEAQSFgoSTVhfREFU",
+ "QV9UWVBFX0ZMT0FUEAUSFwoTTVhfREFUQV9UWVBFX0RPVUJMRRAGEhcKE01Y",
+ "X0RBVEFfVFlQRV9TVFJJTkcQBxIVChFNWF9EQVRBX1RZUEVfVElNRRAIEh0K",
+ "GU1YX0RBVEFfVFlQRV9FTEFQU0VEX1RJTUUQCRIfChtNWF9EQVRBX1RZUEVf",
+ "UkVGRVJFTkNFX1RZUEUQChIcChhNWF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQ",
+ "CxIVChFNWF9EQVRBX1RZUEVfRU5VTRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNV",
+ "UklUWV9DTEFTU0lGSUNBVElPTl9FTlVNEA0SIgoeTVhfREFUQV9UWVBFX0RB",
+ "VEFfUVVBTElUWV9UWVBFEA4SHwobTVhfREFUQV9UWVBFX1FVQUxJRklFRF9F",
+ "TlVNEA8SIQodTVhfREFUQV9UWVBFX1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVN",
+ "WF9EQVRBX1RZUEVfSU5URVJOQVRJT05BTElaRURfU1RSSU5HEBESGwoXTVhf",
+ "REFUQV9UWVBFX0JJR19TVFJJTkcQEhIUChBNWF9EQVRBX1RZUEVfRU5EEBMq",
+ "owMKElByb3RvY29sU3RhdHVzQ29kZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09E",
+ "RV9VTlNQRUNJRklFRBAAEhsKF1BST1RPQ09MX1NUQVRVU19DT0RFX09LEAES",
+ "KAokUFJPVE9DT0xfU1RBVFVTX0NPREVfSU5WQUxJRF9SRVFVRVNUEAISKgom",
+ "UFJPVE9DT0xfU1RBVFVTX0NPREVfU0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQ",
+ "Uk9UT0NPTF9TVEFUVVNfQ09ERV9TRVNTSU9OX05PVF9SRUFEWRAEEisKJ1BS",
+ "T1RPQ09MX1NUQVRVU19DT0RFX1dPUktFUl9VTkFWQUlMQUJMRRAFEiAKHFBS",
+ "T1RPQ09MX1NUQVRVU19DT0RFX1RJTUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFU",
+ "VVNfQ09ERV9DQU5DRUxFRBAHEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1BS",
+ "T1RPQ09MX1ZJT0xBVElPThAIEikKJVBST1RPQ09MX1NUQVRVU19DT0RFX01Y",
+ "QUNDRVNTX0ZBSUxVUkUQCSq/AgoMU2Vzc2lvblN0YXRlEh0KGVNFU1NJT05f",
+ "U1RBVEVfVU5TUEVDSUZJRUQQABIaChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5H",
+ "EAESIQodU0VTU0lPTl9TVEFURV9TVEFSVElOR19XT1JLRVIQAhIiCh5TRVNT",
+ "SU9OX1NUQVRFX1dBSVRJTkdfRk9SX1BJUEUQAxIdChlTRVNTSU9OX1NUQVRF",
+ "X0hBTkRTSEFLSU5HEAQSJQohU0VTU0lPTl9TVEFURV9JTklUSUFMSVpJTkdf",
+ "V09SS0VSEAUSFwoTU0VTU0lPTl9TVEFURV9SRUFEWRAGEhkKFVNFU1NJT05f",
+ "U1RBVEVfQ0xPU0lORxAHEhgKFFNFU1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoV",
+ "U0VTU0lPTl9TVEFURV9GQVVMVEVEEAkywwUKD014QWNjZXNzR2F0ZXdheRJd",
+ "CgtPcGVuU2Vzc2lvbhInLm14YWNjZXNzX2dhdGV3YXkudjEuT3BlblNlc3Np",
+ "b25SZXF1ZXN0GiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVuU2Vzc2lvblJl",
+ "cGx5EmAKDENsb3NlU2Vzc2lvbhIoLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xv",
+ "c2VTZXNzaW9uUmVxdWVzdBomLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VT",
+ "ZXNzaW9uUmVwbHkSVAoGSW52b2tlEiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5N",
+ "eENvbW1hbmRSZXF1ZXN0GiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1h",
+ "bmRSZXBseRJYCgxTdHJlYW1FdmVudHMSKC5teGFjY2Vzc19nYXRld2F5LnYx",
+ "LlN0cmVhbUV2ZW50c1JlcXVlc3QaHC5teGFjY2Vzc19nYXRld2F5LnYxLk14",
+ "RXZlbnQwARJsChBBY2tub3dsZWRnZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdh",
+ "eS52MS5BY2tub3dsZWRnZUFsYXJtUmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3",
+ "YXkudjEuQWNrbm93bGVkZ2VBbGFybVJlcGx5EmEKDFN0cmVhbUFsYXJtcxIo",
+ "Lm14YWNjZXNzX2dhdGV3YXkudjEuU3RyZWFtQWxhcm1zUmVxdWVzdBolLm14",
+ "YWNjZXNzX2dhdGV3YXkudjEuQWxhcm1GZWVkTWVzc2FnZTABEm4KEVF1ZXJ5",
+ "QWN0aXZlQWxhcm1zEi0ubXhhY2Nlc3NfZ2F0ZXdheS52MS5RdWVyeUFjdGl2",
+ "ZUFsYXJtc1JlcXVlc3QaKC5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFs",
+ "YXJtU25hcHNob3QwAUImqgIjWkIuTU9NLldXLk14R2F0ZXdheS5Db250cmFj",
+ "dHMuUHJvdG9iBnByb3RvMw=="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandKind), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderMode), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEventFamily), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmConditionState), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusCategory), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusSource), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxDataType), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatusCode), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.SessionState), }, null, new pbr::GeneratedClrTypeInfo[] {
@@ -616,8 +619,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest.Parser, new[]{ "ClientCorrelationId", "AlarmFullReference", "Comment", "OperatorUser" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser, new[]{ "CorrelationId", "ProtocolStatus", "Hresult", "Status", "DiagnosticMessage" }, new[]{ "Hresult" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest), global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest.Parser, new[]{ "ClientCorrelationId", "AlarmFilterPrefix" }, null, null, null, null),
- new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage.Parser, new[]{ "ActiveAlarm", "SnapshotComplete", "Transition", "ProviderStatus" }, new[]{ "Payload" }, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage.Parser, new[]{ "ActiveAlarm", "SnapshotComplete", "Transition", "ProviderStatus", "SnapshotStatus" }, new[]{ "Payload" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderStatus), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderStatus.Parser, new[]{ "Mode", "Degraded", "Reason", "Since" }, null, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus.Parser, new[]{ "Truncated" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusProxy), global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusProxy.Parser, new[]{ "Success", "Category", "DetectedBy", "Detail", "RawCategory", "RawDetectedBy", "DiagnosticText" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxValue), global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxValue.Parser, new[]{ "DataType", "VariantType", "IsNull", "RawDiagnostic", "RawDataType", "BoolValue", "Int32Value", "Int64Value", "FloatValue", "DoubleValue", "StringValue", "TimestampValue", "ArrayValue", "RawValue", "SparseArrayValue" }, new[]{ "Kind" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxArray), global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxArray.Parser, new[]{ "ElementDataType", "VariantType", "Dimensions", "RawDiagnostic", "RawElementDataType", "BoolValues", "Int32Values", "Int64Values", "FloatValues", "DoubleValues", "StringValues", "TimestampValues", "RawValues" }, new[]{ "Values" }, null, null, null),
@@ -28561,6 +28565,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
case PayloadOneofCase.ProviderStatus:
ProviderStatus = other.ProviderStatus.Clone();
break;
+ case PayloadOneofCase.SnapshotStatus:
+ SnapshotStatus = other.SnapshotStatus.Clone();
+ break;
}
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
@@ -28648,6 +28655,23 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
}
+ /// Field number for the "snapshot_status" field.
+ public const int SnapshotStatusFieldNumber = 5;
+ ///
+ /// Snapshot-completeness status. Emitted once on stream open and again on
+ /// every change of the truncation verdict, so late joiners learn whether the
+ /// feed's active-alarm set may be incomplete.
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus SnapshotStatus {
+ get { return payloadCase_ == PayloadOneofCase.SnapshotStatus ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus) payload_ : null; }
+ set {
+ payload_ = value;
+ payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.SnapshotStatus;
+ }
+ }
+
private object payload_;
/// Enum of possible cases for the "payload" oneof.
public enum PayloadOneofCase {
@@ -28656,6 +28680,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
SnapshotComplete = 2,
Transition = 3,
ProviderStatus = 4,
+ SnapshotStatus = 5,
}
private PayloadOneofCase payloadCase_ = PayloadOneofCase.None;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -28690,6 +28715,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
if (SnapshotComplete != other.SnapshotComplete) return false;
if (!object.Equals(Transition, other.Transition)) return false;
if (!object.Equals(ProviderStatus, other.ProviderStatus)) return false;
+ if (!object.Equals(SnapshotStatus, other.SnapshotStatus)) return false;
if (PayloadCase != other.PayloadCase) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -28702,6 +28728,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
if (HasSnapshotComplete) hash ^= SnapshotComplete.GetHashCode();
if (payloadCase_ == PayloadOneofCase.Transition) hash ^= Transition.GetHashCode();
if (payloadCase_ == PayloadOneofCase.ProviderStatus) hash ^= ProviderStatus.GetHashCode();
+ if (payloadCase_ == PayloadOneofCase.SnapshotStatus) hash ^= SnapshotStatus.GetHashCode();
hash ^= (int) payloadCase_;
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
@@ -28737,6 +28764,10 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
output.WriteRawTag(34);
output.WriteMessage(ProviderStatus);
}
+ if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
+ output.WriteRawTag(42);
+ output.WriteMessage(SnapshotStatus);
+ }
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -28763,6 +28794,10 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
output.WriteRawTag(34);
output.WriteMessage(ProviderStatus);
}
+ if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
+ output.WriteRawTag(42);
+ output.WriteMessage(SnapshotStatus);
+ }
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -28785,6 +28820,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
if (payloadCase_ == PayloadOneofCase.ProviderStatus) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(ProviderStatus);
}
+ if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
+ size += 1 + pb::CodedOutputStream.ComputeMessageSize(SnapshotStatus);
+ }
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -28819,6 +28857,12 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
ProviderStatus.MergeFrom(other.ProviderStatus);
break;
+ case PayloadOneofCase.SnapshotStatus:
+ if (SnapshotStatus == null) {
+ SnapshotStatus = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus();
+ }
+ SnapshotStatus.MergeFrom(other.SnapshotStatus);
+ break;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
@@ -28871,6 +28915,15 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
ProviderStatus = subBuilder;
break;
}
+ case 42: {
+ global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus();
+ if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
+ subBuilder.MergeFrom(SnapshotStatus);
+ }
+ input.ReadMessage(subBuilder);
+ SnapshotStatus = subBuilder;
+ break;
+ }
}
}
#endif
@@ -28921,6 +28974,15 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
ProviderStatus = subBuilder;
break;
}
+ case 42: {
+ global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus();
+ if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
+ subBuilder.MergeFrom(SnapshotStatus);
+ }
+ input.ReadMessage(subBuilder);
+ SnapshotStatus = subBuilder;
+ break;
+ }
}
}
}
@@ -29252,6 +29314,218 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
+ ///
+ /// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
+ /// (after the initial provider_status frame, before the cached active_alarm
+ /// frames) so late joiners learn the current verdict, and again on every change
+ /// of the truncation verdict observed at reconcile. Mirrors the per-record
+ /// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
+ /// consumers can reason about completeness without polling QueryActiveAlarms.
+ ///
+ [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
+ public sealed partial class AlarmSnapshotStatus : pb::IMessage
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ , pb::IBufferMessage
+ #endif
+ {
+ private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new AlarmSnapshotStatus());
+ private pb::UnknownFieldSet _unknownFields;
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public static pb::MessageParser Parser { get { return _parser; } }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public static pbr::MessageDescriptor Descriptor {
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[87]; }
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ pbr::MessageDescriptor pb::IMessage.Descriptor {
+ get { return Descriptor; }
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public AlarmSnapshotStatus() {
+ OnConstruction();
+ }
+
+ partial void OnConstruction();
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public AlarmSnapshotStatus(AlarmSnapshotStatus other) : this() {
+ truncated_ = other.truncated_;
+ _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public AlarmSnapshotStatus Clone() {
+ return new AlarmSnapshotStatus(this);
+ }
+
+ /// Field number for the "truncated" field.
+ public const int TruncatedFieldNumber = 1;
+ private bool truncated_;
+ ///
+ /// True while the monitor's cached active-alarm set derives from a truncated
+ /// (capped) worker fetch — the set may be missing alarms. Distinct from
+ /// provider degradation (AlarmProviderStatus.degraded), which describes the
+ /// fidelity of the records rather than the completeness of the set.
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public bool Truncated {
+ get { return truncated_; }
+ set {
+ truncated_ = value;
+ }
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public override bool Equals(object other) {
+ return Equals(other as AlarmSnapshotStatus);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public bool Equals(AlarmSnapshotStatus other) {
+ if (ReferenceEquals(other, null)) {
+ return false;
+ }
+ if (ReferenceEquals(other, this)) {
+ return true;
+ }
+ if (Truncated != other.Truncated) return false;
+ return Equals(_unknownFields, other._unknownFields);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public override int GetHashCode() {
+ int hash = 1;
+ if (Truncated != false) hash ^= Truncated.GetHashCode();
+ if (_unknownFields != null) {
+ hash ^= _unknownFields.GetHashCode();
+ }
+ return hash;
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public override string ToString() {
+ return pb::JsonFormatter.ToDiagnosticString(this);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public void WriteTo(pb::CodedOutputStream output) {
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ output.WriteRawMessage(this);
+ #else
+ if (Truncated != false) {
+ output.WriteRawTag(8);
+ output.WriteBool(Truncated);
+ }
+ if (_unknownFields != null) {
+ _unknownFields.WriteTo(output);
+ }
+ #endif
+ }
+
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
+ if (Truncated != false) {
+ output.WriteRawTag(8);
+ output.WriteBool(Truncated);
+ }
+ if (_unknownFields != null) {
+ _unknownFields.WriteTo(ref output);
+ }
+ }
+ #endif
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public int CalculateSize() {
+ int size = 0;
+ if (Truncated != false) {
+ size += 1 + 1;
+ }
+ if (_unknownFields != null) {
+ size += _unknownFields.CalculateSize();
+ }
+ return size;
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public void MergeFrom(AlarmSnapshotStatus other) {
+ if (other == null) {
+ return;
+ }
+ if (other.Truncated != false) {
+ Truncated = other.Truncated;
+ }
+ _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
+ }
+
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public void MergeFrom(pb::CodedInputStream input) {
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ input.ReadRawMessage(this);
+ #else
+ uint tag;
+ while ((tag = input.ReadTag()) != 0) {
+ if ((tag & 7) == 4) {
+ // Abort on any end group tag.
+ return;
+ }
+ switch(tag) {
+ default:
+ _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
+ break;
+ case 8: {
+ Truncated = input.ReadBool();
+ break;
+ }
+ }
+ }
+ #endif
+ }
+
+ #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
+ uint tag;
+ while ((tag = input.ReadTag()) != 0) {
+ if ((tag & 7) == 4) {
+ // Abort on any end group tag.
+ return;
+ }
+ switch(tag) {
+ default:
+ _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
+ break;
+ case 8: {
+ Truncated = input.ReadBool();
+ break;
+ }
+ }
+ }
+ }
+ #endif
+
+ }
+
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class MxStatusProxy : pb::IMessage
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
@@ -29267,7 +29541,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[87]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[88]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -29698,7 +29972,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[88]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[89]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -30608,7 +30882,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[89]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[90]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -31402,7 +31676,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[90]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[91]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -31663,7 +31937,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[91]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[92]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -31910,7 +32184,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[92]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[93]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32099,7 +32373,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[93]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[94]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32288,7 +32562,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[94]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[95]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32477,7 +32751,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[95]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[96]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32666,7 +32940,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[96]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[97]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32855,7 +33129,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[97]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[98]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -33042,7 +33316,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[98]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[99]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -33229,7 +33503,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[99]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[100]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -33416,7 +33690,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
- get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[100]; }
+ get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[101]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto
index 0ee3fb2..85d13a7 100644
--- a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto
+++ b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto
@@ -1018,6 +1018,10 @@ message AlarmFeedMessage {
// Provider-mode status. Emitted once on stream open and again on every
// failover/failback so late joiners learn the current mode immediately.
AlarmProviderStatus provider_status = 4;
+ // Snapshot-completeness status. Emitted once on stream open and again on
+ // every change of the truncation verdict, so late joiners learn whether the
+ // feed's active-alarm set may be incomplete.
+ AlarmSnapshotStatus snapshot_status = 5;
}
}
@@ -1028,6 +1032,20 @@ message AlarmProviderStatus {
google.protobuf.Timestamp since = 4;
}
+// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
+// (after the initial provider_status frame, before the cached active_alarm
+// frames) so late joiners learn the current verdict, and again on every change
+// of the truncation verdict observed at reconcile. Mirrors the per-record
+// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
+// consumers can reason about completeness without polling QueryActiveAlarms.
+message AlarmSnapshotStatus {
+ // True while the monitor's cached active-alarm set derives from a truncated
+ // (capped) worker fetch — the set may be missing alarms. Distinct from
+ // provider degradation (AlarmProviderStatus.degraded), which describes the
+ // fidelity of the records rather than the completeness of the set.
+ bool truncated = 1;
+}
+
message MxStatusProxy {
// Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct
// (a 16-bit signed value in the COM struct, widened to int32 on the
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
index 77aa58c..9058615 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
@@ -624,11 +624,13 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
// at-least-once: consumers must still treat alarm state idempotently — apply a transition as
// "set the alarm to this state", never as an increment or a toggle.
//
- // Truncation (`snapshotTruncated`) needs no special handling here, and that is worth saying
+ // Truncation (`snapshotTruncated`) needs no per-alarm handling here, and that is worth saying
// because the obvious worry — a capped fetch reading as a wave of Clears — is answered one
// level down. The worker merges rather than replaces its retained snapshot on a capped fetch,
// so the set arriving here still carries the alarms the capped reply had no room to mention.
- // The flag is therefore only recorded, for the operator-facing completeness caveat.
+ // The flag is set-level status, not a delta: it is recorded, and a CHANGE of verdict is pushed
+ // to the feed as a snapshot_status frame (SetSnapshotTruncated) so live consumers learn the
+ // completeness caveat without polling QueryActiveAlarms.
private void ApplyReconcile(IEnumerable snapshots, bool snapshotTruncated)
{
Dictionary next = new(StringComparer.Ordinal);
@@ -688,11 +690,30 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
_alarms[incoming.Key] = incoming.Value;
}
- _snapshotTruncated = snapshotTruncated;
+ SetSnapshotTruncated(snapshotTruncated);
_currentAlarmsProjection = null;
}
}
+ // Caller holds _sync. Records the truncation verdict and, on a CHANGE of verdict, pushes the
+ // feed-level snapshot_status frame. Edge-triggered rather than per-reconcile: a status frame
+ // repeated every reconcile interval is one consumers learn to ignore. The verdict describes the
+ // whole cached set, not one alarm, so — like provider status — it goes to every subscriber
+ // regardless of alarm-filter prefix.
+ private void SetSnapshotTruncated(bool truncated)
+ {
+ if (_snapshotTruncated == truncated)
+ {
+ return;
+ }
+
+ _snapshotTruncated = truncated;
+ BroadcastToAll(new AlarmFeedMessage
+ {
+ SnapshotStatus = new AlarmSnapshotStatus { Truncated = truncated },
+ });
+ }
+
// Caller holds _sync. Pushes a feed message to every matching subscriber;
// a subscriber that has fallen behind is completed with an error and dropped.
private void Broadcast(AlarmFeedMessage message, string reference)
@@ -738,8 +759,10 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
_alarms.Clear();
// The truncation verdict describes the cache generation being discarded, so it goes
// with it. Carrying it across a monitor restart would caveat an empty set as "may be
- // incomplete" on evidence from a session that no longer exists.
- _snapshotTruncated = false;
+ // incomplete" on evidence from a session that no longer exists. Dropping a truncated
+ // verdict IS a completeness change, and feed subscribers outlive the monitor's worker
+ // session, so this routes through the edge path and they see the clearing frame.
+ SetSnapshotTruncated(false);
_currentAlarmsProjection = null;
}
}
@@ -761,13 +784,16 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
ActiveAlarmSnapshot[] snapshot;
AlarmProviderStatus providerStatus;
+ bool snapshotTruncated;
lock (_sync)
{
// Register before snapshotting under the same lock so neither a
// transition nor a provider-mode change can slip between the snapshot
- // and the live stream.
+ // and the live stream. The truncation verdict is read here too, so the
+ // caveat and the set it qualifies are a consistent pair.
_subscribers.Add(subscriber);
providerStatus = BuildProviderStatus();
+ snapshotTruncated = _snapshotTruncated;
snapshot = _alarms.Values
.Where(alarm => prefix.Length == 0
|| alarm.AlarmFullReference.StartsWith(prefix, StringComparison.Ordinal))
@@ -781,6 +807,15 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
// learns the mode (and whether the feed is degraded) before any alarms.
yield return new AlarmFeedMessage { ProviderStatus = providerStatus };
+ // Then the completeness caveat, BEFORE the cached snapshot it qualifies: a consumer
+ // applying the snapshot as it streams needs to know whether the set may be missing
+ // alarms while it applies it, not after. Unconditional — an explicit false is what
+ // separates "the set is complete" from "this gateway never sends the frame".
+ yield return new AlarmFeedMessage
+ {
+ SnapshotStatus = new AlarmSnapshotStatus { Truncated = snapshotTruncated },
+ };
+
foreach (ActiveAlarmSnapshot alarm in snapshot)
{
yield return new AlarmFeedMessage { ActiveAlarm = alarm };
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs
index 413fbcb..54bcb00 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs
@@ -55,14 +55,25 @@ public interface IGatewayAlarmService
/// the intended granularity for a completeness hint; pairing them exactly
/// would need a combined accessor this seam deliberately does not have.
///
+ ///
+ /// This is the polled read of the verdict. The same verdict is pushed to
+ /// the live feed as the snapshot_status
+ /// (AlarmSnapshotStatus) case of —
+ /// once at open and again on every change — so
+ /// a streaming consumer need not poll this property.
+ ///
///
bool SnapshotTruncated { get; }
///
- /// Attaches to the central alarm feed. The returned stream yields one
- /// per currently-active alarm, then a
- /// single snapshot_complete sentinel, then a transition
- /// for every subsequent change.
+ /// Attaches to the central alarm feed. The returned stream opens with a
+ /// provider_status and a snapshot_status
+ /// — the current provider mode and
+ /// completeness verdict, so the caveats precede the records they
+ /// qualify — then one message per currently-active alarm, then a single
+ /// snapshot_complete sentinel, then a transition for every
+ /// subsequent change (and a further status message on every provider-mode
+ /// or truncation-verdict change).
///
/// Optional alarm-reference prefix scoping the feed.
/// Token that ends the subscription.
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmTruncationSignalTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmTruncationSignalTests.cs
index e8f8772..b226a22 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmTruncationSignalTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmTruncationSignalTests.cs
@@ -16,7 +16,8 @@ namespace ZB.MOM.WW.MxGateway.Tests.Alarms;
///
/// Carries the worker's truncated-fetch verdict across the gateway: worker
/// reply payload → → the public
-/// QueryActiveAlarms stream.
+/// QueryActiveAlarms stream (per record) and the live
+/// StreamAlarms feed (the snapshot_status frame).
///
///
///
@@ -145,6 +146,302 @@ public sealed class AlarmTruncationSignalTests
await monitor.StopAsync(CancellationToken.None);
}
+ ///
+ /// The feed-level edge. A reconcile that flips the verdict to truncated
+ /// pushes exactly one snapshot_status frame to every attached
+ /// subscriber; a following reconcile carrying the same verdict pushes
+ /// none. Without the edge test the frame could be emitted per reconcile —
+ /// a status frame every few seconds forever, which consumers would learn
+ /// to ignore.
+ ///
+ /// A task that represents the asynchronous operation.
+ [Fact]
+ public async Task ReconcileFlippingToTruncated_EmitsOneSnapshotStatusFrame()
+ {
+ using GatewayMetrics metrics = new();
+ StubSessionManager sessions = new();
+ using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
+
+ using CancellationTokenSource cts = new();
+ await monitor.StartAsync(cts.Token);
+ await sessions.WaitForReconcileAsync(WaitTimeout);
+
+ List received = [];
+ TaskCompletionSource attached = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ using CancellationTokenSource streamCts = new();
+ Task reader = ReadFeedAsync(monitor, received, attached, streamCts.Token);
+ await attached.Task.WaitAsync(WaitTimeout);
+
+ // The worker's next fetch comes back capped. A provider-mode event forces the reconcile
+ // immediately, so the test never waits on the periodic timer; each probe also broadcasts a
+ // provider_status frame, which is the barrier the assertions below count on.
+ sessions.SnapshotTruncated = true;
+ sessions.EmitEvent(ProviderModeProbe(1));
+
+ // A second, identical reconcile: same verdict, so no second frame. The third probe is only
+ // a barrier — the monitor handles provider-mode events one at a time and awaits the
+ // reconcile inside each, so its provider_status proves probe 2's reconcile has completed.
+ sessions.EmitEvent(ProviderModeProbe(2));
+ sessions.EmitEvent(ProviderModeProbe(3));
+ await WaitUntilAsync(() => CountOf(received, AlarmFeedMessage.PayloadOneofCase.ProviderStatus) >= 4, WaitTimeout);
+
+ lock (received)
+ {
+ bool[] verdicts = received
+ .Where(m => m.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotStatus)
+ .Select(m => m.SnapshotStatus.Truncated)
+ .ToArray();
+
+ // The first is this subscriber's open-time baseline; the second is the edge.
+ Assert.Equal([false, true], verdicts);
+ }
+
+ await streamCts.CancelAsync();
+ await reader;
+ await cts.CancelAsync();
+ await monitor.StopAsync(CancellationToken.None);
+ }
+
+ ///
+ /// The clearing edge. A fetch that comes back under the cap restores
+ /// absence authority, and the feed says so — otherwise a consumer that
+ /// saw the truncated frame would caveat its alarm set forever.
+ ///
+ /// A task that represents the asynchronous operation.
+ [Fact]
+ public async Task ReconcileClearingTruncation_EmitsTheClearingFrame()
+ {
+ using GatewayMetrics metrics = new();
+ StubSessionManager sessions = new() { SnapshotTruncated = true };
+ using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
+
+ using CancellationTokenSource cts = new();
+ await monitor.StartAsync(cts.Token);
+ await sessions.WaitForReconcileAsync(WaitTimeout);
+ await WaitUntilAsync(() => monitor.SnapshotTruncated, WaitTimeout);
+
+ List received = [];
+ TaskCompletionSource attached = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ using CancellationTokenSource streamCts = new();
+ Task reader = ReadFeedAsync(monitor, received, attached, streamCts.Token);
+ await attached.Task.WaitAsync(WaitTimeout);
+
+ sessions.SnapshotTruncated = false;
+ sessions.EmitEvent(ProviderModeProbe(1));
+
+ await WaitUntilAsync(
+ () => CountOf(received, AlarmFeedMessage.PayloadOneofCase.SnapshotStatus) >= 2,
+ WaitTimeout);
+
+ lock (received)
+ {
+ bool[] verdicts = received
+ .Where(m => m.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotStatus)
+ .Select(m => m.SnapshotStatus.Truncated)
+ .ToArray();
+ Assert.Equal([true, false], verdicts);
+ }
+
+ await streamCts.CancelAsync();
+ await reader;
+ await cts.CancelAsync();
+ await monitor.StopAsync(CancellationToken.None);
+ }
+
+ ///
+ /// Late-joiner priming while the verdict is truncated. The frame must
+ /// arrive after provider_status and before the cached
+ /// active_alarm frames: a consumer that applies the snapshot as it
+ /// streams needs the completeness caveat in hand before the records it
+ /// qualifies, not after.
+ ///
+ /// A task that represents the asynchronous operation.
+ [Fact]
+ public async Task SubscriberAttachingWhileTruncated_IsPrimedBeforeTheCachedSnapshot()
+ {
+ using GatewayMetrics metrics = new();
+ StubSessionManager sessions = new()
+ {
+ SnapshotTruncated = true,
+ Snapshots = [NewSnapshot("Galaxy!Area.Tank01.Level.HiHi", fromTruncatedSnapshot: true)],
+ };
+ using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
+
+ using CancellationTokenSource cts = new();
+ await monitor.StartAsync(cts.Token);
+ await sessions.WaitForReconcileAsync(WaitTimeout);
+ await WaitUntilAsync(() => monitor.SnapshotTruncated && monitor.CurrentAlarms.Count == 1, WaitTimeout);
+
+ AlarmFeedMessage[] preamble = await ReadPreambleAsync(monitor);
+
+ Assert.Equal(
+ [
+ AlarmFeedMessage.PayloadOneofCase.ProviderStatus,
+ AlarmFeedMessage.PayloadOneofCase.SnapshotStatus,
+ AlarmFeedMessage.PayloadOneofCase.ActiveAlarm,
+ AlarmFeedMessage.PayloadOneofCase.SnapshotComplete,
+ ],
+ preamble.Select(m => m.PayloadCase).ToArray());
+ Assert.True(preamble[1].SnapshotStatus.Truncated);
+
+ await cts.CancelAsync();
+ await monitor.StopAsync(CancellationToken.None);
+ }
+
+ ///
+ /// The control for the priming frame: it is unconditional, so a
+ /// subscriber attaching to a complete feed is told so explicitly rather
+ /// than having to infer "no frame means not truncated" — an inference
+ /// that is indistinguishable from a gateway that never sends the frame.
+ ///
+ /// A task that represents the asynchronous operation.
+ [Fact]
+ public async Task SubscriberAttachingWhileComplete_IsPrimedWithTheFalseVerdict()
+ {
+ using GatewayMetrics metrics = new();
+ StubSessionManager sessions = new();
+ using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
+
+ using CancellationTokenSource cts = new();
+ await monitor.StartAsync(cts.Token);
+ await sessions.WaitForReconcileAsync(WaitTimeout);
+
+ AlarmFeedMessage[] preamble = await ReadPreambleAsync(monitor);
+
+ Assert.Equal(
+ [
+ AlarmFeedMessage.PayloadOneofCase.ProviderStatus,
+ AlarmFeedMessage.PayloadOneofCase.SnapshotStatus,
+ AlarmFeedMessage.PayloadOneofCase.SnapshotComplete,
+ ],
+ preamble.Select(m => m.PayloadCase).ToArray());
+ Assert.False(preamble[1].SnapshotStatus.Truncated);
+
+ await cts.CancelAsync();
+ await monitor.StopAsync(CancellationToken.None);
+ }
+
+ ///
+ /// A monitor restart drops the truncation verdict with the cache
+ /// generation it describes. That drop is a completeness change like any
+ /// other, so subscribers — which outlive the monitor's worker session —
+ /// must see the clearing frame; otherwise a feed that silently re-seeds
+ /// leaves them caveating a set that is no longer truncated.
+ ///
+ /// A task that represents the asynchronous operation.
+ [Fact]
+ public async Task MonitorRestartClearingTruncation_EmitsTheClearingFrame()
+ {
+ using GatewayMetrics metrics = new();
+ StubSessionManager sessions = new() { SnapshotTruncated = true };
+ using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
+
+ using CancellationTokenSource cts = new();
+ await monitor.StartAsync(cts.Token);
+ await sessions.WaitForReconcileAsync(WaitTimeout);
+ await WaitUntilAsync(() => monitor.SnapshotTruncated, WaitTimeout);
+
+ List received = [];
+ TaskCompletionSource attached = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ using CancellationTokenSource streamCts = new();
+ Task reader = ReadFeedAsync(monitor, received, attached, streamCts.Token);
+ await attached.Task.WaitAsync(WaitTimeout);
+
+ // Ending the worker event stream faults the monitor lifecycle, which clears the cache.
+ sessions.EndWorkerEventStream();
+
+ await WaitUntilAsync(
+ () => CountOf(received, AlarmFeedMessage.PayloadOneofCase.SnapshotStatus) >= 2,
+ WaitTimeout);
+
+ lock (received)
+ {
+ // Only the first two: the supervisor re-opens the session after its backoff, and that
+ // second lifecycle legitimately re-reports the truncated verdict.
+ bool[] verdicts = received
+ .Where(m => m.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotStatus)
+ .Select(m => m.SnapshotStatus.Truncated)
+ .Take(2)
+ .ToArray();
+ Assert.Equal([true, false], verdicts);
+ }
+
+ await streamCts.CancelAsync();
+ await reader;
+ await cts.CancelAsync();
+ await monitor.StopAsync(CancellationToken.None);
+ }
+
+ private static int CountOf(List received, AlarmFeedMessage.PayloadOneofCase payloadCase)
+ {
+ lock (received)
+ {
+ return received.Count(m => m.PayloadCase == payloadCase);
+ }
+ }
+
+ // Drains the monitor's feed into received (every frame, preamble included), signalling gate
+ // once the subscriber is registered — that is, on its first frame.
+ private static Task ReadFeedAsync(
+ GatewayAlarmMonitor monitor,
+ List received,
+ TaskCompletionSource gate,
+ CancellationToken cancellationToken)
+ {
+ return Task.Run(
+ async () =>
+ {
+ try
+ {
+ await foreach (AlarmFeedMessage message in monitor.StreamAsync(null, cancellationToken))
+ {
+ lock (received)
+ {
+ received.Add(message);
+ }
+
+ gate.TrySetResult();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when the test cancels the stream.
+ }
+ },
+ CancellationToken.None);
+ }
+
+ // Reads one subscriber's open-time preamble: everything up to and including snapshot_complete.
+ private static async Task ReadPreambleAsync(GatewayAlarmMonitor monitor)
+ {
+ List preamble = [];
+ using CancellationTokenSource streamCts = new(WaitTimeout);
+ await foreach (AlarmFeedMessage message in monitor.StreamAsync(null, streamCts.Token))
+ {
+ preamble.Add(message);
+ if (message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotComplete)
+ {
+ break;
+ }
+ }
+
+ return [.. preamble];
+ }
+
+ // A no-op provider-mode event. The monitor forces an immediate reconcile after every one,
+ // which is how these tests drive a reconcile pass without waiting on the periodic timer.
+ private static MxEvent ProviderModeProbe(ulong sequence) => new()
+ {
+ Family = MxEventFamily.OnAlarmProviderModeChanged,
+ WorkerSequence = sequence,
+ OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent
+ {
+ Mode = AlarmProviderMode.Alarmmgr,
+ Reason = "probe",
+ At = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow),
+ },
+ };
+
private static ActiveAlarmSnapshot NewSnapshot(string reference, bool fromTruncatedSnapshot)
{
return new ActiveAlarmSnapshot
@@ -226,18 +523,43 @@ public sealed class AlarmTruncationSignalTests
private readonly Channel _events = Channel.CreateUnbounded();
private readonly TaskCompletionSource _reconciled =
new(TaskCreationOptions.RunContinuationsAsynchronously);
+ private readonly object _sync = new();
+ private bool _snapshotTruncated;
+ private IReadOnlyList _snapshots = [];
- /// Gets or sets the truncation verdict the scripted reply carries.
- public bool SnapshotTruncated { get; init; }
+ ///
+ /// Gets or sets the truncation verdict the scripted reply carries. Settable mid-test so
+ /// a suite can drive the verdict across reconciles and observe the feed-level edge.
+ ///
+ public bool SnapshotTruncated
+ {
+ get { lock (_sync) { return _snapshotTruncated; } }
+ set { lock (_sync) { _snapshotTruncated = value; } }
+ }
/// Gets or sets the snapshots the scripted reply carries.
- public IReadOnlyList Snapshots { get; init; } = [];
+ public IReadOnlyList Snapshots
+ {
+ get { lock (_sync) { return _snapshots; } }
+ set { lock (_sync) { _snapshots = value; } }
+ }
/// Completes once the monitor has issued its first QueryActiveAlarms.
/// The maximum time to wait.
/// A task that represents the asynchronous operation.
public Task WaitForReconcileAsync(TimeSpan timeout) => _reconciled.Task.WaitAsync(timeout);
+ /// Pushes a worker event into the session's distributor pump.
+ /// The event to push.
+ public void EmitEvent(MxEvent mxEvent) =>
+ _events.Writer.TryWrite(new WorkerEvent { Event = mxEvent });
+
+ ///
+ /// Ends the worker event stream, which faults the monitor's lifecycle exactly as a
+ /// worker exit would and takes it through its cache-clearing teardown.
+ ///
+ public void EndWorkerEventStream() => _events.Writer.TryComplete();
+
///
public Task OpenSessionAsync(
SessionOpenRequest request,
@@ -275,8 +597,13 @@ public sealed class AlarmTruncationSignalTests
if (command.Command?.Kind == MxCommandKind.QueryActiveAlarms)
{
- QueryActiveAlarmsReplyPayload payload = new() { SnapshotTruncated = SnapshotTruncated };
- payload.Snapshots.AddRange(Snapshots.Select(snapshot => snapshot.Clone()));
+ QueryActiveAlarmsReplyPayload payload = new();
+ lock (_sync)
+ {
+ payload.SnapshotTruncated = _snapshotTruncated;
+ payload.Snapshots.AddRange(_snapshots.Select(snapshot => snapshot.Clone()));
+ }
+
reply.QueryActiveAlarms = payload;
_reconciled.TrySetResult();
}