diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/TelemetryProtoContract.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/TelemetryProtoContract.cs
new file mode 100644
index 00000000..b251a542
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/TelemetryProtoContract.cs
@@ -0,0 +1,25 @@
+using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
+
+namespace ZB.MOM.WW.OtOpcUa.Commons.Protos;
+
+///
+/// The single source of truth for which variants the
+/// Phase 5 telemetry transport actually handles. Both the contract-lock test (which proves this
+/// list stays in lock-step with the generated oneof) and the later oneof↔domain converter
+/// reference this array, so adding a fifth channel to telemetry.proto forces a matching
+/// entry here or the build/test goes red.
+///
+public static class TelemetryProtoContract
+{
+ ///
+ /// Exactly the four telemetry oneof cases mirrored from the domain records:
+ /// alerts / script-logs / driver-health / driver-resilience-status.
+ ///
+ public static readonly TelemetryEvent.EventOneofCase[] HandledCases =
+ [
+ TelemetryEvent.EventOneofCase.AlarmTransition,
+ TelemetryEvent.EventOneofCase.ScriptLog,
+ TelemetryEvent.EventOneofCase.DriverHealth,
+ TelemetryEvent.EventOneofCase.DriverResilience,
+ ];
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/telemetry.proto b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/telemetry.proto
new file mode 100644
index 00000000..0997fb2e
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/telemetry.proto
@@ -0,0 +1,83 @@
+syntax = "proto3";
+
+package telemetry.v1;
+
+option csharp_namespace = "ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1";
+
+import "google/protobuf/timestamp.proto";
+
+// Per-cluster mesh Phase 5: the four live-telemetry channels that used to fan out over
+// DistributedPubSub (alerts / script-logs / driver-health / driver-resilience-status) now travel over
+// ONE gRPC server-streaming contract. Central dials each driver node and opens Subscribe; the node
+// streams its own live telemetry as a sequence of TelemetryEvent envelopes.
+//
+// Additive-only field evolution: never renumber or reuse a tag; a pre-field peer must read a new
+// field's proto3 default correctly (that is why nullable domain fields are modelled with `optional`
+// for explicit presence). Locked by the contract test in the Commons test project.
+service TelemetryStreamService {
+ rpc Subscribe(TelemetryStreamRequest) returns (stream TelemetryEvent);
+}
+
+message TelemetryStreamRequest {
+ string correlation_id = 1;
+}
+
+message TelemetryEvent {
+ string correlation_id = 1;
+ oneof event {
+ AlarmTransition alarm_transition = 2; // <- "alerts" / AlarmTransitionEvent
+ ScriptLog script_log = 3; // <- "script-logs" / ScriptLogEntry
+ DriverHealth driver_health = 4; // <- "driver-health" / DriverHealthChanged
+ DriverResilienceStatus driver_resilience = 5; // <- "driver-resilience-status" / DriverResilienceStatusChanged
+ }
+}
+
+// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Alerts.AlarmTransitionEvent.
+message AlarmTransition {
+ string alarm_id = 1;
+ string equipment_path = 2;
+ string alarm_name = 3;
+ string transition_kind = 4; // string in the record (Activated/Cleared/...); kept as string
+ int32 severity = 5;
+ string message = 6;
+ string user = 7;
+ google.protobuf.Timestamp timestamp_utc = 8;
+ string alarm_type_name = 9; // record default "AlarmCondition"
+ optional string comment = 10; // nullable in the record — presence distinguishes null from ""
+ optional bool historize_to_aveva = 11; // bool? in the record — three states (null / true / false)
+ repeated string referencing_equipment_paths = 12; // null in the record is treated as empty
+}
+
+// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging.ScriptLogEntry.
+message ScriptLog {
+ string script_id = 1;
+ string level = 2; // string in the record (Trace/Debug/Information/...); kept as string
+ string message = 3;
+ google.protobuf.Timestamp timestamp_utc = 4;
+ optional string virtual_tag_id = 5; // nullable in the record
+ optional string alarm_id = 6; // nullable in the record
+ optional string equipment_id = 7; // nullable in the record
+}
+
+// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers.DriverHealthChanged.
+message DriverHealth {
+ string cluster_id = 1;
+ string driver_instance_id = 2;
+ string state = 3; // DriverState-as-string in the record; kept as string
+ google.protobuf.Timestamp last_successful_read_utc = 4; // DateTime? — absent Timestamp encodes null
+ optional string last_error = 5; // nullable in the record
+ int32 error_count_5min = 6;
+ google.protobuf.Timestamp published_utc = 7;
+}
+
+// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers.DriverResilienceStatusChanged.
+message DriverResilienceStatus {
+ string driver_instance_id = 1;
+ string host_name = 2;
+ bool breaker_open = 3;
+ int32 consecutive_failures = 4;
+ int32 current_in_flight = 5;
+ google.protobuf.Timestamp last_breaker_open_utc = 6; // DateTime? — absent Timestamp encodes null
+ google.protobuf.Timestamp last_sampled_utc = 7;
+ google.protobuf.Timestamp published_utc = 8;
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/ZB.MOM.WW.OtOpcUa.Commons.csproj b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/ZB.MOM.WW.OtOpcUa.Commons.csproj
index acad5d43..5e55544f 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/ZB.MOM.WW.OtOpcUa.Commons.csproj
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/ZB.MOM.WW.OtOpcUa.Commons.csproj
@@ -27,6 +27,7 @@
+
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/TelemetryProtoContractTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/TelemetryProtoContractTests.cs
new file mode 100644
index 00000000..855bb17a
--- /dev/null
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/TelemetryProtoContractTests.cs
@@ -0,0 +1,32 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Commons.Protos;
+using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
+
+namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;
+
+///
+/// Contract-lock for the Phase 5 telemetry oneof. If a fifth variant is added to
+/// telemetry.proto without adding a matching entry to
+/// , this test goes red — the single guard that
+/// keeps the wire contract and the handled-case set from silently drifting apart.
+///
+public class TelemetryProtoContractTests
+{
+ [Fact]
+ public void EveryOneofVariant_IsAccountedFor()
+ {
+ var variants = System.Enum.GetValues()
+ .Where(c => c != TelemetryEvent.EventOneofCase.None).ToArray();
+
+ variants.ShouldBe(TelemetryProtoContract.HandledCases, ignoreOrder: true);
+ }
+
+ [Fact]
+ public void The_service_base_and_client_types_generate()
+ {
+ // Referencing these types is the whole assertion — GrpcServices="Both" must emit both.
+ typeof(TelemetryStreamService.TelemetryStreamServiceBase).ShouldNotBeNull();
+ typeof(TelemetryStreamService.TelemetryStreamServiceClient).ShouldNotBeNull();
+ }
+}