{ "planPath": "docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md", "note": "Per-cluster mesh Phase 2. Decisions pre-made: config-flagged dark switch (MeshTransport:Mode), buffer-size=0, alarm-commands central leg in scope. Three design-doc corrections and the single-mesh duplicate-delivery trap are documented in the plan header \u2014 read them before Task 3.", "tasks": [ { "id": 0, "subject": "Task 0: MeshTransportOptions + validator + registration", "status": "completed", "classification": "small", "parallelizableWith": [ 1 ], "commit": "5439f148" }, { "id": 1, "subject": "Task 1: receptionist extension, buffer-size=0, frame-size logging (effective-config test)", "status": "completed", "classification": "small", "parallelizableWith": [ 0 ], "commit": "7b71a6a3", "note": "First draft of the buffering test was VACUOUS -- Config.GetInt returns 0 for a missing key, so it passed before the feature existed. Rewritten to assert through ClusterClientSettings/ClusterReceptionistSettings." }, { "id": 2, "subject": "Task 2: MeshCommand envelope + generation-suffixed ClusterClient factory", "status": "completed", "classification": "small", "blockedBy": [ 0 ], "commit": "0d6669d5", "note": "Dropped a third test that asserted ClusterClientSettings directly and never touched the factory." }, { "id": 3, "subject": "Task 3: CentralCommunicationActor \u2014 dark-switched SendToAll fan-out, DB-sourced contacts, ApplyAck forward", "status": "completed", "classification": "high-risk", "blockedBy": [ 1, 2 ], "parallelizableWith": [ 4 ], "commit": "d5b5cb6e", "note": "SendToAll sabotage-verified. Two test-harness bugs found: SubscribeAck goes to the SENDER, and EventFilter matches case-INSENSITIVELY (a 'was NOT' filter also caught 'was not created')." }, { "id": 4, "subject": "Task 4: NodeCommunicationActor \u2014 inbound to node-local EventStream, outbound ApplyAck", "status": "completed", "classification": "standard", "blockedBy": [ 1, 2 ], "parallelizableWith": [ 3 ], "commit": "915beec1", "note": "No Ask crosses the boundary in Phase 2, so this actor is far smaller than the sister project's." }, { "id": 5, "subject": "Task 5: node subscribers accept EventStream too; rename _coordinatorOverride -> _ackRouter", "status": "completed", "classification": "standard", "blockedBy": [ 4 ], "commit": "5cb0e721", "note": "Two wiring tests added -- unit tests either side of the seam both pass with the subscription deleted. First draft raced (ActorOf returns before PreStart; EventStream publish to nobody is dropped silently); alarm version was vacuous until the warm-up moved outside the assertion window." }, { "id": 6, "subject": "Task 6: wire both sides, receptionist registration, pin the wire-contract actor paths", "status": "completed", "classification": "standard", "blockedBy": [ 3, 5 ], "commit": "16d59856", "note": "Dropped a planned 'not a singleton' assertion: two candidate discriminators were both invalid (a SingletonManager also sits at /user/; /singleton is null even for the known singleton config-publish). A sabotage re-registering via WithSingleton left both green, which exposed them. Property deferred to Task 8." }, { "id": 7, "subject": "Task 7: route the five publish sites through the mesh router (nullable, mediator fallback)", "status": "completed", "classification": "standard", "blockedBy": [ 6 ], "note": "Sabotage-verified by inverting the branch: all 6 tests red. Comm actor registration moved above the singletons so registry.Get works; an earlier ResolveOne().GetAwaiter().GetResult() blocked inside a props factory." }, { "id": 8, "subject": "Task 8: real two-ActorSystem ClusterClient boundary test + falsifiability control", "status": "completed", "classification": "high-risk", "blockedBy": [ 7 ], "parallelizableWith": [ 9 ], "commit": "5468b79d", "note": "Not flaky (3/3 clean repeats; 4 tests, ~16s). Both legs sabotage-verified against production code, and the control was itself falsified by restoring the receptionist. The control necessarily removes RegisterService too -- registering would materialise the extension it is removing. Task 6's dropped per-node/singleton property does NOT land here (one node per side cannot discriminate); it is live-gate step 8, and the MeshCommActorPathTests comment was corrected to say so." }, { "id": 9, "subject": "Task 9: docker-dev rig config + docs + design-doc correction", "status": "completed", "commit": "e4cb9a08", "note": "Rig Mode is ${OTOPCUA_MESH_MODE:-Dps} rather than a hardcoded Dps, so Task 10 can flip all six nodes at `docker compose up` with no compose edit and no rebuild. Verified via `docker compose config` (6 services resolved).", "classification": "small", "blockedBy": [ 7 ], "parallelizableWith": [ 8 ] }, { "id": 10, "subject": "Task 10: live gate on docker-dev (11 steps; step 4 = deploy-seal semantics, step 8 = per-node/contact-rotation discriminator [replaces the dropped Task 6 assertion \u2014 do not skip], step 9 = both-centrals drop behavior)", "status": "completed", "note": "PASSED for the transport (steps 1-5, 8, 11 green; flip end-to-end via OTOPCUA_MESH_MODE=ClusterClient). Record: 2026-07-22-mesh-phase2-live-gate.md. Steps 6/7 NOT RUN (no UI surface for driver-control \u2014 finding D, DriverStatusPanel is orphaned; no alarms configured). Step 8's stated log-signal does not exist (HandleApplyAck is silent on success \u2014 finding E); proven instead by both centrals independently logging per-instance ClusterClient creation + rebuild, which a singleton cannot. FOUR findings: A) 10-30s post-restart delivery window is PRE-EXISTING (DPS control failed identically); B) buffer-size=0 rationale OVERSTATED \u2014 a node applies a TimedOut deploy anyway via Bootstrap() orphan-row replay at +44s, corrected in 3 docs; C) stopping BOTH centrals strands non-seed nodes at the membership layer (Phase 7 drill, transport-independent); D) driver Restart/Reconnect UI orphaned since v3 Batch 2. Rig restored to Dps default.", "classification": "high-risk", "blockedBy": [ 8, 9 ] } ], "lastUpdated": "2026-07-22T00:00:00Z" }