diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Messages/Mesh/MeshCommand.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Messages/Mesh/MeshCommand.cs
new file mode 100644
index 00000000..a12ab795
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Messages/Mesh/MeshCommand.cs
@@ -0,0 +1,28 @@
+namespace ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh;
+
+///
+/// A central→node command, plus the DistributedPubSub topic it would have been published on.
+///
+///
+/// The legacy DPS topic. Carried so the router can publish under
+/// MeshTransport:Mode = Dps without a second dispatch table; ignored entirely under
+/// ClusterClient mode, where the destination is an actor path rather than a topic.
+///
+///
+/// The command itself, never wrapped. Node-side handlers see exactly the type they see today,
+/// which is what lets the dark switch touch publishers only.
+///
+///
+///
+/// This envelope exists so the admin singletons stop knowing which transport is in force.
+/// Before per-cluster mesh Phase 2 each publisher held its own
+/// DistributedPubSub.Get(Context.System).Mediator call, so the switch would have had
+/// to be threaded through five call sites across two actors, and each would have been free
+/// to drift.
+///
+///
+/// Central-side only — it never crosses the wire, so it carries no serialization contract
+/// and needs no additive-evolution discipline.
+///
+///
+public sealed record MeshCommand(string Topic, object Message);
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Communication/IMeshClusterClientFactory.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Communication/IMeshClusterClientFactory.cs
new file mode 100644
index 00000000..5ea57434
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Communication/IMeshClusterClientFactory.cs
@@ -0,0 +1,47 @@
+using System.Collections.Immutable;
+using Akka.Actor;
+using Akka.Cluster.Tools.Client;
+
+namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Communication;
+
+///
+/// Creates the ClusterClient central uses to reach driver nodes. Exists as a seam so
+/// CentralCommunicationActor can be tested against a TestProbe without standing up
+/// a second cluster.
+///
+public interface IMeshClusterClientFactory
+{
+ /// Creates a client for the given receptionist contact points.
+ /// The actor system to spawn the client in.
+ /// Receptionist actor paths, one per reachable node.
+ /// The new client's .
+ IActorRef Create(ActorSystem system, ImmutableHashSet contacts);
+}
+
+///
+public sealed class DefaultMeshClusterClientFactory : IMeshClusterClientFactory
+{
+ ///
+ /// Per-incarnation generation counter, appended to the actor name.
+ ///
+ ///
+ /// Context.Stop of the previous client is asynchronous — its actor name stays
+ /// reserved until termination completes — so recreating the same name while handling a single
+ /// message throws . That is exactly what a contact-set
+ /// change does: stop the old client, create the replacement, both in one handler. A
+ /// generation suffix makes every incarnation's name unique by construction. Ported from the
+ /// sister project, where it was a shipped bug fix rather than foresight.
+ ///
+ private long _generation;
+
+ ///
+ public IActorRef Create(ActorSystem system, ImmutableHashSet contacts)
+ {
+ ArgumentNullException.ThrowIfNull(system);
+ ArgumentNullException.ThrowIfNull(contacts);
+
+ var settings = ClusterClientSettings.Create(system).WithInitialContacts(contacts);
+ var name = $"mesh-client-{Interlocked.Increment(ref _generation)}";
+ return system.ActorOf(ClusterClient.Props(settings), name);
+ }
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Communication/MeshClusterClientFactoryTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Communication/MeshClusterClientFactoryTests.cs
new file mode 100644
index 00000000..911142b7
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Communication/MeshClusterClientFactoryTests.cs
@@ -0,0 +1,67 @@
+using System.Collections.Immutable;
+using Akka.Actor;
+using Akka.Cluster.Tools.Client;
+using Akka.Configuration;
+using Akka.TestKit.Xunit2;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.ControlPlane.Communication;
+
+namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Communication;
+
+///
+/// Guards the one non-obvious part of client creation: recreating a client with the same name
+/// inside a single message handler throws, because Context.Stop is asynchronous and the
+/// name stays reserved until termination completes.
+///
+public class MeshClusterClientFactoryTests : TestKit
+{
+ private static readonly Config TestConfig = ConfigurationFactory.ParseString(@"
+ akka.actor.provider = cluster
+ akka.remote.dot-netty.tcp.port = 0
+ akka.remote.dot-netty.tcp.hostname = 127.0.0.1")
+ .WithFallback(ClusterClientReceptionist.DefaultConfig());
+
+ public MeshClusterClientFactoryTests()
+ : base(TestConfig)
+ {
+ }
+
+ // A real ClusterClient pointed at an unreachable contact is safe in TestKit — it simply retries
+ // the initial-contact rotation and never fails the test.
+ private static ImmutableHashSet UnreachableContact => ImmutableHashSet.Create(
+ ActorPath.Parse("akka.tcp://otopcua@127.0.0.1:65501/system/receptionist"));
+
+ [Fact]
+ public void Recreating_a_client_in_one_pass_does_not_collide_on_the_actor_name()
+ {
+ var factory = new DefaultMeshClusterClientFactory();
+
+ var first = factory.Create(Sys, UnreachableContact);
+ Sys.Stop(first);
+
+ // Stop is asynchronous: `first`'s name is still reserved right here. Without the generation
+ // suffix this throws InvalidActorNameException — which is precisely what a contact-set
+ // change would do in production, where stop-then-recreate happens in one handler.
+ var second = factory.Create(Sys, UnreachableContact);
+
+ second.Path.Name.ShouldNotBe(first.Path.Name);
+ }
+
+ [Fact]
+ public void Every_incarnation_gets_a_distinct_name()
+ {
+ var factory = new DefaultMeshClusterClientFactory();
+
+ var names = Enumerable.Range(0, 5)
+ .Select(_ => factory.Create(Sys, UnreachableContact).Path.Name)
+ .ToList();
+
+ names.Distinct().Count().ShouldBe(5);
+ }
+
+ // Contact-set propagation is deliberately NOT asserted here: reading initial contacts back
+ // off a created ClusterClient is not possible, and asserting ClusterClientSettings directly
+ // would test Akka rather than this factory. It is covered where it can actually fail — the
+ // real two-ActorSystem boundary test, which only delivers if the contacts reached the client.
+}