From b0e65d4f31f8847dd8b1b4a80d18e12c6349f412 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 9 Aug 2026 19:47:00 -0400 Subject: [PATCH 1/3] feat(worker): correlate OnWriteComplete onto plain Write/Write2 replies (06/S-1 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OtOpcUa's dominant FreeAccess write path goes out as MX_COMMAND_KIND_WRITE, not WriteSecured — the original 06/S-1 brief mis-scoped the correlation, so a refused plain write was invisible on the unary reply (verified live on windev 2026-08-09). ExecuteWrite/ExecuteWrite2 now use the same pre-call version baseline + bounded pump-wait as the secured kinds. Bulk writes stay fire-and-forget. --- .../Generated/MxaccessGateway.cs | 25 ++-- .../Protos/mxaccess_gateway.proto | 21 +-- .../MxAccess/MxAccessCommandExecutorTests.cs | 127 +++++++++++++++--- .../MxAccess/MxAccessCommandExecutor.cs | 32 ++++- 4 files changed, 170 insertions(+), 35 deletions(-) diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs index 400e221..bbaa9ff 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs @@ -8012,6 +8012,11 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto { } + /// + /// The unary reply's statuses field carries the correlated OnWriteComplete + /// outcome when it arrives within the worker's bounded wait — see + /// MxCommandReply.statuses. + /// [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class WriteCommand : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE @@ -8330,6 +8335,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto { } + /// + /// Same statuses correlation as WriteCommand. + /// [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class Write2Command : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE @@ -17215,18 +17223,19 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto { = pb::FieldCodec.ForMessage(58, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusProxy.Parser); private readonly pbc::RepeatedField statuses_ = new pbc::RepeatedField(); /// - /// Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2 - /// replies the worker holds the reply for a bounded window (default 1.5 s, - /// MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching - /// MXAccess OnWriteComplete callback and copies its status rows here, so - /// statuses[0] carries the real MXAccess commit outcome (success OR failure) - /// while protocol_status/hresult still describe command acceptance only. - /// Empty statuses on a write reply means the completion did not arrive + /// Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED / + /// WRITE_SECURED2 replies the worker holds the reply for a bounded window + /// (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for + /// the matching MXAccess OnWriteComplete callback and copies its status rows + /// here, so statuses[0] carries the real MXAccess commit outcome (success OR + /// failure) while protocol_status/hresult still describe command acceptance + /// only. Empty statuses on a write reply means the completion did not arrive /// within the window — the write is unconfirmed, not failed. Correlation is /// best-effort per (server_handle, item_handle): MXAccess's callback carries /// no transaction id, so concurrent writes to the same item within the /// window can swap rows. The OnWriteComplete event still flows on the event - /// stream unchanged. Other command kinds leave this field as before. + /// stream unchanged. Bulk write kinds and all non-write kinds leave this + /// field as before. /// [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 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 ee923b5..db5fb4f 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto @@ -241,6 +241,9 @@ message ActivateCommand { int32 item_handle = 2; } +// The unary reply's statuses field carries the correlated OnWriteComplete +// outcome when it arrives within the worker's bounded wait — see +// MxCommandReply.statuses. message WriteCommand { int32 server_handle = 1; int32 item_handle = 2; @@ -248,6 +251,7 @@ message WriteCommand { int32 user_id = 4; } +// Same statuses correlation as WriteCommand. message Write2Command { int32 server_handle = 1; int32 item_handle = 2; @@ -531,18 +535,19 @@ message MxCommandReply { // transport failures. optional int32 hresult = 5; MxValue return_value = 6; - // Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2 - // replies the worker holds the reply for a bounded window (default 1.5 s, - // MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching - // MXAccess OnWriteComplete callback and copies its status rows here, so - // statuses[0] carries the real MXAccess commit outcome (success OR failure) - // while protocol_status/hresult still describe command acceptance only. - // Empty statuses on a write reply means the completion did not arrive + // Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED / + // WRITE_SECURED2 replies the worker holds the reply for a bounded window + // (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for + // the matching MXAccess OnWriteComplete callback and copies its status rows + // here, so statuses[0] carries the real MXAccess commit outcome (success OR + // failure) while protocol_status/hresult still describe command acceptance + // only. Empty statuses on a write reply means the completion did not arrive // within the window — the write is unconfirmed, not failed. Correlation is // best-effort per (server_handle, item_handle): MXAccess's callback carries // no transaction id, so concurrent writes to the same item within the // window can swap rows. The OnWriteComplete event still flows on the event - // stream unchanged. Other command kinds leave this field as before. + // stream unchanged. Bulk write kinds and all non-write kinds leave this + // field as before. repeated MxStatusProxy statuses = 7; string diagnostic_message = 8; diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs index 34f9406..a8ac61d 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs @@ -851,6 +851,9 @@ public sealed class MxAccessCommandExecutorTests FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); + // No completion source in this test — disable the bounded reply wait + // so the forwarding assertions don't pay the default 1.5 s timeout. + session.WriteCompletionTimeout = TimeSpan.Zero; await session.StartAsync(workerProcessId: 1234); MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand( @@ -874,6 +877,8 @@ public sealed class MxAccessCommandExecutorTests FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); + // Same rationale as the Write forwarding test above. + session.WriteCompletionTimeout = TimeSpan.Zero; await session.StartAsync(workerProcessId: 1234); DateTime timestamp = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc); @@ -952,7 +957,7 @@ public sealed class MxAccessCommandExecutorTests FakeMxAccessComObject fakeComObject = new(registerHandle: 82); FakeMxAccessComObjectFactory factory = new(fakeComObject); CompletionCacheEventSink sink = new(); - fakeComObject.OnWriteSecuredCallback = () => + fakeComObject.OnWriteCallback = () => sink.WriteCompletionCache.Record(82, 820, CreateCompletionRows(detail: 4321)); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, sink); @@ -989,7 +994,7 @@ public sealed class MxAccessCommandExecutorTests // baseline is committed and a Record from the test thread is // guaranteed to be "newer" — no fixed sleep racing the STA thread. using System.Threading.ManualResetEventSlim comCallReached = new(initialState: false); - fakeComObject.OnWriteSecuredCallback = () => comCallReached.Set(); + fakeComObject.OnWriteCallback = () => comCallReached.Set(); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, sink); session.WriteCompletionTimeout = TimeSpan.FromSeconds(10); @@ -1068,7 +1073,7 @@ public sealed class MxAccessCommandExecutorTests FakeMxAccessComObject fakeComObject = new(registerHandle: 86); FakeMxAccessComObjectFactory factory = new(fakeComObject); CompletionCacheEventSink sink = new(); - fakeComObject.OnWriteSecuredCallback = () => + fakeComObject.OnWriteCallback = () => sink.WriteCompletionCache.Record(86, 860, CreateCompletionRows(detail: 2222)); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, sink); @@ -1085,25 +1090,110 @@ public sealed class MxAccessCommandExecutorTests } /// - /// Verifies plain Write stays fire-and-forget: even with a huge completion - /// timeout configured and no completion source, the reply returns - /// immediately (guarded well under the configured wait) with empty - /// statuses — only the secured write kinds enter the bounded wait. + /// Verifies plain Write correlates the same way as WriteSecured + /// (fast-completion edge): OtOpcUa's dominant FreeAccess path goes out + /// as MX_COMMAND_KIND_WRITE, so its reply must carry the OnWriteComplete + /// rows too. /// /// A task that represents the asynchronous operation. [Fact] - public async Task DispatchAsync_Write_DoesNotWaitForCompletion() + public async Task DispatchAsync_Write_WhenCompletionArrivesDuringComCall_ReturnsStatuses() { FakeMxAccessComObject fakeComObject = new(registerHandle: 87); FakeMxAccessComObjectFactory factory = new(fakeComObject); CompletionCacheEventSink sink = new(); + fakeComObject.OnWriteCallback = () => + sink.WriteCompletionCache.Record(87, 870, CreateCompletionRows(detail: 5555)); + using StaRuntime runtime = CreateRuntime(); + using MxAccessStaSession session = new(runtime, factory, sink); + // Hermetic: don't inherit MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS + // from the test runner's environment. + session.WriteCompletionTimeout = TimeSpan.FromSeconds(10); + await session.StartAsync(workerProcessId: 1234); + + MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand( + "plain-write-fast", serverHandle: 87, itemHandle: 870, value: 1, userId: 5)); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.True(reply.HasHresult); + Assert.Equal(0, reply.Hresult); + MxStatusProxy row = Assert.Single(reply.Statuses); + Assert.Equal(5555, row.Detail); + Assert.Equal(MxStatusCategory.Ok, row.Category); + } + + /// + /// Verifies the plain-Write timeout fallback mirrors the secured one: + /// no completion within the bounded wait returns protocol OK with EMPTY + /// statuses — unconfirmed, never a synthesized failure row. + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task DispatchAsync_Write_WhenNoCompletion_TimesOutWithEmptyStatusesAndOkProtocol() + { + FakeMxAccessComObject fakeComObject = new(registerHandle: 88); + FakeMxAccessComObjectFactory factory = new(fakeComObject); + CompletionCacheEventSink sink = new(); + using StaRuntime runtime = CreateRuntime(); + using MxAccessStaSession session = new(runtime, factory, sink); + session.WriteCompletionTimeout = TimeSpan.FromMilliseconds(100); + await session.StartAsync(workerProcessId: 1234); + + MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand( + "plain-write-timeout", serverHandle: 88, itemHandle: 880, value: 1, userId: 5)); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.True(reply.HasHresult); + Assert.Equal(0, reply.Hresult); + Assert.Empty(reply.Statuses); + } + + /// + /// Verifies Write2 correlates the same way as Write (fast-completion + /// edge). + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task DispatchAsync_Write2_WhenCompletionArrivesDuringComCall_ReturnsStatuses() + { + FakeMxAccessComObject fakeComObject = new(registerHandle: 89); + FakeMxAccessComObjectFactory factory = new(fakeComObject); + CompletionCacheEventSink sink = new(); + fakeComObject.OnWriteCallback = () => + sink.WriteCompletionCache.Record(89, 890, CreateCompletionRows(detail: 6666)); + using StaRuntime runtime = CreateRuntime(); + using MxAccessStaSession session = new(runtime, factory, sink); + session.WriteCompletionTimeout = TimeSpan.FromSeconds(10); + await session.StartAsync(workerProcessId: 1234); + + MxCommandReply reply = await session.DispatchAsync(CreateWrite2Command( + "plain-write2-fast", serverHandle: 89, itemHandle: 890, value: 1, + timestamp: new DateTime(2026, 8, 9, 12, 0, 0, DateTimeKind.Utc), userId: 6)); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.Equal(6666, Assert.Single(reply.Statuses).Detail); + } + + /// + /// Verifies WriteBulk stays fire-and-forget: even with a huge completion + /// timeout configured and no completion source, the reply returns + /// immediately with per-entry results — bulk writes never enter the + /// bounded wait (latency for high-rate loops). + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task DispatchAsync_WriteBulk_DoesNotWaitForCompletion() + { + FakeMxAccessComObject fakeComObject = new(registerHandle: 90); + FakeMxAccessComObjectFactory factory = new(fakeComObject); + CompletionCacheEventSink sink = new(); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, sink); session.WriteCompletionTimeout = TimeSpan.FromSeconds(30); await session.StartAsync(workerProcessId: 1234); - Task pending = session.DispatchAsync(CreateWriteCommand( - "plain-write-no-wait", serverHandle: 87, itemHandle: 870, value: 1, userId: 5)); + Task pending = session.DispatchAsync(CreateWriteBulkCommand( + "bulk-write-no-wait", serverHandle: 90, entries: new[] { (itemHandle: 900, value: 1, userId: 5) })); Task completed = await Task.WhenAny(pending, Task.Delay(TimeSpan.FromSeconds(5))); Assert.Same(pending, completed); @@ -2004,12 +2094,13 @@ public sealed class MxAccessCommandExecutorTests private readonly List operationNames = new(); /// - /// Invoked at the end of a successful WriteSecured/WriteSecured2 — - /// stands in for MXAccess committing synchronously and delivering - /// OnWriteComplete while the COM call is still on the stack, so - /// tests can exercise the fast-completion ordering edge. + /// Invoked at the end of a successful Write/Write2/WriteSecured/ + /// WriteSecured2 — stands in for MXAccess committing synchronously + /// and delivering OnWriteComplete while the COM call is still on + /// the stack, so tests can exercise the fast-completion ordering + /// edge on every correlated write kind. /// - public Action? OnWriteSecuredCallback { get; set; } + public Action? OnWriteCallback { get; set; } /// Initializes a fake MXAccess COM object with the given handles and optional exceptions. /// Return value for Register method. @@ -2285,6 +2376,7 @@ public sealed class MxAccessCommandExecutorTests WriteUserId = userId; WriteThreadId = Environment.CurrentManagedThreadId; ThrowIfWriteFailureConfigured(itemHandle); + OnWriteCallback?.Invoke(); } /// @@ -2303,6 +2395,7 @@ public sealed class MxAccessCommandExecutorTests WriteUserId = userId; WriteThreadId = Environment.CurrentManagedThreadId; ThrowIfWriteFailureConfigured(itemHandle); + OnWriteCallback?.Invoke(); } /// @@ -2321,7 +2414,7 @@ public sealed class MxAccessCommandExecutorTests WriteValue = value; WriteThreadId = Environment.CurrentManagedThreadId; ThrowIfWriteFailureConfigured(itemHandle); - OnWriteSecuredCallback?.Invoke(); + OnWriteCallback?.Invoke(); } /// @@ -2342,7 +2435,7 @@ public sealed class MxAccessCommandExecutorTests WriteTimestamp = timestamp; WriteThreadId = Environment.CurrentManagedThreadId; ThrowIfWriteFailureConfigured(itemHandle); - OnWriteSecuredCallback?.Invoke(); + OnWriteCallback?.Invoke(); } private void ThrowIfWriteFailureConfigured(int itemHandle) diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs index 6b061bd..9c36df2 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs @@ -427,13 +427,28 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor return CreateInvalidRequestReply(command, "Write command value is required."); } + // Same pre-call baseline rule as ExecuteWriteSecured: plain Write is + // also fire-and-forget in MXAccess, and its commit outcome only exists + // in the later OnWriteComplete callback. + MxAccessWriteCompletionCache completionCache = session.WriteCompletionCache; + ulong completionBaseline = completionCache.CurrentVersion( + writeCommand.ServerHandle, + writeCommand.ItemHandle); + session.Write( writeCommand.ServerHandle, writeCommand.ItemHandle, variantConverter.ConvertToComValue(writeCommand.Value), writeCommand.UserId); - return CreateOkReply(command); + MxCommandReply reply = CreateOkReply(command); + AwaitWriteCompletion( + reply, + completionCache, + writeCommand.ServerHandle, + writeCommand.ItemHandle, + completionBaseline); + return reply; } private MxCommandReply ExecuteWrite2(StaCommand command) @@ -454,6 +469,12 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor return CreateInvalidRequestReply(command, "Write2 command timestamp value is required."); } + // Same pre-call baseline rule as ExecuteWriteSecured. + MxAccessWriteCompletionCache completionCache = session.WriteCompletionCache; + ulong completionBaseline = completionCache.CurrentVersion( + write2Command.ServerHandle, + write2Command.ItemHandle); + session.Write2( write2Command.ServerHandle, write2Command.ItemHandle, @@ -461,7 +482,14 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor variantConverter.ConvertToComValue(write2Command.TimestampValue), write2Command.UserId); - return CreateOkReply(command); + MxCommandReply reply = CreateOkReply(command); + AwaitWriteCompletion( + reply, + completionCache, + write2Command.ServerHandle, + write2Command.ItemHandle, + completionBaseline); + return reply; } private MxCommandReply ExecuteWriteSecured(StaCommand command) From 794c44246a2c64230850a454871e892b47ea19b3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 9 Aug 2026 19:47:04 -0400 Subject: [PATCH 2/3] chore(clients): regenerate Go/Java bindings and Rust vendored proto for the plain-write statuses comments --- .../internal/generated/mxaccess_gateway.pb.go | 21 +- .../mxaccess_gateway/v1/MxaccessGateway.java | 496 ++++++++++-------- clients/rust/protos/mxaccess_gateway.proto | 21 +- 3 files changed, 298 insertions(+), 240 deletions(-) diff --git a/clients/go/internal/generated/mxaccess_gateway.pb.go b/clients/go/internal/generated/mxaccess_gateway.pb.go index 2064cc0..d0f041c 100644 --- a/clients/go/internal/generated/mxaccess_gateway.pb.go +++ b/clients/go/internal/generated/mxaccess_gateway.pb.go @@ -2553,6 +2553,9 @@ func (x *ActivateCommand) GetItemHandle() int32 { return 0 } +// The unary reply's statuses field carries the correlated OnWriteComplete +// outcome when it arrives within the worker's bounded wait — see +// MxCommandReply.statuses. type WriteCommand struct { state protoimpl.MessageState `protogen:"open.v1"` ServerHandle int32 `protobuf:"varint,1,opt,name=server_handle,json=serverHandle,proto3" json:"server_handle,omitempty"` @@ -2621,6 +2624,7 @@ func (x *WriteCommand) GetUserId() int32 { return 0 } +// Same statuses correlation as WriteCommand. type Write2Command struct { state protoimpl.MessageState `protogen:"open.v1"` ServerHandle int32 `protobuf:"varint,1,opt,name=server_handle,json=serverHandle,proto3" json:"server_handle,omitempty"` @@ -4583,18 +4587,19 @@ type MxCommandReply struct { // transport failures. Hresult *int32 `protobuf:"varint,5,opt,name=hresult,proto3,oneof" json:"hresult,omitempty"` ReturnValue *MxValue `protobuf:"bytes,6,opt,name=return_value,json=returnValue,proto3" json:"return_value,omitempty"` - // Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2 - // replies the worker holds the reply for a bounded window (default 1.5 s, - // MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching - // MXAccess OnWriteComplete callback and copies its status rows here, so - // statuses[0] carries the real MXAccess commit outcome (success OR failure) - // while protocol_status/hresult still describe command acceptance only. - // Empty statuses on a write reply means the completion did not arrive + // Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED / + // WRITE_SECURED2 replies the worker holds the reply for a bounded window + // (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for + // the matching MXAccess OnWriteComplete callback and copies its status rows + // here, so statuses[0] carries the real MXAccess commit outcome (success OR + // failure) while protocol_status/hresult still describe command acceptance + // only. Empty statuses on a write reply means the completion did not arrive // within the window — the write is unconfirmed, not failed. Correlation is // best-effort per (server_handle, item_handle): MXAccess's callback carries // no transaction id, so concurrent writes to the same item within the // window can swap rows. The OnWriteComplete event still flows on the event - // stream unchanged. Other command kinds leave this field as before. + // stream unchanged. Bulk write kinds and all non-write kinds leave this + // field as before. Statuses []*MxStatusProxy `protobuf:"bytes,7,rep,name=statuses,proto3" json:"statuses,omitempty"` DiagnosticMessage string `protobuf:"bytes,8,opt,name=diagnostic_message,json=diagnosticMessage,proto3" json:"diagnostic_message,omitempty"` // Types that are valid to be assigned to Payload: diff --git a/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java b/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java index a69750f..85a91b0 100644 --- a/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java +++ b/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java @@ -24235,6 +24235,12 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { int getUserId(); } /** + *
+   * The unary reply's statuses field carries the correlated OnWriteComplete
+   * outcome when it arrives within the worker's bounded wait — see
+   * MxCommandReply.statuses.
+   * 
+ * * Protobuf type {@code mxaccess_gateway.v1.WriteCommand} */ public static final class WriteCommand extends @@ -24527,6 +24533,12 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return builder; } /** + *
+     * The unary reply's statuses field carries the correlated OnWriteComplete
+     * outcome when it arrives within the worker's bounded wait — see
+     * MxCommandReply.statuses.
+     * 
+ * * Protobuf type {@code mxaccess_gateway.v1.WriteCommand} */ public static final class Builder extends @@ -25036,6 +25048,10 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { int getUserId(); } /** + *
+   * Same statuses correlation as WriteCommand.
+   * 
+ * * Protobuf type {@code mxaccess_gateway.v1.Write2Command} */ public static final class Write2Command extends @@ -25370,6 +25386,10 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return builder; } /** + *
+     * Same statuses correlation as WriteCommand.
+     * 
+ * * Protobuf type {@code mxaccess_gateway.v1.Write2Command} */ public static final class Builder extends @@ -50513,18 +50533,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -50533,18 +50554,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { getStatusesList(); /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -50552,18 +50574,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy getStatuses(int index); /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -50571,18 +50594,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { int getStatusesCount(); /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -50591,18 +50615,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { getStatusesOrBuilderList(); /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -51333,18 +51358,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { private java.util.List statuses_; /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -51355,18 +51381,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -51378,18 +51405,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -51400,18 +51428,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -51422,18 +51451,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-     * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-     * replies the worker holds the reply for a bounded window (default 1.5 s,
-     * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-     * MXAccess OnWriteComplete callback and copies its status rows here, so
-     * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-     * while protocol_status/hresult still describe command acceptance only.
-     * Empty statuses on a write reply means the completion did not arrive
+     * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+     * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+     * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+     * the matching MXAccess OnWriteComplete callback and copies its status rows
+     * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+     * failure) while protocol_status/hresult still describe command acceptance
+     * only. Empty statuses on a write reply means the completion did not arrive
      * within the window — the write is unconfirmed, not failed. Correlation is
      * best-effort per (server_handle, item_handle): MXAccess's callback carries
      * no transaction id, so concurrent writes to the same item within the
      * window can swap rows. The OnWriteComplete event still flows on the event
-     * stream unchanged. Other command kinds leave this field as before.
+     * stream unchanged. Bulk write kinds and all non-write kinds leave this
+     * field as before.
      * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54156,18 +54186,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54181,18 +54212,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54206,18 +54238,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54231,18 +54264,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54263,18 +54297,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54292,18 +54327,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54323,18 +54359,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54355,18 +54392,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54384,18 +54422,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54413,18 +54452,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54443,18 +54483,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54471,18 +54512,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54499,18 +54541,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54521,18 +54564,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54546,18 +54590,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54572,18 +54617,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54594,18 +54640,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; @@ -54617,18 +54664,19 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-       * Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
-       * replies the worker holds the reply for a bounded window (default 1.5 s,
-       * MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
-       * MXAccess OnWriteComplete callback and copies its status rows here, so
-       * statuses[0] carries the real MXAccess commit outcome (success OR failure)
-       * while protocol_status/hresult still describe command acceptance only.
-       * Empty statuses on a write reply means the completion did not arrive
+       * Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
+       * WRITE_SECURED2 replies the worker holds the reply for a bounded window
+       * (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
+       * the matching MXAccess OnWriteComplete callback and copies its status rows
+       * here, so statuses[0] carries the real MXAccess commit outcome (success OR
+       * failure) while protocol_status/hresult still describe command acceptance
+       * only. Empty statuses on a write reply means the completion did not arrive
        * within the window — the write is unconfirmed, not failed. Correlation is
        * best-effort per (server_handle, item_handle): MXAccess's callback carries
        * no transaction id, so concurrent writes to the same item within the
        * window can swap rows. The OnWriteComplete event still flows on the event
-       * stream unchanged. Other command kinds leave this field as before.
+       * stream unchanged. Bulk write kinds and all non-write kinds leave this
+       * field as before.
        * 
* * repeated .mxaccess_gateway.v1.MxStatusProxy statuses = 7; diff --git a/clients/rust/protos/mxaccess_gateway.proto b/clients/rust/protos/mxaccess_gateway.proto index ee923b5..db5fb4f 100644 --- a/clients/rust/protos/mxaccess_gateway.proto +++ b/clients/rust/protos/mxaccess_gateway.proto @@ -241,6 +241,9 @@ message ActivateCommand { int32 item_handle = 2; } +// The unary reply's statuses field carries the correlated OnWriteComplete +// outcome when it arrives within the worker's bounded wait — see +// MxCommandReply.statuses. message WriteCommand { int32 server_handle = 1; int32 item_handle = 2; @@ -248,6 +251,7 @@ message WriteCommand { int32 user_id = 4; } +// Same statuses correlation as WriteCommand. message Write2Command { int32 server_handle = 1; int32 item_handle = 2; @@ -531,18 +535,19 @@ message MxCommandReply { // transport failures. optional int32 hresult = 5; MxValue return_value = 6; - // Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2 - // replies the worker holds the reply for a bounded window (default 1.5 s, - // MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching - // MXAccess OnWriteComplete callback and copies its status rows here, so - // statuses[0] carries the real MXAccess commit outcome (success OR failure) - // while protocol_status/hresult still describe command acceptance only. - // Empty statuses on a write reply means the completion did not arrive + // Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED / + // WRITE_SECURED2 replies the worker holds the reply for a bounded window + // (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for + // the matching MXAccess OnWriteComplete callback and copies its status rows + // here, so statuses[0] carries the real MXAccess commit outcome (success OR + // failure) while protocol_status/hresult still describe command acceptance + // only. Empty statuses on a write reply means the completion did not arrive // within the window — the write is unconfirmed, not failed. Correlation is // best-effort per (server_handle, item_handle): MXAccess's callback carries // no transaction id, so concurrent writes to the same item within the // window can swap rows. The OnWriteComplete event still flows on the event - // stream unchanged. Other command kinds leave this field as before. + // stream unchanged. Bulk write kinds and all non-write kinds leave this + // field as before. repeated MxStatusProxy statuses = 7; string diagnostic_message = 8; From 91d8715c74ed0d3084ec0c914747688807c75b47 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 9 Aug 2026 19:47:04 -0400 Subject: [PATCH 3/3] docs: write-completion correlation covers all four unary write kinds --- docs/DesignDecisions.md | 15 ++++++++++----- docs/GatewayConfiguration.md | 2 +- gateway.md | 9 +++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/DesignDecisions.md b/docs/DesignDecisions.md index 6b14485..fcaeafd 100644 --- a/docs/DesignDecisions.md +++ b/docs/DesignDecisions.md @@ -534,7 +534,7 @@ against the live MXAccess attribute set. - [Alarm Client Discovery — Subtag provider](./AlarmClientDiscovery.md) - [gRPC Contract — provider_status and degraded fields](./Grpc.md) -## Secured-Write Completion Correlation +## Write Completion Correlation MXAccess writes are fire-and-forget: the toolkit call returns before the Galaxy commit, and the per-item outcome only exists in the later @@ -542,7 +542,8 @@ Galaxy commit, and the per-item outcome only exists in the later proved worker-side command acceptance only, forcing consumers (OtOpcUa's GalaxyDriver) to report every write as provisionally good. -For `WriteSecured`/`WriteSecured2` the worker now holds the unary reply for a +For the unary write kinds (`Write`/`Write2`/`WriteSecured`/`WriteSecured2`) +the worker now holds the unary reply for a bounded window (`MxGateway:Worker:WriteCompletionWaitMilliseconds`, default 1.5 s, `0` disables; conveyed to the worker via `MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS`) and copies the matching @@ -565,9 +566,13 @@ callback's status rows onto `MxCommandReply.statuses`. Key choices, argued in acceptance; the MXAccess outcome (success or failure) rides only in `statuses[0]`; the `OnWriteComplete` event still streams unchanged (nothing swallowed, nothing synthesized). -- **Scope: secured writes only.** Plain `Write`/`Write2` and bulk writes stay - fire-and-forget — the wait would add a device round-trip per write to - high-rate supervisory loops. +- **Scope: all four unary write kinds; bulk writes stay fire-and-forget.** + The first cut correlated `WriteSecured`/`WriteSecured2` only, but OtOpcUa's + dominant FreeAccess write path goes out as plain `Write` (2026-08-09 live + verification, 06/S-1) — a refused plain write was invisible on the reply. + Plain `Write`/`Write2` now correlate identically. Bulk writes keep + fire-and-forget replies: waiting per entry would add a device round-trip per + item to high-rate supervisory loops. - **Best-effort correlation.** The callback carries only `(hItem, statuses)` — no transaction id — so concurrent writes to the same item within the window can swap rows; benign for the serialized single-write diff --git a/docs/GatewayConfiguration.md b/docs/GatewayConfiguration.md index 58ca7a3..781c50f 100644 --- a/docs/GatewayConfiguration.md +++ b/docs/GatewayConfiguration.md @@ -114,7 +114,7 @@ launch CWD (SEC-01, SEC-33). | `MxGateway:Worker:StartupProbeRetryAttempts` | `3` | Number of retry attempts for transient worker startup probe failures before pipe connection and handshake continue. | | `MxGateway:Worker:StartupProbeRetryDelayMilliseconds` | `250` | Delay between transient startup probe retry attempts. | | `MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds` | `2000` | Per-attempt timeout used by the worker named-pipe connect retry path. The overall pipe connection still stays under the startup budget. | -| `MxGateway:Worker:WriteCompletionWaitMilliseconds` | `1500` | Bounded wait the worker holds a `WriteSecured`/`WriteSecured2` reply for the matching MXAccess `OnWriteComplete` callback, so the reply's `statuses` carry the real commit outcome. `0` disables the wait (pure fire-and-forget replies). Must be `>= 0`. The gateway conveys the value to the worker via the `MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS` environment variable. Consumers that time their own writes must budget above this wait: OtOpcUa's GalaxyDriver wraps gateway writes in a 2 s Tier A resilience timeout, so a deployment raising this option past ~2000 must raise that driver `ResilienceConfig` write timeout in step or slow-but-successful commits surface as consumer-side failures. | +| `MxGateway:Worker:WriteCompletionWaitMilliseconds` | `1500` | Bounded wait the worker holds a unary write reply (`Write`/`Write2`/`WriteSecured`/`WriteSecured2`; bulk writes excluded) for the matching MXAccess `OnWriteComplete` callback, so the reply's `statuses` carry the real commit outcome. `0` disables the wait (pure fire-and-forget replies). Must be `>= 0`. The gateway conveys the value to the worker via the `MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS` environment variable. Consumers that time their own writes must budget above this wait: OtOpcUa's GalaxyDriver wraps gateway writes in a 2 s Tier A resilience timeout, so a deployment raising this option past ~2000 must raise that driver `ResilienceConfig` write timeout in step or slow-but-successful commits surface as consumer-side failures. | | `MxGateway:Worker:ShutdownTimeoutSeconds` | `10` | Grace period for worker shutdown before the gateway treats shutdown as failed and may kill the worker process tree. | | `MxGateway:Worker:HeartbeatIntervalSeconds` | `5` | Worker heartbeat send interval and gateway heartbeat check cadence input. | | `MxGateway:Worker:HeartbeatGraceSeconds` | `15` | Maximum age of the last worker heartbeat before the gateway faults the worker. This must be greater than or equal to `HeartbeatIntervalSeconds`. | diff --git a/gateway.md b/gateway.md index 0f68da5..391f5a5 100644 --- a/gateway.md +++ b/gateway.md @@ -431,9 +431,10 @@ Core commands: - `AuthenticateUser` - `ArchestrAUserToId` -**Secured-write completion correlation.** MXAccess writes are fire-and-forget +**Write completion correlation.** MXAccess writes are fire-and-forget at the toolkit level — the per-item outcome only exists in the later -`OnWriteComplete` callback. For `WriteSecured` and `WriteSecured2` the worker +`OnWriteComplete` callback. For the unary write kinds — `Write`, `Write2`, +`WriteSecured`, `WriteSecured2` — the worker therefore holds the unary reply for a bounded window (`MxGateway:Worker:WriteCompletionWaitMilliseconds`, default 1.5 s, `0` disables) and, when the matching callback arrives, copies its status rows onto @@ -444,8 +445,8 @@ empty `statuses` means unconfirmed (the callback missed the window), never failed. The `OnWriteComplete` event still flows on the event stream unchanged. Correlation is best-effort per `(server_handle, item_handle)` — the callback carries no transaction id, so concurrent writes to the same item within the -window can swap rows. Plain `Write`/`Write2` and the bulk write commands stay -fire-and-forget: waiting there would add a device round-trip of latency to +window can swap rows. The bulk write commands stay +fire-and-forget: waiting per entry would add a device round-trip of latency to high-rate supervisory write loops. Bulk variants (single gRPC round-trip carries the full list, the worker