diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/ItemHandleReuseProbeTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/ItemHandleReuseProbeTests.cs
new file mode 100644
index 0000000..6557b3f
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/ItemHandleReuseProbeTests.cs
@@ -0,0 +1,160 @@
+// ItemHandleReuseProbeTests.cs
+//
+// Probe for issue #166: does MXAccess hand out a new item handle number when the
+// same item is removed and re-added on one LMXProxyServer registration? The
+// AlarmAckWriter's Unconfirmed mitigation (drop the item, re-add it, wait on the
+// fresh handle) relies on the number changing, because OnWriteComplete carries
+// only the (server, item) handle pair.
+//
+// The probe only adds, advises and removes items. It never writes, so it does
+// not acknowledge the alarm or create Historian rows.
+//
+// HOW TO RUN (dev rig with MXAccess installed and the DEV Galaxy running):
+// 1. Remove (or set to null) the Skip parameter on the [Fact] below.
+// 2. dotnet test src/ZB.MOM.WW.MxGateway.Worker.Tests/ZB.MOM.WW.MxGateway.Worker.Tests.csproj
+// -p:Platform=x86 --filter FullyQualifiedName~ItemHandleReuseProbeTests
+// --logger "console;verbosity=detailed"
+// 3. Read the logged handles. Results are recorded in docs/AlarmProbeFindings.md.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Threading;
+using ArchestrA.MxAccess;
+using ZB.MOM.WW.MxGateway.Worker.MxAccess;
+using Xunit.Abstractions;
+
+namespace ZB.MOM.WW.MxGateway.Worker.Tests.Probes;
+
+///
+/// Live probe that logs the item handles MXAccess returns when the same
+/// AckMsg item is removed and re-added, to decide whether a late
+/// OnWriteComplete for a dropped handle could match a re-added one.
+///
+public sealed class ItemHandleReuseProbeTests
+{
+ private const string AckItem = "TestMachine_001.TestAlarm001.AckMsg";
+ private const string OtherItem = "TestMachine_001.TestAlarm001.Acked";
+ private const int Rounds = 5;
+
+ private readonly ITestOutputHelper output;
+ private readonly List log = new List();
+
+ /// Initializes a new instance of the class.
+ /// Test output helper for logging.
+ public ItemHandleReuseProbeTests(ITestOutputHelper output)
+ {
+ this.output = output;
+ }
+
+ /// Adds, removes and re-adds the AckMsg item on a fresh registration and logs every handle.
+ [Fact(Skip = "Live dev-rig probe (issue #166) — flip Skip=null on a host with MXAccess and the DEV Galaxy running.")]
+ public void RemoveThenReAdd_LogsItemHandles()
+ {
+ Exception? threadException = null;
+ Thread thread = new Thread(() =>
+ {
+ try { RunProbe(); }
+ catch (Exception ex) { threadException = ex; }
+ });
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ thread.Join();
+
+ foreach (string line in log)
+ {
+ output.WriteLine(line);
+ }
+
+ if (threadException is not null)
+ {
+ throw new InvalidOperationException("Probe failed: " + threadException.Message, threadException);
+ }
+ }
+
+ private void RunProbe()
+ {
+ LMXProxyServerClass com = new LMXProxyServerClass();
+ try
+ {
+ IMxAccessServer mx = new MxAccessComServer(com);
+ int server = mx.Register("MxGatewayProbe166");
+ Log($"Register -> server handle {server}");
+
+ // A: the AlarmAckWriter drop path (Advise, then UnAdvise + RemoveItem).
+ for (int i = 0; i < Rounds; i++)
+ {
+ int h = mx.AddItem(server, AckItem);
+ mx.Advise(server, h);
+ Log($"A{i}: AddItem+Advise {AckItem} -> {h}");
+ mx.UnAdvise(server, h);
+ mx.RemoveItem(server, h);
+ Log($"A{i}: UnAdvise+RemoveItem {h}");
+ }
+
+ // B: RemoveItem without Advise.
+ for (int i = 0; i < Rounds; i++)
+ {
+ int h = mx.AddItem(server, AckItem);
+ Log($"B{i}: AddItem {AckItem} -> {h}");
+ mx.RemoveItem(server, h);
+ Log($"B{i}: RemoveItem {h}");
+ }
+
+ // C: interleave another item to see whether a freed number is handed to any item.
+ int c1 = mx.AddItem(server, AckItem);
+ Log($"C: AddItem {AckItem} -> {c1}");
+ mx.RemoveItem(server, c1);
+ Log($"C: RemoveItem {c1}");
+ int c2 = mx.AddItem(server, OtherItem);
+ Log($"C: AddItem {OtherItem} -> {c2}");
+ int c3 = mx.AddItem(server, AckItem);
+ Log($"C: AddItem {AckItem} -> {c3}");
+ mx.RemoveItem(server, c2);
+ mx.RemoveItem(server, c3);
+ Log($"C: RemoveItem {c2}, {c3}");
+
+ // D: the same item added twice while the first is still held.
+ int d1 = mx.AddItem(server, AckItem);
+ int d2 = mx.AddItem(server, AckItem);
+ Log($"D: AddItem {AckItem} twice while held -> {d1}, {d2}");
+ mx.RemoveItem(server, d1);
+ mx.RemoveItem(server, d2);
+
+ mx.Unregister(server);
+ Log($"Unregister {server}");
+
+ // E: the TearDownServer path: re-register on the same COM object and add again.
+ int server2 = mx.Register("MxGatewayProbe166");
+ int e1 = mx.AddItem(server2, AckItem);
+ Log($"E: re-Register -> server handle {server2}; AddItem {AckItem} -> {e1}");
+ mx.RemoveItem(server2, e1);
+ mx.Unregister(server2);
+ }
+ finally
+ {
+ Marshal.FinalReleaseComObject(com);
+ }
+
+ // F: a fresh COM object, as AlarmAckWriter creates after a teardown.
+ LMXProxyServerClass com2 = new LMXProxyServerClass();
+ try
+ {
+ IMxAccessServer mx2 = new MxAccessComServer(com2);
+ int server3 = mx2.Register("MxGatewayProbe166");
+ int f1 = mx2.AddItem(server3, AckItem);
+ Log($"F: new COM object Register -> server handle {server3}; AddItem {AckItem} -> {f1}");
+ mx2.RemoveItem(server3, f1);
+ mx2.Unregister(server3);
+ }
+ finally
+ {
+ Marshal.FinalReleaseComObject(com2);
+ }
+ }
+
+ private void Log(string line)
+ {
+ log.Add(DateTime.Now.ToString("HH:mm:ss.fff") + " " + line);
+ }
+}